便准C的IO缓冲区
在C语言中,它的IO接口都是带缓冲区的;
比如什么printf,fprintf,puts,putc,scanf,sscanf等;C语言的标准IO都是有缓冲区的;
那它的缓冲区在哪呢?
在FILE
这个结构体中:
有兴趣可以在Linux操作系统中:打开文件/usr/include/stdio.h
里面可找到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
};
我们可以看到FILE结构体里面有很多缓冲区,这就是C语言缓冲区的位置;
当我们执行所谓的C语言IO接口操作时候,都必须经过C的缓冲区;
比如执行有关写操作的IO接口,不是直接把信息写到磁盘和屏幕或者其他外设,而是先写入到缓冲区中;然后再由缓冲区的刷新策略决定,是否将缓冲区的数据刷新到内核缓冲区中(内核缓冲区就是为了提高与外设交换的一个缓冲区),再由内核缓冲区刷新到外设中。(内核缓冲区刷新到外设这个是由操作系统搞的,我们不必关心内核缓冲区)
刷新策略有三种:
如何从C语言的缓冲区刷新数据到内核缓冲区呢?
这里式通过文件描述符刷新到内核缓冲区的;因为C接口IO的调用,一定要经过系统调用,系统调用IO一定会使用文件描述符;
标准C的IO接口缓冲区导致的一些现象解释
所以一些代码现象我们就可以解释了:
#include<stdio.h>
int main(){
printf("hello printf\n");
return 0;
}
这段代码会输出:
hello printf
原因printf执行先将hello printf\n
放入到C语言的缓冲区中,由于式往显示器打印,所以当碰到了\n时候,就会刷新缓冲区内容到屏幕显示出来;
#include<stdio.h>
int main(){
printf("hello printf");
while(1){;}
return 0;
}
这段代码执行结果没有显示hello printf
,并且陷入死循环;
原因:printf往显示器打印数据,是使用行刷新策略,当你printf没有换行,那么你的数据就一直保存在缓冲区中,没有被刷新到显示器,由于后面一直死循环,所以进程不会退出,进程不退出那么也不会刷新缓冲区的数据,所以不会显示内容;
这段代码本意是做一个输出重定向,把本输出到屏幕的内容输出到log.txt文件中
#include<string.h>
#include<stdio.h>
#include<fcntl.h>
int main(){
close(1);
int fd = open("./log.txt",O_CREAT | O_WRONLY,0644);
printf("hello printf\n");
fprintf(stdout,"hello fprint\n");
close(fd);
return 0;
}
代码的结果:
可是,当我们执行该文件时候,去观察log.txt文件中,并没有我们重定向的内容;
原因:因为当我们关闭了close(1),再打开log.txt,此时printf输出的内容,不再是往显示器打印了,而是往文件中写入了;
由于原本是往屏幕写入数据的,现在变成往文件写数据,也就是往磁盘写数据;
此时刷新缓冲区的策略从行刷新,变成了满刷新,也就是等缓冲区满了才会把数据刷新到文件中;
很明显,我们这两句打印的数据内容没有填满缓冲区;
并且在进程退出前,close(fd),直接关闭文件描述符,也就断开了进程在退出时,会主动刷新缓冲区的时机;
因为文件描述符都关闭了,也就断开了从C语言缓冲区刷新数据到内核缓冲区的过程;
这段代码:假如我们把它的执行结果使用输出重定向到log.txt文件中,我们观察 log.txt文件,发现只有
hello write
,并没有hello printf
,这是为啥呢?
#include<unistd.h>
#include<string.h>
#include<stdio.h>
#include<fcntl.h>
int main(){
const char* msg = "hello write\n";
write(1,msg,strlen(msg));
printf("hello printf\n");
close(1);
return 0;
}
原因解释:很明显:当我们把结果输出重定向到log.txt中,会使得文件描述符1指向了log.txt文件,还隐含的也改变了缓冲区的刷新策略,由原来的行缓冲刷新变成满缓冲刷新;
由于write是系统调用没有缓冲区的,会显示 hello write 而printf是C语言接口,带缓冲区的,当重定向到log.txt时候,就变成了满缓冲刷新策略;
此时在进程退出前,我们关闭close(1),也就是关闭文件描述符指针,此时本来进程退出可以将缓冲区刷新到log.txt的机会,但是被close(1)关闭了,断开了刷新缓冲区的路径;所以不会打印hello printf;
#include<unistd.h>
#include<string.h>
#include<stdio.h>
#include<fcntl.h>
int main(){
const char* msg = "hello write\n";
write(1,msg,strlen(msg));
printf("hello printf\n");
fork();
return 0;
}
当我们输出重定向该代码结果到log.txt时候,观察log.txt文件会显示如下内容;
为什么hello printf有两句呢?而hello write只有一句呢?
解释原因:
因为当我们发生重定向时候,同时修改了刷新策略,从行刷新到满才刷新,我们的hello write 直接重定向到了log.txt,因为它没有缓冲区,而hello printf 先存入到了缓冲区;
当我们再调用fork时候,创建子进程,它会继承父类的缓冲区内容,当子进程退出时候,会刷新子进程的缓冲区内容,把它刷新到了log.txt文件中,当父进程也推出时候,也刷新了父进程的缓冲区,自然而然把父进程的缓冲区内容刷新到了log.txt中,所以hello printf有两份;
由于write是系统调用,没有缓冲区,在创建子进程之前,write就已经将内容重定向到了log.txt,所以创建子进程后,hello write只打印一份;
总结用户层缓冲区问题
我们在上面谈的缓冲区,都是用户层的缓冲区,我只不过是以C结构的IO举例,其实任何语言的IO都有自己的缓冲区,在我们把它成为用户层的缓冲区;
其实内核也有个缓冲区,这个缓冲区是和硬件:磁盘,屏幕等外设交互的,为的是提高存取效率;这里我们不关心内核缓冲区;
系统调用的IO接口时没有缓冲区的,因为这是系统调用啊,不是用户层的接口;