Linux文件:缓冲区、缓冲区刷新机制 | C库模拟实现
- 一、缓冲区的作用
- 二、缓冲区的刷新机制
- 三、测试样例解析
- 3.1 测试样例和运行结果
- 3.2 结果分析
- 1、向显示器文件写入:
- 2、向磁盘文件进行写入:
- 四、语言级别的缓冲区究竟在哪?
- 五、C库函数封装简单模拟
- 5.1 结构体封装内容
- 5.2 文件打开接口封装
- 5.3 文件关闭和文件刷新
- 5.4 向显示器文件写入
一、缓冲区的作用
缓冲区本质上就是一部分内存,用于提高效率!!
对于文件的IO等操作,用户可以直接通过系统调用直接向操作系统进行读操作和写操作。但这样时间开销较大。所以在语言层面一般会维护一段语言级别的缓冲区,用于暂存数据。我们可以快速向缓冲区中写入数据,然后通过一定的刷新方式。将数据从语言级别的缓冲区中拷贝到内核缓冲区。大大提高使用者的效率!!
&mesp;同时由于缓冲区的存在,我们可以积累一定的数据后在统一发生,提高发送的效率!!
二、缓冲区的刷新机制
缓冲区可以暂存数据,必定存在一定的刷新机制。常见的刷新机制由以下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 0int _blksize;
#elseint _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 结构体封装内容
这里我们在结构体中封装文件描述符、缓冲区、刷新策略以及有效数据范围!!
- 这里我们实现的刷新策略只有三种:无缓冲、行刷新、全缓冲。
- 有效数据范围本应该通过一些指针来维护,这里博主简化为:有效数据从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 文件打开接口封装
文件打开:
- 首先我们需要先获取文件的打开方式(这里博主仅实现
w、r、a
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 文件关闭和文件刷新
- 关闭文件后,进程一般会将文件中的内容进行刷新。
- 但对于刷新而言,如果文件中由内容需要被刷新,我们直接调用系统调用接口即可完成!!
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!!
- 向显示器文件中写入时,我们需要判断是否存在字符
\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;//返回刷新个数
}