C 标准库IO缓冲区和内核缓冲区的区别

1.C标准库的I/O缓冲区 
        UNIX的传统 是Everything is a file,键盘、显示器、串口、磁盘等设备在/dev 目录下都有一个特殊的设备文件与之对应,这些设备文件也可以像普通文件(保存在磁盘上的文件)一样打开、读、写和关闭,使用的函数接口是相同的。用户程序调用C标准I/O库函数读写普通文件或设备,而这些库函数要通过系统调用把读写请求传给内核 ,最终由内核驱动磁盘或设备完成I/O操作。C标准库为每个打开的文件分配一个I/O缓冲区以加速读写操作,通过文件的FILE 结构体可以找到这个缓冲区,用户调用读写函数大多数时候都在I/O缓冲区中读写,只有少数时候需要把读写请求传给内核。以fgetc / fputc 为例,当用户程序第一次调用fgetc 读一个字节时,fgetc 函数可能通过系统调用 进入内核读1K字节到I/O缓冲区中,然后返回I/O缓冲区中的第一个字节给用户,把读写位置指 向I/O缓冲区中的第二个字符,以后用户再调fgetc ,就直接从I/O缓冲区中读取,而不需要进内核 了,当用户把这1K字节都读完之后,再次调用fgetc 时,fgetc 函数会再次进入内核读1K字节 到I/O缓冲区中。在这个场景中用户程序、C标准库和内核之间的关系就像在“Memory Hierarchy”中 CPU、Cache和内存之间的关系一样,C标准库之所以会从内核预读一些数据放 在I/O缓冲区中,是希望用户程序随后要用到这些数据,C标准库的I/O缓冲区也在用户空间,直接 从用户空间读取数据比进内核读数据要快得多。另一方面,用户程序调用fputc 通常只是写到I/O缓 冲区中,这样fputc 函数可以很快地返回,如果I/O缓冲区写满了,fputc 就通过系统调用把I/O缓冲 区中的数据传给内核,内核最终把数据写回磁盘或设备。有时候用户程序希望把I/O缓冲区中的数据立刻 传给内核,让内核写回设备或磁盘,这称为Flush操作,对应的库函数是fflush,fclose函数在关闭文件 之前也会做Flush操作。

        我们知道main 函数被启动代码这样调用:exit(main(argc, argv));。

        main 函数return时启动代码会 调用exit ,exit 函数首先关闭所有尚未关闭的FILE *指针(关闭之前要做Flush操作),然后通 过_exit 系统调用进入内核退出当前进程.

C标准库的I/O缓冲区有三种类型:全缓冲、行缓冲和无缓冲。当用户程序调用库函数做写操作时, 不同类型的缓冲区具有不同特性。 

    全缓冲 

如果缓冲区写满了就写回内核。常规文件通常是全缓冲的。 

   行缓冲 

如果用户程序写的数据中有换行符就把这一行写回内核,或者如果缓冲区写满了就写回内 核。标准输入和标准输出对应终端设备时通常是行缓冲的。 

     无缓冲 

用户程序每次调库函数做写操作都要通过系统调用写回内核。标准错误输出通常是无缓冲的,这样用户程序产生的错误信息可以尽快输出到设备。


        除了写满缓冲区、写入换行符之外,行缓冲还有两种情况会自动做Flush操作。如果: 
用户程序调用库函数从无缓冲的文件中读取 
或者从行缓冲的文件中读取,并且这次读操作会引发系统调用从内核读取数据


       如果用户程序不想完全依赖于自动的Flush操作,可以调fflush函数手动做Flush操作。 
#include <stdio.h> 
int fflush(FILE *stream); 
返回值:成功返回0,出错返回EOF并设置errno
fflush函数用于确保数据写回了内核,以免进程异常终止时丢失数据,如fflush(stdout); 作为一个特例,调 用fflush(NULL)可以对所有打开文件的I/O缓冲区做Flush操作。


2. 用户程序的缓冲区
        在函数栈上分配的如char buf[10];之类的缓冲区, strcpy(buf, str);  str 所指向的字符串有可能超过10个字符而导致写越界,这种写越界可能当时不出错, 而在函数返回时出现段错误,原因是写越界覆盖了保存在栈帧上的返回地址, 函数返回时跳转到非法地址,因而出错。像buf 这种由调用者分配并传给函数读或写的一段内存通 常称为缓冲区(Buffer),缓冲区写越界的错误称为缓冲区溢出(Buffer Overflow)。如果只是出 现段错误那还不算严重,更严重的是缓冲区溢出Bug经常被恶意用户利用,使函数返回时跳转到一 个事先设好的地址,执行事先设好的指令,如果设计得巧妙甚至可以启动一个Shell,然后随心所欲 执行任何命令,可想而知,如果一个用root 权限执行的程序存在这样的Bug,被攻陷了,后果将很 严重。


       下图以fgets / fputs 示意了I/O缓冲区的作用,使用fgets / fputs 函数时在用户程序中也需要分配缓冲 区(图中的buf1 和buf2 ),注意区分用户程序的缓冲区和C标准库的I/O缓冲区。



3.内核缓冲区
(1)终端缓冲

   终端设备有输入和输出队列缓冲区,如下图所示


         以输入队列为例,从键盘输入的字符经线路规程过滤后进入输入队列,用户程序以先进先出的顺序 从队列中读取字符,一般情况下,当输入队列满的时候再输入字符会丢失,同时系统会响铃警报。 终端可以配置成回显(Echo)模式,在这种模式下,输入队列中的每个字符既送给用户程序也送给 输出队列,因此我们在命令行键入字符时,该字符不仅可以被程序读取,我们也可以同时在屏幕上 看到该字符的回显。
        注意上述情况是用户进程(shell进程也是)调用read/write等unbuffer I/O函数的情况,当调用printf/scanf (底层实现也是read/write)等C标准I/O库函数时,当用户程序调用scanf读取键盘输入时,开始输入的字符都存到输入队列,直到我们遇到换行符(标准输入和标准输出都是行缓冲的)时,系统调用read将输入队列的内容读到用户进程的I/O缓冲区; 当调用printf 打印一个字符串时,如果语句中带换行符,则立刻将放在I/O缓冲区的字符串调用write写到内核的输出队列,打印到屏幕上,如果printf语句没带换行符,则由上面的讨论可知,程序退出时会做fflush操作.
   
 
(2)虽然write 系统调用位于C标准库I/O缓冲区的底 层,被称为Unbuffered I/O函数,但在write 的底层也可以分配一个内核I/O缓冲区,所以write 也不一定是直接写到文件的,也 可能写到内核I/O缓冲区中,可以使用fsync函数同步至磁盘文件,至于究竟写到了文件中还是内核缓冲区中对于进程来说是没有差别 的,如果进程A和进程B打开同一文件,进程A写到内核I/O缓冲区中的数据从进程B也能读到,因为内核空间是进程共享的, 而c标准库的I/O缓冲区则不具有这一特性,因为进程的用户空间是完全独立的.


(3)为了减少读盘次数,内核缓存了目录的树状结构,称为dentry(directory entry(目录下项) cache


(4)FIFO和UNIX Domain Socket这两种IPC机制都是利用文件系统中的特殊文件来标识的。FIFO文件在磁盘上没有数据块,仅用来标识内核中的一条通道,各进程可以打开这个文件进行read / write ,实际上是在读写内核通道(根本原因在于这个file 结构体所指向的read 、write 函数指针和常规文件不一样),这样就实现了进程间通信。UNIX Domain Socket和FIFO的原理类似,也需 要一个特殊的socket文件来标识内核中的通道,文件类型s表示socket,这些文件在磁盘上也没有数据块。UNIX Domain Socket是目前最广泛使用 的IPC机制.如下图:



4.stack overflow 无穷递归或者定义的极大数组都可能导致操作系统为程序预留的栈空间耗尽 程序崩溃(段错误) 


参考:《linux c 编程一站式学习》

posted on 2018-02-26 13:28  AlanTu  阅读(2219)  评论(1编辑  收藏  举报

导航