在输出重定向的时候为什么必须fflush(stdout)才能将内容刷新到指定文件呢?我们当时回答是因为存在缓冲区。那么本篇文章我们将重点了解认识一下缓冲区。
目录
缓冲区的本质就是一段内存。 那么这段内存在哪里呢?我们接下来将会说明这个问题。
我们举个例子来理解这个概念:
假设你在北京大学上学,你的朋友在上海交通大学上学,你有10本书想给你的朋友,你打算怎么将这些书送给你的同学呢?
第一种方式:你自己带着10本书从北京到上海,亲自送给你的朋友。但是这种方式成本明显过于大,并且耽误你的时间。因此我们通常是采用第二种方式。
第二种方式:你在北京大学门口菜鸟驿站将10本书打包成快递发给你在上海交通大学的朋友。当你发送完快递后你就什么也不用管了,静静地等着你朋友收到快递的消息即可。
因此这个快递存在的最大价值是解放你的时间。这里快递存在意义等同于缓冲区的意义。
缓冲区的意义:
我们使用一段代码来理解
- #include
- #include
- #include
- #include
- #include
- #include
-
- int main()
- {
- printf("hello printf");// stdout -> 1
- const char* msg = "hello write";
- write(1,msg,strlen(msg));
- sleep(5);
- return 0;
- }
printf内部封装了write,而printf不显示的原因是因为printf的内容在缓冲区内,当sleep时,内容存在在缓冲区内,当我们不带'\n'时,不会被理解刷新出来,数据被暂存在缓冲区内。
但是我们看到hello write被立马刷新,那么printf封装了write,那么这个缓冲区在哪里呢?
我们通过现象可以回答的是这个缓冲区一定不在write内。因此这个缓冲区只能是语言提供的(C语言)。因此这个缓冲区是一个语言级别的缓冲区。
那么我们来具体深挖一下缓冲区的位置.stdout的返回值是FILE,FILE内部有struct结构体,结构体内封装了很多的属性,其中包括上篇我们提到的文件描述符fd,除此之外还有该File对应的语言级别的缓冲区!
printf fwrite 库函数会自带缓冲区,而 write 系统调用没有带缓冲区。另外,我们这里所说的缓冲区,
都是用户级缓冲区。其实为了提升整机性能,OS也会提供相关内核级缓冲区,不过不再我们讨论范围之内。那这个缓冲区谁提供呢? printf fwrite 是库函数, write 是系统调用,库函数在系统调用的“上层”, 是对系统调用的“封装”,但是 write 没有缓冲区,而 printf fwrite 有,足以说明,该缓冲区是二次加上的,又因为是 C,所以由C标准库提供
我们也可以一起看看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
- };
刷新策略说白了就是什么时候刷新?
结合上面的之后,下面的这段代码的执行结果是什么?
- #include <stdio.h>
- #include <string.h>
- #include <fcntl.h>
- #include <unistd.h>
- #include <sys/types.h>
- #include <sys/stat.h>
-
- int main()
- {
- const char *str1 = "hello printf\n";
- const char *str2 = "hello fprintf\n";
- const char *str3 = "hello fputs\n";
- const char *str4 = "hello write\n";
-
- //C库函数
- printf(str1);
- fprintf(stdout,str2);
- fputs(str3,stdout);
-
- //系统接口
- write(1,str4,strlen(str4));
-
- //是调用完了上面的代码才执行的fork
- fork();
-
- return 0;
- }
我们运行上述代码后,将结果重定向到log.txt内部,为什么会有7条消息?
答:当我们重定向后,本来要把显示在显示器的文件重定向到指定文件时,缓冲区的刷新策略由行缓冲(显示器文件)切换成了全缓冲(磁盘文件)。答案一定是和fork()有关系。我们可以这样理解,当str1,str2,str3把数据打印到文件里,此时已经重定向到log.txt,数据不会立即刷新,而变成了全缓冲,所以前三条信息暂存在了log.txt缓冲区内部,当我们调用fork()时,fork()要创建子进程,fork之后父子进程同时退出,退出之后父子进程就要刷新缓冲区了,而刷新的本质就是把缓冲区的数据写入到操作系统内部,并清空缓冲区。这里的缓冲区是自己的FILE内部维护的,属于父进程内部的数据区域,当我们刷新的时候,代码和数据要发生写时拷贝,因此这份代码父进程刷一份,子进程刷一份,因此我们就看到了有2个str1,2个str2,2个str3刷到了log.txt。
我们写的是样例代码不代表全部的标准的实现。从代码层面上理解一下原理
- #include <stdio.h>
- #include <string.h>
- #include <stdlib.h>
- #include <assert.h>
- #include <unistd.h>
- #include <sys/stat.h>
- #include <sys/types.h>
- #include <fcntl.h>
-
- #define NUM 1024
-
- #define NONE_FLUSH 0x0
- #define LINE_FLUSH 0x1
- #define FULL_FLUSH 0x2
-
- typedef struct _MyFILE
- {
- int _fileno;
- char _buffer[NUM];
- int _end;
- int _flags;//fflush method
- }MyFILE;
-
- MyFILE *my_fopen(const char* filename,const char*method)
- {
- assert(filename);
- assert(method);
-
- int flags = O_RDONLY;
-
- if(strcmp(method,"r") == 0)
- {
- }
- else if(strcmp(method,"r+") == 0)
- {}
- else if(strcmp(method,"w") == 0)
- {
- flags = O_WRONLY | O_CREAT |O_TRUNC;
- }
- else if(strcmp(method,"w+") == 0)
- {}
- else if(strcmp(method,"a") == 0)
- {
- flags = O_WRONLY | O_CREAT |O_APPEND;
- }
- else if(strcmp(method,"a+") == 0)
- {}
-
- int fileno = open(filename,flags,0666);
- if(fileno < 0)
- {
- return NULL;
- }
-
- MyFILE *fp = (MyFILE*)malloc(sizeof(MyFILE));
- if(fp == NULL ) return fp;
- memset(fp,0,sizeof(MyFILE));
-
- fp->_fileno = fileno;
- fp->_flags |= LINE_FLUSH;
- fp->_end = 0;
- return fp;
- }
- void my_fflush(MyFILE* fp)
- {
- assert(fp);
- if(fp->_end > 0)
- {
- write(fp->_fileno,fp->_buffer,fp->_end);
- fp->_end =0;
- syncfs(fp->_fileno);
- }
-
- }
-
- void my_fwrite(MyFILE* fp,const char* start,int len)
- {
- assert(fp);
- assert(start);
- assert(len>0);
-
- // abcde->追加
- strncpy(fp->_buffer+fp->_end,start,len);//将数据写入缓冲区
- fp->_end += len;
-
- if(fp->_flags & NONE_FLUSH){}
- if(fp->_flags & LINE_FLUSH)
- {
- if(fp->_end > 0 && fp->_buffer[fp->_end-1] == '\n')
- {
- write(fp->_fileno,fp->_buffer,fp->_end);
- fp->_end = 0;
- syncfs(fp->_fileno);
- }
- }
- if(fp->_flags & FULL_FLUSH){}
-
- }
-
- void my_fclose(MyFILE* fp)
- {
- my_fflush(fp);
- close(fp->_fileno);
- free(fp);
- }
-
- int main()
- {
- MyFILE * fp = my_fopen("log.txt","w");
- if(fp == NULL)
- {
- printf("my_fopen error\n");
- return 1;
- }
- const char *msg = "hello my_file 11111111\n";
- my_fwrite(fp,msg,strlen(msg));
-
- printf("hello my_file 11111111消息立即刷新\n");
- sleep(3);
-
-
- const char *mssg = "hello 222222222";
- my_fwrite(fp,mssg,strlen(mssg));
- sleep(3);
- printf("写入了一个不满足条件的字符串hello 222222222\n");
-
- const char *msssg = "hello 33333333";
- my_fwrite(fp,msssg,strlen(msssg));
- sleep(3);
- printf("写入了一个不满足条件的字符串hello 33333333\n");
-
- const char *mssssg = "end\n";
- my_fwrite(fp,mssssg,strlen(mssssg));
- printf("写了一个满足条件的字符串end\n");
- sleep(3);
-
-
- const char *msssssg = "aaaaaaa";
- my_fwrite(fp,msssssg,strlen(msssssg));
- printf("写了一个满足条件的字符串aaaaaaa\n");
- sleep(1);
- my_fflush(fp);
- sleep(3);
-
-
-
- my_fclose(fp);
- return 0;
- }
我们也可以模拟进程退出
(本篇完)