Linux文件:缓冲区、缓冲区刷新机制 | C库模拟实现
一、缓冲区的作用
缓冲区本质上就是一部分内存,用于提高效率!!
对于文件的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 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 结构体封装内容
这里我们在结构体中封装文件描述符、缓冲区、刷新策略以及有效数据范围!!
- 这里我们实现的刷新策略只有三种:无缓冲、行刷新、全缓冲。
- 有效数据范围本应该通过一些指针来维护,这里博主简化为:有效数据从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;//返回刷新个数
}