【Linux】文件缓冲区

2023-09-20 17:30:39

目录

一、dup2

二、 引入

三、C语言FILE中的缓冲区

3.1 缓冲区的作用

3.2 缓冲区的刷新机制

3.3 对引入代码现象的解释

3.4 模拟实现C语言中的FILE

四、文件系统中的缓冲区

4.1 fsync


在本期内容正式开始之前,我们先介绍一个上期遗漏的知识点:

一、dup2

我们如果要想用代码实现重定向,先要关闭对应的标准流文件,再使用open函数打开需要的文件,来占据原本关闭的标准流的文件描述符,最终实现重定向

还要关闭原本的标准流文件,这样子是不是太麻烦了

下面我们来介绍一个函数dup2(包含在头文件unistd.h中),可以直接替换文件的文件描述符:

int dup2(int oldfd, int newfd);
// dup2() makes newfd be the copy of oldfd, closing newfd first if necessary

● 该函数会将newfd文件描述符所对应的文件,替换成oldfd文件描述符所对应的文件

我们来试试看:

#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>    
#include<sys/stat.h>    
#include<fcntl.h>    

#define LOG "test.txt"

int main()
{
  int fd = open(LOG,O_WRONLY | O_APPEND | O_CREAT , 0666);
  dup2(fd,1);
  printf("You can see me\n");    
  printf("You can see me\n");    
  printf("You can see me\n");    
  printf("You can see me\n");
  close(fd);
  return 0;
}

运行效果:

二、 引入

我们先来看下面的一段代码:

#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>    
#include<sys/stat.h>    
#include<fcntl.h>    

int main()
{
  const char* n="Welcome to my blog\n";
  fprintf(stdout,n);
  write(1,n,strlen(n));
  fork();
  return 0;
}

运行一下: 

咦?怎么重定向了之后多向文件中打印了一个Welcome to my blog?

下面我们来慢慢分析

三、C语言FILE中的缓冲区

3.1 缓冲区的作用

我们知道在C语言中使用文件操作会有FILE*的文件指针,指向一个FILE类型的结构体:

struct _IO_FILE {
	int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags
	//缓冲区相关
	/* The following pointers correspond to the C++ streambuf protocol. */
	/* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
	char* _IO_read_ptr; /* Current read pointer */
	char* _IO_read_end; /* End of get area. */
	char* _IO_read_base; /* Start of putback+get area. */
	char* _IO_write_base; /* Start of put area. */
	char* _IO_write_ptr; /* Current put pointer. */
	char* _IO_write_end; /* End of put area. */
	char* _IO_buf_base; /* Start of reserve area. */
	char* _IO_buf_end; /* End of reserve area. */
	/* The following fields are used to support backing up and undo. */
	char* _IO_save_base; /* Pointer to start of non-current get area. */
	char* _IO_backup_base; /* Pointer to first valid character of backup area */
	char* _IO_save_end; /* Pointer to end of non-current get area. */
	struct _IO_marker* _markers;
	struct _IO_FILE* _chain;
	int _fileno; //封装的文件描述符
#if 0
	int _blksize;
#else
	int _flags2;
#endif
	_IO_off_t _old_offset; /* This used to be _offset but it's too small. */
#define __HAVE_COLUMN /* temporary */
	/* 1+column number of pbase(); 0 is unknown. */
	unsigned short _cur_column;
	signed char _vtable_offset;
	char _shortbuf[1];
	/* char* _save_gptr; char* _save_egptr; */
	_IO_lock_t* _lock;
#ifdef _IO_USE_OLD_IO_FILE
};

在这个结构体中会有文件缓冲区的存在,就是在FILE结构体内部动态开辟的一块空间

这个文件缓冲区有什么用呢?在我们使用fwrite、fread这些函数时,这是一个IO的过程。IO的次数越多,那么时间浪费越多,效率越低。如果使用一个东西预先存储这些数据,当到达一定规模时统一写入文件,那么IO的次数就会减少,进而效率提升。

因此,缓冲区存在的意义就是通过减少IO次数达到效率上的提升

那这个缓冲区什么时候与文件进行数据交换呢?这就涉及到缓冲区的刷新机制了:

3.2 缓冲区的刷新机制

缓冲区共有四种刷新规则:立即刷新(无缓冲)、行刷新(行缓冲)、满刷新(全缓冲)、强制刷新

立即刷新(无缓冲):所谓立即刷新就是字面意义,每当向缓冲区中写入数据就刷新一次。

行刷新(行缓冲):当在数据中检测到换行符'\n'时刷新,这叫行刷新。

比如向显示器printf("hello world\n");时会执行行刷新。

满刷新(全缓冲):当缓冲区中数据已满时刷新,这叫满刷新。

强制刷新:即由人强制缓冲区执行刷新操作,例如fflush就是一种强制刷新。

不同文件对应刷新规则不同,这是主要是由文件使用性质决定的。

对于标准输入输出流(显示器、键盘)来说,其采用行刷新策略。

对于磁盘文件来说,其采用满刷新策略。

3.3 对引入代码现象的解释

在我们第一次运行代码时并没有重定向,所以默认输出流stdout是显示器文件,其对应的缓冲区刷新机制为行刷新(行缓冲),所以在调用fprintf函数时会立即打印传入的字符串

但是在第二次运行时,我们输出重定向了,重定向文件为test.txt,对于磁盘文件来说,其采用满刷新策略。所以在我们调用fprintf函数时不会立即打印传入的字符串(因为这些字符串还不足以将缓冲区填满),不过调用系统级接口write时会立即写入文件(因为系统级接口中并没有缓冲区),在最后fork函数创进程时,子进程会复制父进程的缓冲区。所以在程序最后结束对缓冲区进行刷新时,会写实拷贝出两个缓冲区,两个缓冲区的内容都要刷新到文件中,就造成了最后的现象

3.4 模拟实现C语言中的FILE

下面我们写一个小demo,简单模拟实现一下C语言中的FILE:

#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <malloc.h>
#include <unistd.h>
#include <assert.h>


#define NUM 1024
#define BUFF_NONE 0x1//无缓冲
#define BUFF_LINE 0x2//行缓冲
#define BUFF_ALL  0x4//全缓冲

typedef struct _MY_FILE
{
    int fd;
    int flags; //缓冲区的刷新机制
    char outputbuffer[NUM];//文件缓冲区
    int  current;//记录文件缓冲区的存贮字符的位数
} MY_FILE;




// my_fopen("/a/b/c.txt", "a");
// my_fopen("/a/b/c.txt", "r");
// my_fopen("/a/b/c.txt", "w");
MY_FILE *my_fopen(const char *path, const char *mode)
{
    //根据传入的mode参数识别标志位
    int flag = 0;
    if(strcmp(mode, "r") == 0) flag |= O_RDONLY;
    else if(strcmp(mode, "w") == 0) flag |= (O_CREAT | O_WRONLY | O_TRUNC);
    else if(strcmp(mode, "a") == 0) flag |= (O_CREAT | O_WRONLY | O_APPEND);
    else {
        //other operator...
        //"r+", "w+", "a+"
    }
    mode_t m = 0666;//创建文件时的权限
    int fd = 0;
    //判断是否有O_CREAT标志位来决定打开文件的方式
    if(flag & O_CREAT) fd = open(path, flag, m);
    else fd = open(path, flag);

    if(fd < 0) return NULL;//打开失败

    //构建MY_FILE对象最后返回
    MY_FILE *mf = (MY_FILE*)malloc(sizeof(MY_FILE));
    if(mf == NULL) 
    {
        close(fd);
        return NULL;
    }

    //初始化MY_FILE对象
    mf->fd = fd;
    mf->flags = 0;
    mf->flags |= BUFF_LINE;//默认所有文件都进行行缓冲
    memset(mf->outputbuffer, '\0',sizeof(mf->outputbuffer));
    mf->current = 0;
    //返回文件对象
    return mf;
}

int my_fflush(MY_FILE *fp)
{
    assert(fp);
    //将用户缓冲区中的数据,通过系统调用接口write,冲刷给OS
    write(fp->fd, fp->outputbuffer, fp->current);
    fp->current = 0;//刷新完置0
    return 0;
}

size_t my_fwrite(const void *ptr, size_t size, size_t nmemb,MY_FILE *stream)
{
    // 缓冲区如果已经满了,就直接写入
    if(stream->current == NUM) my_fflush(stream);

    //根据缓冲区剩余情况,进行数据拷贝即可
    size_t user_size = size * nmemb;//用户要写入的字节数
    size_t my_size = NUM - stream->current; //缓冲区剩余空间的字节数

    size_t writen = 0;//writen记录实际写入的字节数
    if(my_size >= user_size)//缓冲区剩余容量大于等于用户写入的字节数
    {
        memcpy(stream->outputbuffer+stream->current, ptr, user_size);//直接将用户数据拷贝到缓冲区
        //更新计数器字段
        stream->current += user_size;
        writen = user_size;
    }
    else//缓冲区剩余容量小于用户写入的字节数
    {
        memcpy(stream->outputbuffer+stream->current, ptr, my_size);//剩余多少空间就写入多少字节
        //更新计数器字段
        stream->current += my_size;
        writen = my_size;
    }
    
    //判断刷新机制,根据不同的刷新机制执行不同的方法
    if(stream->flags & BUFF_ALL)//全缓冲
    {
        if(stream->current == NUM) my_fflush(stream);
    }
    else if(stream->flags & BUFF_LINE)//行缓冲
    {
        if(stream->outputbuffer[stream->current-1] == '\n') my_fflush(stream);
    }
    else if(stream->flags & BUFF_NONE)//无缓冲
    {
        my_fflush(stream);
    }
    return writen;
}

int my_fclose(MY_FILE *fp)
{
    assert(fp);
    //冲刷缓冲区
    if(fp->current > 0) my_fflush(fp);
    //关闭文件
    close(fp->fd);
    //释放空间
    free(fp);
    fp = NULL;
    return 0;
}

四、文件系统中的缓冲区

我们在上期博客中也说到过,文件系统中的file结构体也有自己的缓冲区:

那这个缓冲区和C语言中FILE结构体中缓冲区有什么相似之处呢?

我们来看到下图:

我们可以看到,如果我们作为用户调用c标准库中的fopen、fwrite之类的函数,其会将对应FILE结构体中的缓冲区的数据,利用系统调用接口与进程进行交互。 进程再拿着数据交给文件系统中的file结构体中的缓冲区,这个缓冲区的数据最终会被OS刷新到磁盘中(但是对于OS怎么来刷新缓冲区中的数据我们目前还无法弄清楚,它有其自己对应的刷新机制)。

但是系统提供了一个函数接口用来强制刷新文件系统中的缓冲区:fsync

4.1 fsync

#include <unistd.h>
int fsync(int fd);

该函数可以将fd对应的文件描述符中的缓冲区的内容刷新到磁盘中

所以我们可以在之前模拟实现的my_fflush函数中强制刷新一下文件系统中的缓冲区:

int my_fflush(MY_FILE *fp)
{
    assert(fp);
    //将用户缓冲区中的数据,通过系统调用接口write,冲刷给OS
    write(fp->fd, fp->outputbuffer, fp->current);
    fp->current = 0;//刷新完置0
    fsync(fp->fd);//强制刷新文件系统的缓冲区到磁盘
    return 0;
}

下期不见不散~

更多推荐

【Python】使用 pyecharts 模块绘制动态时间线柱状图 ① ( 列表排序 | 使用 sorted 函数对容器进行排序 | 使用 list.sort 函数对列表进行排序 | 设置排序函数 )

文章目录一、列表排序1、使用sorted函数对容器进行排序2、使用list.sort函数对列表进行排序3、使用list.sort函数对列表进行排序-设置排序函数4、使用list.sort函数对列表进行排序-设置lambda匿名排序函数pyecharts画廊网站:https://gallery.pyecharts.org

健康云HIS系统源码,满足基层医疗机构业务需求,提供挂号支持、病患问诊、电子病历、开药发药、会员管理、统计查询、医生站和护士站等功能

云his系统源码二级医院HIS系统全套源代码自主研发,自主版权一款满足基层医疗机构各类业务需要的健康云HIS系统。该系统能帮助基层医疗机构完成日常各类业务,提供病患挂号支持、病患问诊、电子病历、开药发药、会员管理、统计查询、医生站和护士站等一系列常规功能,能与公卫、PACS等各类外部系统融合,实现多层机构之间的融合管理

怎样定制开发小程序微商城_流程_报价_OctShop

互联网和5G的快速发展,变化速度1天比1天快了,小程序微商城的开发也这在浪潮中得到了蓬勃发展,小程序微商城在我们的生活当中已经非常普通了,很多人通过扫描小程序微商城二维码,进入小程序微商城进行购物。随着互联网与5G的发展,很多企业或商家通过微信生态,如:微信群,朋友圈等来发展自己企业的业务。如果我们想通过朋友圈或微信群

Redis代码实践总结

一、背景:redis从安装到实践,做一些具体的记录。1.1Redis和RedisStack和RedisEnterpriseredis简介Redis是一种开源(BSD许可)内存中数据结构存储,用作数据库、缓存、消息代理和流引擎。Redis提供数据结构,例如字符串、散列、列表、集合、带范围查询的排序集、位图、超级日志、地理

别再盯着40系,这些才是目前性价比最高的显卡

有人说,当前畸形的显卡市场成了咱们升级电脑配置的最大阻碍。在小忆看来这话说得还真没啥毛病!CPU、主板、内存、硬盘、电源,哪个不是一台电脑中的重要核心硬件;它们飘了吗?没有,各个品牌在竞争中相互制约,价格呢也都维持在一个相对合理的状态。唯独显卡领域,NVIDIA独领风骚,彻底掌控定价权。RTX40系列疯狂挤牙膏价格不降

回归测试策略指南

作为一名软件测试人员,我们需要进行回归测试,以确保代码修改后软件的既有功能不会受到影响。那么如何设计和执行有效的回归测试策略呢?本文将为大家提供一些专业建议。明确回归测试的范围回归测试不可能也不需要对软件做完整测试,要识别出核心功能和关键业务场景,将回归测试的范围控制在可管理的范围内。比如在一个电商网站修改了订单模块代

实战经验分享:如何通过HTTP代理解决频繁封IP问题

在网络爬虫和数据采集等应用中,频繁遇到目标网站封锁或限制IP的情况是非常常见的。为了解决这个问题,使用HTTP代理是一种有效的方法。本文将与您分享一些实战经验,帮助您通过HTTP代理解决频繁封IP问题,确保您的数据采集工作顺利进行。一、了解频繁封IP问题频繁封IP问题是指目标网站采取措施检测并封锁过多请求来自同一IP地

Spring 篇

1、什么是Spring?Spring是一个轻量级的IOC和AOP容器框架。是为Java应用程序提供基础性服务的一套框架,目的是用于简化企业应用程序的开发,它使得开发者只需要关心业务需求。常见的配置方式有三种:基于XML的配置、基于注解的配置、基于Java的配置。主要由以下几个模块组成:SpringCore:核心类库,提

RFID技术在工业智能制造生产线中的应用

随着自动化和信息化的快速发展,工业智能制造成为制造业的重要趋势,在制造商的生产线上,准确获取和管理工艺流程等各个环节的信息至关重要,作为物联网感知层的核心组成部分,RFID技术以其非接触式、无感知的特点,实现了智能化的识别和数据采集,通过RFID电子标签实现了设备的互联,在复杂的工业制造环境中,结合RFID电子标签、R

xterm使用

xterm使用前言1.xterm介绍2.xterm使用2.1xterm简单示例2.2xterm监听输入并在终端中实时显示方式1:onKey监听方式2:onData监听onData和onKey什么区别2.3xterm与vue整合2.3xterm+vue+websocket附录配置说明前言vue与xterm整合记录1.xt

让开源数据开发平台助力提质增效!

用低代码技术平台,可以提高办公协作效率,可以让数据资源变得更有意义和价值,也可以为企业做出更理想的发展决策。作为开源数据开发平台服务商,流辰信息谨守研发初心,一直在低代码技术平台领域努力耕耘,为行业的进步和数字化发展贡献力量。由于社会的发展和进步,传统的表单制作工具已经无法为企业创造高效益,如果想要获得发展和壮大,需要

热文推荐