天天看点

fork()调用中关于缓存区继承而引起的思考

     题目:请问下面的程序一共输出多少个“-”?

     1    #include <stdio.h>

     2    #include <sys/types.h>

     3    #include <unistd.h>

     4    int main(void)

     5    {

     6      int i;

     7      for(i=0; i<2; i++){

     8         fork();

     9         printf("-");

    10      }

    11      return 0;

    12    }

       这是从酷壳网上看到的一个问题,看到这个题目本能的思考了一下,自己也画了一个图,如下面所示:

fork()调用中关于缓存区继承而引起的思考

       从上面画的示意图可以看出,最后会输出6个’—‘,于是就赶紧看一下正确答案,结果答案不是6,而是8,究其原因,主要有两个因素决定最后结果,一个是fork调用的作用,一个是printf标准I/O调用的作用。

       一个现有进程可以调用fork函数创建一个新进程。由fork创建的新进程被称为子进程。fork函数被调用一次但返回两次。两次返回的唯一区别是子进程中返回0值而父进程中返回子进程ID。子进程是父进程的副本,它将获得父进程数据空间、堆、栈等资源的副本。注意,子进程持有的是上述地址空间的“副本”,这意味着父子进程间不共享这些存储空间。

       Linux中对文件系统的读写可以通过系统调用read和write来完成,前面的博文也说过,用户写数据时会内核首先会把用户数据拷贝到内核高速缓冲中,然后在采取延迟写策略写到文件中去。这两个系统调用是通过文件描述符和输入输出关联,但是这就涉及到需要用户来管理缓冲区,因为不同的缓存区的长度会影响读写的效率。为了克服这个缺陷,后来开发出了标准I/O库,不再通过文件描述符和输入输出关联,而是通过FILE指针和流关联,FILE结构中包括了文件描述符,缓冲区的大小,当调用fopen的时候系统会动态分配一块内存作为缓存区。而printf作为标准I/O中的函数,也有自己的缓冲区。

       标准I/O对待缓存的数据采用3种不同的策略,全缓冲、行缓冲、无缓冲。

       对于没有交互的终端,例如块设备文件,系统采用全缓冲;

       对于标准输入,标准输出这样交互设备采用行缓冲;

       对于需要立即响应的设备,例如标准错误采用无缓冲;

       所以printf调用输出到标准输出将采用行缓冲策略。当i=0时,fork调用后,父子进程同时输出‘—’,此时‘—’并不会立即输出到终端显示屏,而是会暂存在各自的缓冲区内,而当i=1时,再次fork时,由于子进程是父进程的副本,这样这两个进程的孩子进程将得到他们父进程的缓冲区,这就是最后答案会由6到8的原因,可用下图表示为:

fork()调用中关于缓存区继承而引起的思考

所以,为了防止这种不是我们希望的结果出现,就需要采取相应的措施,对于行缓冲,程序在遇到“\n”,或是EOF,或是缓中区满,或是文件描述符关闭,或是主动flush,或是程序退出,都会把数据刷出缓冲区。这样如果我们这样输出:

printf(“—\n”);

或者主动洗刷缓冲区

fflush(stdout);

这样都可以清空进程的缓冲区,子进程就不会应拷贝了父进程的缓冲区而出现奇怪的行为。

继续阅读