一、首先看一个程序:
#include <stdio.h>
#include <sys/wait.h>
#include <unistd.h>
Int main()
{
Int i = 0;
For(i = 0 ; i < 2 ; i++)
{
Fork();
Printf(“-”);
}
Wait(NULL);
Wait(NULL);
Return 0;
}
上面这个程序一共输出多少个“-”?
相信对fork()函数有所了解的,可能会说出是6个,但是结果真的是这样的吗?其实不然,正确的输出是8个。为什么会是这样呢?下面我们就来揭晓:
其实很简单,不知道有人注意没,上面的程序的printf语句中没有换行。首先,我们都知道,fork时,子进程会复制父进程的进程空间,包括指令,变量值,程序调用栈,环境变量,缓冲区等。上面那个程序之所以输出8个是因为printf语句有缓存,该语句将“-”放入了缓存中,而没有真正输出,所以fork时,子进程会复制父进程的缓冲区,就多出了两个“-”。
用图来表示可能更清楚些。
相信说到这,大家都明白了,上述程序之所以打印出8个“-”,是因为上图中的①和②进程复制了各自的父进程缓冲区,所以那两个进程的缓冲区就各自都有两个“-”,导致打印结果会多出两个“-”。
若是在输出语句中加上“\n”,或在之后加上fflush(stdout)刷新缓冲区就会是正常的6个。,加“\n”的原因是因为标准输出是行缓冲,遇到”\n”的时候会刷出缓存。
补充说明:
①、linux下的设备有块设备和字符设备,块设备是一块一块的进行数据存取,字符设备是一次存取一个字符的设备。磁盘和内存是块设备;字符设备如键盘和串口。块设备一般都有缓存,而字符设备一般没有。
②、程序遇到“\n”,或是EOF,或是缓中区满,或是文件描述符关闭,或是主动flush,或是程序退出,就会把数据刷出缓冲区。需要注意的是,标准输出是行缓冲,所以遇到“\n”的时候会刷出缓冲区,但对于磁盘这个块设备来说,“\n”并不会引起缓冲区刷出的动作,那是全缓冲,你可以使用setvbuf来设置缓冲区大小,或是用fflush刷缓存。
二、为什么vfork的子进程调用return会将整个程序挂掉而使用exit()不会挂掉
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
int a = 0;
pid_t pid = vfork();
if(pid < 0)
{
printf("vfork error!\n");
}
else if(pid == 0)
{
a++;
return 0;
}
else
{
printf(" a = %d\n",a);
}
return 0;
}
运行结果:
为什么会产生上面的结果?
因为函数栈父子进程共享,在vfork中return了,那么,这就意味main()函数return了。
在子进程中调用return的过程:
1)子进程的main() 函数 return了,于是程序的函数栈发生了变化。
2)而main()函数return后,通常会调用 exit()或相似的函数(如:_exit(),exitgroup())
3)这时,父进程收到子进程exit(),开始从vfork返回,父进程的栈都被子进程给释放掉了,无法执行
相对于exit,return会释放局部变量,并弹栈,回到上级函数执行。exit直接退掉。在c++中,return会调用局部对象的析构函数,exit不会。(注:exit不是系统调用,是glibc对系统调用 _exit()或_exitgroup()的封装)可见,子进程调用exit() 没有修改函数栈,所以,父进程得以顺利执行。
注意:如果你调用 exit() 函数,还是会有问题的,正确的方法应该是调用 _exit() 函数,因为 exit() 函数 会 flush 并 close 所有的 标准 I/O ,这样会导致父进程受到影响。(这个情况在fork下也会受到影响,会导致一些被buffer的数据被flush两次。
三、最后对fork和vfork做一总结
① fork和vfork的差别
fork是创建一个子进程,并把父进程的内存数据copy到子进程中,父子进程执行顺序不确定;
vfork是创建一个子进程,并和父进程的内存数据share一起用,保证子进程先执行;当子进程调用exit或exec之后,父进程再往下执行。
②、vfork的好处
起初,只有fork,但很多程序在fork一个子进程之后就exec一个外部程序,于是fork需要copy父进程的数据这个动作就毫无意义,而且这样干很重,所以就有了vfork,这样成本比较低。因此,vfork就是为exec而生
③、关于fork的优化
因为fork太重,每次创建一个子进程都会将父进程的数据copy到子进程中,而这步操作在很多时候是不必要的(子进程不需要对该内存修改),所以就有了写时拷贝(COW)的技术产生。也就是说,对于fork后并不是马上拷贝内存,而是只有当发生修改的时候,才会从父进程中拷贝到子进程中,这样fork后立马执行exec的成本就非常小了。所以,Linux的Man Page中并不鼓励使用vfork() 。