缓冲区
从一个简单的实例开始,话不多说,直接上代码。
#include <stdio.h> #include <unistd.h> int main() { printf("hello\n"); // 实例1 // printf("hello"); // 实例2 write(STDOUT_FILENO, "lijd\n", 5); printf("world\n"); return 0; }
实例1 运行结果截图:
实例2 运行结果截图:
从这个程序运行的表象,引出内核高速缓冲区和stdio缓冲的概念。程序内部为提高IO速度,减少CPU等待IO而浪费CPU资源。
如下图概括了stdio函数库和内核所采用的缓冲(针对输出文件),以及对各种缓冲类型的控制机制。从图中自上而下,首先是通过 stdio 库将用户数据传递到 stdio 缓冲区,该缓冲区位于用户态内存区。当缓冲区填满时,stdio 库会调用 write()系统调用,将数据传递到内核高速缓冲区(位于内核态内存区)。最终,内核发起磁盘操作,将数据传递到磁盘。
printf缓冲区刷新的条件:
1.进程结束。
2.遇到\n。
3.缓冲区满。
4.手动刷新缓冲区fflush(stdout)。
5.调用exit(0) (但是还可以调用_exit(0),不刷新缓冲区)。由上图可知,利用fflush()函数刷新stdio缓冲区,实例代码如下。
#include <stdio.h> #include <unistd.h> int main() { printf("hello"); fflush(STDOUT_FILENO); write(STDOUT_FILENO, "lijd\n", 5); printf("world\n"); return 0; }
执行结果如下:
综上所述:write()的输出结果先于printf()而出现,是因为 write()会将数据立即传给内核高速缓存,而 printf()的输出则需要等到满足一定条件时才刷新stdio缓冲区。
缓冲区与fork()函数
fork()的流程:fork()创建子进程时继承了父进程的数据段、代码段、栈段、堆,注意从父进程继承来的是虚拟地址空间,同时也复制了页表(没有复制物理块)。因此,此时父子进程拥有相同的虚拟地址,映射的物理内存也是一致的(独立的虚拟地址空间,共享父进程的物理内存)。当父子进程中有更改相应段的操作发生时,系统会给子进程分配物理空间。这就是写时拷贝。写时拷贝是以“页”为单位复制的,没发生写时拷贝时子进程虚拟内存中的指针指的是父进程的物理地址。
给一个简单的案例如下:
#include <stdio.h> #include <sys/types.h> #include <unistd.h> int main() { int a = 0x1234; if(fork() == 0) { a = 0x5678; printf("son : pid=%d, a=%x, &a=%x\n", getpid(), a, &a); }else{ wait(); // 等待子进程执行完成返回 printf("father : pid=%d, a=%x, &a=%x\n", getpid(), a, &a); } return 0; }
执行结果如下:
上述案例执行的结果中&a地址打印的是a变量的虚拟地址,由此可以看出fork出的子进程的虚拟地址与父进程的虚拟地址值相同,开始指向同一物理地址,当子进程对变量进行修改时,发生了写时拷贝。
分析如下案例代码:
#include <stdio.h> #include <sys/types.h> #include <unistd.h> int main() { printf("hello world\n"); write(STDOUT_FILENO, "lijd\n", 5); pid_t pid = fork(); if(pid == -1) { perror("fork"); } else if(pid == 0) { printf("son~~~\n"); } else{ wait(); printf("father~~~\n"); } return 0; }
两种执行结果如下:
为什么会出现这种现象?首先要记住,是在进程的用户空间内存中维护 stdio 缓冲区的。因此,通过 fork()创建子进程时会复制这些缓冲区。当标准输出定向到终端时,因为缺省为行缓冲,所以会立即显示函数 printf()输出的包含换行符的字符串。不过,当标准输出重定向到文件时,由于缺省为块缓冲,所以在本例中,当调用 fork()时,printf()输出的字符串仍在父进程的 stdio 缓冲区中,并随子进程的创建而产生一份副本。父、子进程调用 exit()时会刷新各自的 stdio 缓冲区,从而导致重复的输出结果。
如何避免重复的输出结果?
1、作为针对stdio缓冲区问题的特定解决方案,可以在调用fork()之前使用函数fflush()来刷新stdio缓冲区。作为另一种选择,也可以使用 setvbuf()和 setbuf()来关闭stdio流的缓冲功能。
2、子进程可以调用_exit()而非exit(),以便不再刷新stdio缓冲区。这一技术例证了一个更为通用的原则:在创建子进程的应用中,典型情况下仅有一个进程(一般为父进程)应通过调用exit()终止,而其他进程应调用_exit()终止,从而确保只有一个进程调用退出处理程序并刷新 stdio缓冲区,这也算是众望所归吧。
#include <stdio.h> #include <sys/types.h> #include <unistd.h> int main() { printf("hello world\n"); write(STDOUT_FILENO, "lijd\n", 5); pid_t pid = fork(); if(pid == -1) { perror("fork"); } else if(pid == 0) { printf("son~~~\n"); char bufr[512]; if (setvbuf(STDOUT_FILENO, bufr, _IOFBF, 512) != 0) //方法1 { perror("setvbuf"); } } else{ wait(); printf("father~~~\n"); _exit(0); //方法2 } return 0; }
执行结果如下图所示: