Bootstrap

Linux文件:缓冲区、缓冲区刷新机制 | C库模拟实现

一、缓冲区的作用

 缓冲区本质上就是一部分内存,用于提高效率!!

 对于文件的IO等操作,用户可以直接通过系统调用直接向操作系统进行读操作和写操作。但这样时间开销较大。所以在语言层面一般会维护一段语言级别的缓冲区,用于暂存数据。我们可以快速向缓冲区中写入数据,然后通过一定的刷新方式。将数据从语言级别的缓冲区中拷贝到内核缓冲区。大大提高使用者的效率!!

&mesp;同时由于缓冲区的存在,我们可以积累一定的数据后在统一发生,提高发送的效率!!

二、缓冲区的刷新机制

 缓冲区可以暂存数据,必定存在一定的刷新机制。常见的刷新机制由以下3种:

  1. 无缓冲(立即刷新)
  2. 行缓冲(行刷新)
  3. 全缓冲(缓冲区全部写满后在刷新)

其中显示器文件的刷新机制就是行刷新;对于磁盘上的文件则是全缓冲!!

除此之外,还存在一些特殊的刷新机制:

  • 强制刷新。比如调用flush函数,以及printf在执行时,碰到/n时强制刷新!
  • 进程退出时,一般会刷新缓冲区!

三、测试样例解析

3.1 测试样例和运行结果

 下面我们调用3个常见的C库函数和一个系统调用,都向显示器文件中进行写入。当写入操作完毕后创建子进程,我们来看看分别向显示器文件磁盘文件中进行写入会发生什么?

int main()    
{    
    fprintf(stdout, "C: hello fprintf\n");    
    printf("C: hello printf\n");                                          
    fputs("C: hello fputs\n", stdout);    
    const char* buf = "system call: hello write\n";    
    write(1, buf, strlen(buf));    
    fork();    
    return 0;    
} 

 我们编译后,直接运行向显示器写入。然后通过输出重定向向log.txt磁盘文件进行写入:

【运行结果】:

在这里插入图片描述

3.2 结果分析

1、向显示器文件写入:

 当我们直接向显示器打印消息时,由于显示器的刷新机制是行刷新;并且我们所打印的字符串都带了‘\n(在printf中,'\n'是一种强制刷新的触发机制)。所以在fork()创建子进程前,数据已经全部被刷新!!

2、向磁盘文件进行写入:

 当我们重定向向磁盘文件写入数据时,缓冲区的刷新机制由行缓冲变成全缓冲。全缓冲也就意味着缓冲区变大,简单的几个字符串不足以将缓冲区写满,无法触发刷新机制

 此时缓冲数据还在缓冲区但当前缓冲区为C语言所提供的缓冲区,和操作系统无关。所以当前缓冲区中的数据依然属于进程。当fork()创建完子进程退出时,一般会刷新缓冲区。

 而刷新缓冲区本质上也是一种清空或者写入操作。所以父进程和子进程在退出前数据共享同一份;当退出时刷新缓冲区,对数据进行修改会发生写时拷贝,父、子进程各自私有一份。所以最后对于C函数调用会存在两份!!

 至于系统调用数据只打印一份的原因在于:系统调用在语言之下,数据不是向语言基本的缓冲区中写入;而是直接向操作系统内核缓冲区中写入。此时数据属于操作系统,不在属于进程!!

四、语言级别的缓冲区究竟在哪?

 下面我们以C为例。

 在此时样例中,我们已经知道对于库函数printf、fprintf自带缓冲区,而系统调用write则没有。(内核提提供的缓冲区此处不考虑)库函数时对系统调用的封装,这也意味着C所提供的缓冲区是二次加上的,由C本身所提供!!

 在C中,所有的IO函数都存在一个FILE指针,或者底层会封装FILE指针。而C的缓冲区的相关信息则保存在FILE结构体中,由FILE结构体来维护!!

FILE结构体内容】:
 在/usr/include/stdio.h路径下,存在这样一段代码typedef struct _IO_FILE FILE。所以可以通过下面操作查找FILE:
在这里插入图片描述

FILE结构体】:

/usr/include/libio.h
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
};

五、C库函数封装简单模拟

 这里博主仅简单封装小部分C库函数,主要用于验证缓冲区。
【待实现接口函数】:

extern myFILE *my_fopen(const char *path, const char *mode); //打开文件
extern int my_fwrite(const char *s, int num, myFILE *stream);             
extern int my_fflush(myFILE *stream);    //刷新文件
extern int my_fclose(myFILE *fp); //关闭文件

5.1 结构体封装内容

 这里我们在结构体中封装文件描述符、缓冲区、刷新策略以及有效数据范围!!

  1. 这里我们实现的刷新策略只有三种:无缓冲、行刷新、全缓冲。
  2. 有效数据范围本应该通过一些指针来维护,这里博主简化为:有效数据从0开始,用有效数据个数来间接代替有效数据范围!
#define SIZE 4096    
#define FLUSH_NONE 1    
#define FLUSH_LINE (1<<1)    
#define FLUSH_ALL  (1<<2)

typedef struct _myFILE    
{    
    char buffer[SIZE];	//缓冲区    
    int end;	//简化有效空间范围,从0开始    
    int flag;	//标志位,刷新策略    
    int fileno;    //文件描述符
}myFILE;

5.2 文件打开接口封装

 文件打开:

  1. 首先我们需要先获取文件的打开方式(这里博主仅实现w、r、a3种)
  2. 获取到打开方式后,我们需要判断文件是否存在。存在,直接通过系统调用正确打开文件;否则需要先创建文件。
  3. 既然文件打开了,最后就是修改结构体myFILE中的内容了!(这里默认创建文件的刷新方式为行刷新 )

【源代码】:

#define FILE_MODE 0666//文件创建默认权限

myFILE *my_fopen(const char *path, const char *mode)
{
    int flag = 0;
    int fd = 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
    {
        //do nothing
    }  
 //-------------------------------------------------------------------------        
     //打开文件
    if(flag & O_CREAT)//文件需要被创建
    {
        fd = open(path, flag, FILE_MODE);                                 
    }
    else
    {
        fd = open(path, flag);
    }
    //文件打开失败
    if(fd < 0)
    {
        errno = 2;//没有该文件
        return NULL;
    }
 //------------------------------------------------------------------------- 
	//修改结构体内容,默认创建文件的刷新方式为行刷新 
    myFILE *fp = (myFILE*)malloc(sizeof(myFILE));
    if(!fd)
    {
        errno = 3;
        return NULL;
    }

    fp->flag = FLUSH_LINE;
    fp->fileno = fd;
    fp->end = 0;
    return fp;
}
        

5.3 文件关闭和文件刷新

  1. 关闭文件后,进程一般会将文件中的内容进行刷新。
  2. 但对于刷新而言,如果文件中由内容需要被刷新,我们直接调用系统调用接口即可完成!!
int my_fflush(myFILE *stream)
{
    if(stream->end > 0)//存在内容需要刷新
    {
        write(stream->fileno, stream->buffer, stream->end);
        //fscnc(stream->fileno);//强制内核缓冲区刷新
        stream->end = 0;
    }
    return 0;
}

int my_fclose(myFILE *stream)
{
    my_fflush(stream);//刷新文件内容,是否存在内容需要刷新我们由my_fflush函数来判断
    return close(stream->fileno);
}

5.4 向显示器文件写入

 C库函数中fwrite的原型如下,这里博主简化直接传递数据个数,大小为1byte!!
在这里插入图片描述

  1. 向显示器文件中写入时,我们需要判断是否存在字符\n是会触发`fwrite’刷新机制,刷新缓冲区!
int my_fwrite(const char *s, int num, myFILE *stream)                     
{
    //写入
    memcpy(stream->buffer + stream->end, s, num + 1);
    stream->end += num;

    //判断是否需要刷新
    int i = stream->end - 1;
    if((stream->flag & FLUSH_LINE) && stream->end>0)
    {
        while(i)//从后往前遍历查找是否存在'\n'
        {
            if(stream->buffer[i] == '\n')
            {
                 my_fflush(stream);
                 break;
            }
            i--;
        }
    }
    return i == 0 ? 0 : num;//返回刷新个数
}

;