• 关于C中I/O缓冲区的解释


    用户程序调用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标准库和内核之间的关系就像CPU、Cache和内存之间的关系一样,C标准库之 所以会从内核预读一些数据放在I/O缓冲区中,是希望用户程序随后要用到这些数据,C标准库的I/O缓冲区也在用户空间,直接从用户空间读取数据比进内核 读数据要快得多。另一方面,用户程序调用fputc通常只是写到I/O缓冲区中,这样fputc函数可以很快地返回,如果I/O缓冲区写满了,fputc就通过系统调用把I/O缓冲区中的数据传给内核,内核最终把数据写回磁盘。有时候用户程序希望把I/O缓冲区中的数据立刻传给内核,让内核写回设备,这称为Flush 操作,对应的库函数是fflushfclose函数在关闭文件之前也会做Flush操作。

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

     C标准库的I/O缓冲区:
    图片

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

    全缓冲

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

    行缓冲

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

    无缓冲

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

    下面通过一个简单的例子证明标准输出对应终端设备时是行缓冲的。

    #include <stdio.h>
    
    int main()
    {
      printf("hello world");
      while(1);
      return 0;
    }
    

    运行这个程序,会发现hello world并没有打印到屏幕上。用Ctrl-C终止它,去掉程序中的while(1);语句再试一次:

    $ ./a.out
    hello world$
    

    hello world被打印到屏幕上,后面直接跟Shell提示符,中间没有换行。

    我们知道main函数被启动代码这样调用:exit(main(argc, argv));main函数return时启动代码会调用exitexit函数首先关闭所有尚未关闭的FILE *指针(关闭之前要做Flush操作),然后通过_exit系统调用进入内核退出当前进程。

        在上面的例子中,由于标准输出是行缓冲的,printf("hello world");打印的字符串中没有换行符,所以只把字符串写到标准输出的I/O缓冲区中而没有写回内核(写到终端设备),如果敲Ctrl-C,进程是异常终止的,并没有调用exit,也就没有机会Flush I/O缓冲区,因此字符串最终没有打印到屏幕上。如果把打印语句改成printf("hello world ");,有换行符,就会立刻写到终端设备,或者如果把while(1);去掉也可以写到终端设备,因为程序退出时会调用exitFlush所有I/O缓冲区。在本书的其它例子中,printf打印的字符串末尾都有换行符,以保证字符串在printf调用结束时就写到终端设备。

    我们再做个实验,在程序中直接调用_exit退出。

    #include <stdio.h>
    #include <unistd.h>
    
    int main()
    {
      printf("hello world");
      _exit(0);
    }
    

    结果也不会把字符串打印到屏幕上,如果把_exit调用改成exit就可以打印到屏幕上。

    除了写满缓冲区、写入换行符之外,行缓冲还有一种情况会自动做Flush操作。如果:

    • 用户程序调用库函数从无缓冲的文件中读取

    • 或者从行缓冲的文件中读取,并且这次读操作会引发系统调用从内核读取数据

    那么在读取之前会自动Flush所有行缓冲。例如:

    #include <stdio.h>
    #include <unistd.h>
    
    int main()
    {
      char buf[20];
      printf("Please input a line: ");
      fgets(buf, 20, stdin);
      return 0;
    }
    

    虽然调用printf并不会把字符串写到设备,但紧接着调用fgets读一个行缓冲的文件(标准输入),在读取之前会自动Flush所有行缓冲,包括标准输出。

    如果用户程序不想完全依赖于自动的Flush操作,可以调fflush函数手动做Flush操作。

    #include <stdio.h>
    
    int fflush(FILE *stream);
    返回值:成功返回0,出错返回EOF并设置errno
    

    对前面的例子再稍加改动:

    #include <stdio.h>
    
    int main()
    {
      printf("hello world");
      fflush(stdout);
      while(1);
    }
    

    虽然字符串中没有换行,但用户程序调用fflush强制写回内核,因此也能在屏幕上打印出字符串。fflush函数用于确保数据写回了内核,以免进程异常终止时丢失数据。作为一个特例,调用fflush(NULL)可以对所有打开文件的I/O缓冲区做Flush操作。

    总结一下:C语言中的I/O缓冲区是这样的:

    对于printf函数,行缓冲,每次输出有换行符输出超过缓冲区大小的数据时,会在屏幕上输出数据。

    有两个例外:1.当从无缓冲文件(或数据流)中读数据输出;

                           2.当printf后跟了scanf,由于scanf是行缓冲流,而且调用scanf会引发系统写数据到内

                   核,所以printf会自动flush。

    对于scanf函数,行缓冲,每次输入时会把数据连同末尾的换行符送进缓冲区,然后缓冲区会有一个“指针”,始终指向当前读到的那个字符(即“当前字符指针”)。当把字符从缓冲区中送入内存中的具体变量中时,清除缓冲区中的该字符。(敲回车输入的换行符在缓冲区中当做空格符'  '处理)。值得注意的是,每次scanf时,先检测缓冲区的“当前字符指针”之后是否有未读字符。

     这样,连续scanf  %c数据时,就有可能把数据尾的换行符直接赋给接下来本要输入的字符变量。

    举例:(思考)

    一个小程序,其中有以下几句:

    char c1,c2;

    scanf("%c",&c1);

    printf("%c",c1);

    scanf("%c",&c2);

    printf("%c",c2);

    这样,run以后,输入c1,敲回车以后就不能输入c2,程序直接结束了,为什么?

    后来修改一下,

    把“scanf("%c",&c2);

            printf("%c",c2);”改成了:

    scanf("%s",&c2);

    printf("%c",c2);

    却可以,为什么?%s和%c有什么不同?(%s读取第一个非空字符)

    还有一种方法:
    scanf("%c",&ch);改为scanf(" %c",&ch);%c前面加一个空格,这是为什么?

    有还不清楚的请直接在留言中提问,谢谢。

  • 相关阅读:
    堆排序回顾
    动画函数封装
    mouseenter 和mouseover的区别
    元素滚动 scroll 系列
    元素可视区 client 系列
    元素偏移量 offset 系列
    JS执行机制
    BOM
    常用键盘事件
    常用鼠标事件
  • 原文地址:https://www.cnblogs.com/pricks/p/3832391.html
Copyright © 2020-2023  润新知