一.引入
我们发现 printf 和 fwrite (库函数)都输出了2次,而 write 只输出了一次(系统调用)。为什么呢?肯定和fork有关!
C接口的函数被打印了两次系统接口前后只是打印了一次:和fork函数有关,fork会创建子进程。在创建子进程的时候,数据会被处理成两份,父子进程发生写时拷贝,我们进行printf调用数据的时候,数据写到显示器外设上,就不属于父进程了,数据没被写到显示器上,依旧属于父进程,而调用printf并不一定把数据刷到显示器上,没有被显示本质就是数据没有从内存到外设,所以这份没有被显示的数据依旧属于这进程,当我们去fork的时候,进程退出要刷新缓冲区,此时刷新的过程就是把数据从内存刷新到外设,刷新到外设的同时,也会把程序内部的缓冲区的数据直接清走,这就是写入,跟写时拷贝有关系
对于这个现象的问题我们可以直接往下看👇
二.认识缓冲区
1.为什么
缓冲区的本质就是一段内存。在内存空间中预留了一定的存储空间,这些存储空间用来缓冲输入或输出的数据,这部分预留的空间就叫做缓冲区。
数据如果直接从内存到磁盘,在内存中速度快,但是访问外设效率比较低,那太消耗时间了,属于外设IO,所以缓冲区的意义就是节省进程进行数据IO的时间!进程需要把数据拷贝到缓冲区里:我们并不需要拷贝,而是调用fwrite,与其理解fwrite是写入到文件的函数,倒不如理解fwrite是拷贝函数,将数据从进程拷贝到缓冲区或者外设当中。
数据可以直接拷贝到缓冲区,高速设备不用在等待低速设备,提高计算机的效率。
2.刷新策略
缓冲区的刷新策略:如果有一块数据,一次写入到外设(效率最高)vs如果有一块数据,多次少量写入到外设,需要多次IO
缓冲区一定结合具体的设备定制自己的刷新策略:
1.立即刷新——无缓冲 ,场景较少,比如调用printf直接fflush
2.行刷新——行缓冲——显示器 ,数据的printf带上\n就会立马显示到显示器上。显示器为什么是行缓冲:显示器是外设,进程运行时在内存里的,把数据定期要刷新到外设,显示器设备比较特殊,是给用户来看的,从左到右,所以显示器为了保证刷新效率,并且用户体验良好,所以显示器采用行缓冲,满足用户的阅读体验并且在一定程度上效率不至于太低
3.缓冲区满——全缓冲——磁盘文件,效率最高,只需要一次IO,比如文件读写的时候,直接写到磁盘文件
但是存在特殊情况:a.用户强制刷新 b,进程退出——一般到要进行缓冲区刷新
所以对于全缓冲,缓冲区满了采取刷新,减少IO次数,提高效率。
3.在哪里
缓冲区的位置究竟在哪里:从上面的例子我们直接往显示器上打印结果为4条,往文件打印为7条,这跟缓冲区有关,同时这也说明了缓冲区一定不在内核中,为什么?如果在内核中write也应该打印两次,write是系统接口。我们之前谈论的所有缓冲区都指的是用户级语言层面提供的缓冲区。这个缓冲区,在stdout,stdin,stderr对应的类型---->FILE*,FILE是一个结构体,里面封装了fd,同时还包括了一个缓冲区!
FILE结构体缓冲区,所以我们直接要强制刷新的时候fflush(文件指针),关闭文件fclose(文件指针),这是因为传进去的文件指针对应的缓冲区
从源码出发,我们可以来看一看FILE结构体:
所以我们一般所说的缓冲区是语言级别的缓冲区,C语言提供的在FILE结构体里对应的缓冲区。
现在,我们现在重新来看一看刚开始的现象:
1.如果我们没有进行重定向>,看到了4条消息,stdout默认使用的是行刷新,在进程fork之前,三条C函数已经将数据打印输出到显示器上(外设),你的FILE内部进程内部就不存在对应的数据了。
2.如果我们进行了重定向>,写入文件不在是显示器,而是普通文件,采用的刷新策略是全缓冲,之前的3条C函数虽然带了\n,但是不足以将stdout缓冲区写满,所以数据并没有刷新! 在执行fork的时候,stdout属于父进程,fork创建子进程紧接着就是进程退出,谁先退出就要进行缓冲区刷新,刷新的本质就是修改,修改的时候发生写时拷贝!所以数据最终会显示两份!上面的过程都和write无关,write没有FILE,而用的是fd,就没有C提供的缓冲区!
简单总结来说:重定向导致刷新策略发生了改变(由行缓冲变成了全缓冲)。同时发生了写时拷贝,父子进程各自刷新
三、理解缓冲区
对于缓冲区的理解我们可以自己通过代码来简单实现:
FILE_结构体的设计,这里为了避免与FILE发生冲突,我们命名为FILE_:
#define SIZE 1024
typedef struct _FILE
{
int flags;//刷新方式
int fileno;//文件描述符
int cap;//buffer的总容量
int size;//buffer当前使用量
char buffer[SIZE];//缓冲区 }FILE_;
主函数:
int main()
{
//打开
FILE_ *fp = fopen_("./log.txt","w");
//打开失败
if(fp==NULL)
{
return 1;
}
int cnt = 10;
const char*masg = "hello world ";
while(1)
{
//写入
fwrite_(masg,strlen(masg),fp);
//刷新
fflush_(fp);
//睡眠
sleep(1);
printf("count:%d\n",cnt);
cnt--;
if(cnt==0) break;
}
//关闭
fclose_(fp);
return 0;
}
对于C语言来说,文件接口一旦打开成功,其余接口要带上FILE*,因为FILE结构体里包含了各种数据:
下面是我们需要自己实现的文件接口:
//打开
FILE_ * fopen_(const char*path_name,const char*mode);
//以下的接口都需要带上FILE_*
void fwrite_(const void *ptr,int num, FILE_*fp);
void fflush_(FILE_*fp);
void fclose_(FILE_* fp);
fopen_:打开我们需要去判断具体是按什么方式打开:
FILE_ *fopen_(const char *path_name, const char *mode)
{
int flags = 0;
int defaultMode=0666; //设置默认权限
//读方式
if(strcmp(mode, "r") == 0)
{
flags |= O_RDONLY;
}
//写方式
else if(strcmp(mode, "w") == 0)
{
flags |= (O_WRONLY | O_CREAT |O_TRUNC);
}
//追加方式
else if(strcmp(mode, "a") == 0)
{
flags |= (O_WRONLY | O_CREAT |O_APPEND);
}
//其他
else
{
//其他方式这里就不展开了
}
int fd = 0;
if(flags & O_RDONLY) fd = open(path_name, flags);
else fd = open(path_name, flags, defaultMode);
//处理打开失败
if(fd < 0)
{
const char *err = strerror(errno);
write(2, err, strlen(err));
return NULL;
}
//打开成功
FILE_ *fp = (FILE_*)malloc(sizeof(FILE_));
assert(fp);
fp->flags = SYNC_LINE; //默认设置成为行刷新
fp->fileno = fd;
fp->cap = SIZE;
fp->size = 0;
memset(fp->buffer, 0 , SIZE);
return fp;
}
fwrite_:
void fwrite_(const void *ptr, int num, FILE_ *fp)
{
// 写入到缓冲区中
memcpy(fp->buffer+fp->size, ptr, num); //不考虑缓冲区溢出的问题
fp->size += num;
// 判断是否刷新
if(fp->flags & SYNC_NOW)
{
write(fp->fileno, fp->buffer, fp->size);
fp->size = 0; //清空缓冲区
}
else if(fp->flags & SYNC_FULL)
{
if(fp->size == fp->cap)
{
write(fp->fileno, fp->buffer, fp->size);
fp->size = 0;
}
}
else if(fp->flags & SYNC_LINE)
{
if(fp->buffer[fp->size-1] == '\n') // abcd\nefg在这个地方不考虑
{
write(fp->fileno, fp->buffer, fp->size);
fp->size = 0;
}
}
else{
}
}
fclose:
void fclose_(FILE_ * fp)
{
fflush_(fp);
close(fp->fileno);
}
fflush_:这里将数据强制要求操作系统进行外设刷新要用到fsync:
void fflush_(FILE_ *fp)
{
if( fp->size > 0) write(fp->fileno, fp->buffer, fp->size);
fsync(fp->fileno); //将数据,强制要求OS进行外设刷新!
fp->size = 0;
}