【Linux】文件缓冲区

目录

一、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
#include
#include    
#include    
#include    

#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;
}

运行效果:

【Linux】文件缓冲区_第1张图片

二、 引入

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

#include
#include
#include    
#include    
#include    

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

运行一下: 

【Linux】文件缓冲区_第2张图片

咦?怎么重定向了之后多向文件中打印了一个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 
#include 
#include 
#include 
#include 
#include 
#include 
#include 


#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结构体也有自己的缓冲区:

【Linux】文件缓冲区_第3张图片

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

我们来看到下图:

【Linux】文件缓冲区_第4张图片

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

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

4.1 fsync

#include 
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;
}

下期不见不散~

你可能感兴趣的:(Linux,linux,运维,服务器,c语言)