对于刚刚接触Linux的同学,fork一定是大家最头疼的概念,它看起来很简单,但理解起来却十分的复杂。我们先来从一道经典的笔试题开始。
#include
#include
#include
#include
int main()
{
int i;
for (i = 0; i < 2; i++)
{
fork();
printf("-");
}
wait(NULL);
wait(NULL);
return 0;
}
问:代码一共会输出几个‘-’?
大家可以先打开电脑把代码敲一遍,看看结果怎么样。我们待会再讲。
先从fork的man手册说起吧。
FORK(2) Linux Programmer's Manual FORK(2)
NAME
fork - create a child process
SYNOPSIS
#include
pid_t fork(void);
RETURN VALUE
On success, the PID of the child process is returned in the parent, and 0 is returned in the child. On failure,
-1 is returned in the parent, no child process is created, and errno is set appropriately.
这英文感觉也不难,所以我就不翻译啦!
fork函数调用一次却返回两次:向父进程返回子进程的ID,向子进程中返回0。这是因为父进程可能存在很多个子进程,所以必须通过返回的子进程ID来跟踪子进程。
看一段代码:
#include
#include
#include
#include
int main()
{
pid_t pid;
int count=0;
pid = fork();
printf( "This is first time, who am i? pid = %d\n", pid );
count++;
printf( "count = %d\n", count );
if (-1 == pid)
{
perror("fork");
}
else if (pid > 0)
{
printf("This is the parent process!\n");
wait(NULL);
}
else if (pid == 0)
{
printf("This is the child process!\n");
}
printf("This is second time, who am i? pid = %d\n", pid);
return 0;
}
运行结果如下:
这个结果很奇怪了,为什么13行和32行的printf语句执行两次,而15行的“count++”语句却只执行了一次?
把代码稍微修改一下,并且加上注释:
#include
#include
#include
#include
int main()
{
pid_t pid;
int count=0;
/*通过fork系统调用创建了一个新的进程
这个进程共享父进程的数据和堆栈空间等,
fork调用是一个复制父进程的过程,
fork不像线程需提供一个函数做为入口,
fork调用后,新进程的入口就在fork的下一条语句。*/
pid = fork();
/*通过pid的值可以判断此时执行的是父进程还是子进程,都有可能 */
printf( "This is first time, who am i? pid = %d\n", pid );
if (-1 == pid)
{
perror("fork");
}
else if (pid > 0)
{
printf("This is the parent process!\n");
/*fork系统调用向父进程返回子进程的pid,
count仍然为0, 因为父进程中的count始终没有被重新赋值,
这里就可以看出子进程的数据和堆栈空间和父进程是独立的,
而不是共享数据。*/
printf("Parent process count = %d\n", count);
wait(NULL);
}
else if (pid == 0)
{
printf("This is the child process!\n");
/*在子进程中对count进行自加1的操作,
但是并没有影响到父进程中的count值,
父进程中的count值仍然为0。*/
count++;
printf("Child process count = %d\n", count);
}
printf("This is second time, who am i? pid = %d\n", pid);
return 0;
}
结果如下:
看这个程序的时候,头脑中必须首先了解一个概念:在语句pid=fork()之前,只有一个进程在执行这段代码,但在这条语句之后,就变成两个进程在执行了,这两个进程的代码部分相同,将要执行的下一条语句都是fork下面的判断语句。
父子进程的区别除了进程标识符(process ID)不同外,变量pid的值也不相同,pid存放的是fork的返回值。fork调用的一个奇妙之处就是它仅仅被调用一次,却能够返回两次,它可能有三种不同的返回值:
fork出错可能有两种原因:
当前的进程数已经达到了系统规定的上限,这时errno的值被设置为EAGAIN;
系统内存不足,这时errno的值被设置为ENOMEM。
接下来我们来看看《UNIX环境高级编程》中对fork的说明:
The new process created by fork is called the child process. This function is called once but returns twice. The only difference in the returns is that the return value in the child is 0, whereas the return value in the parent is the process ID of the new child. The reason the child’s process ID is returned to the parent is that a process can have more than one child, and there is no function that allows a process to o^ain the process IDs of its children. The reason fork returns 0 to the child is that a process can have only a single parent, and the child can always call getppid to o^ain the process ID of its parent. (Process ID 0 is reserved for use by the kernel, so it’s not possible for 0 to be the process ID of a child.)
被fork创建的新进程叫做子进程。fork函数被调用一次,却有两次返回。返回值唯一的区别是在子进程中返回0,而在父进程中返回子进程的pid。在父进程中要返回子进程的pid的原因是父进程可能有不止一个子进程,而一个进程又没有任何函数可以得到他的子进程的pid。子进程返回0是因为它只有一个父进程,并且可以通过getppid来获得父进程id。
Both the child and the parent continue executing with the instruction that follows the call to fork. The child is a copy of the parent. For example, the child gets a copy of the parent’s data space, heap, and stack. Note that this is a copy for the child; the parent and the child do not share these portions of memory. The parent and the child share the text segment (Section 7.6).
子进程和父进程都执行在fork函数调用之后的代码,子进程是父进程的一个拷贝。例如,父进程的数据空间、堆栈空间都会给子进程一个拷贝,而不是共享这些内存。
仔细分析后,我们就可以知道:
一个程序一旦调用fork函数,系统就为一个新的进程分配了新的地址空间(包含数据段、代码段、堆栈段)。首先,系统让新的进程与旧的进程使用同一个代码段,因为它们的程序还是相同的;对于数据段和堆栈段,当代码中涉及到写内存操作时,系统则复制一份给新的进程,这样,父进程的所有数据都可以留给子进程;但是,子进程一旦开始运行,虽然它继承了父进程的一切数据,但实际上数据却已经分开,相互之间不再有影响了,也就是说,它们之间不再共享任何数据了。
fork()不仅创建出与父进程数据相同的子进程,而且父进程在fork执行点的所有上下文场景也被自动复制到子进程中,包括:
全局和局部变量
打开的文件句柄
共享内存、消息等同步对象
信号
最后,再来看看文章开头留给大家的问题。
答案是8!
第一次循环:
父进程通过fork创建子进程,然后父子进程都会执行下面的printf语句。两次!
第二次循环:
需要注意的是,因为有了第一次循环,已经创建了子进程,所以父子进程都会进入第二次循环。
父进程第二次循环,再次创建子进程,两个进程都会执行printf语句,所以打印“-”再加两次!
子进程第二次循环,再次创建孙进程,两个进程都会执行printf语句,于是打印“-”再加两次!
不对呀,这样一分析结果是6呀!大家再把代码敲一遍,每次输出的时候,加上换行符:
printf("-\n");
这样修改代码后,结果还真的是6,跟我们分析的一样。但是为什么加上换行符后结果就不一样了呢?
这是因为printf语句有缓冲区(属于行缓冲,遇到换行符才会输出)。所以,对于上述没有换行符程序,printf把“-”放到了缓存中,并没有真正的输出,在fork的时候,缓存被复制到了子进程空间,所以,就多了两个,变成了8个。
什么玩应?
没看懂是吧!我来画张图给大家看看!
更多内容,关注公众号 学益得智能硬件。