3. 进程控制
3.1. fork函数
#include <sys/types.h> #include <unistd.h> pid_t fork(void);
fork
调用失败则返回-1,调用成功的返回值见下面的解释。我们通过一个例子来理解fork
是怎样创建新进程的。
例 30.3. fork
#include <sys/types.h> #include <unistd.h> #include <stdio.h> #include <stdlib.h> int main(void) { pid_t pid; char *message; int n; pid = fork(); if (pid < 0) { perror("fork failed"); exit(1); } if (pid == 0) { message = "This is the child\n"; n = 6; } else { message = "This is the parent\n"; n = 3; } for(; n > 0; n--) { printf(message); sleep(1); } return 0; }
$ ./a.out This is the child This is the parent This is the child This is the parent This is the child This is the parent This is the child $ This is the child This is the child
这个程序的运行过程如下图所示。
图 30.4. fork
-
父进程初始化。
-
父进程调用
fork
,这是一个系统调用,因此进入内核。 -
内核根据父进程复制出一个子进程,父进程和子进程的PCB信息相同,用户态代码和数据也相同。因此,子进程现在的状态看起来和父进程一样,做完了初始化,刚调用了
fork
进入内核,还没有从内核返回。 -
现在有两个一模一样的进程看起来都调用了
fork
进入内核等待从内核返回(实际上fork
只调用了一次),此外系统中还有很多别的进程也等待从内核返回。是父进程先返回还是子进程先返回,还是这两个进程都等待,先去调度执行别的进程,这都不一定,取决于内核的调度算法。 -
如果某个时刻父进程被调度执行了,从内核返回后就从
fork
函数返回,保存在变量pid
中的返回值是子进程的id,是一个大于0的整数,因此执下面的else
分支,然后执行for
循环,打印"This is the parent\n"
三次之后终止。 -
如果某个时刻子进程被调度执行了,从内核返回后就从
fork
函数返回,保存在变量pid
中的返回值是0,因此执行下面的if (pid == 0)
分支,然后执行for
循环,打印"This is the child\n"
六次之后终止。fork
调用把父进程的数据复制一份给子进程,但此后二者互不影响,在这个例子中,fork
调用之后父进程和子进程的变量message
和n
被赋予不同的值,互不影响。 -
父进程每打印一条消息就睡眠1秒,这时内核调度别的进程执行,在1秒这么长的间隙里(对于计算机来说1秒很长了)子进程很有可能被调度到。同样地,子进程每打印一条消息就睡眠1秒,在这1秒期间父进程也很有可能被调度到。所以程序运行的结果基本上是父子进程交替打印,但这也不是一定的,取决于系统中其它进程的运行情况和内核的调度算法,如果系统中其它进程非常繁忙则有可能观察到不同的结果。另外,读者也可以把
sleep(1);
去掉看程序的运行结果如何。 -
这个程序是在Shell下运行的,因此Shell进程是父进程的父进程。父进程运行时Shell进程处于等待状态(第 3.3 节 “wait和waitpid函数”会讲到这种等待是怎么实现的),当父进程终止时Shell进程认为命令执行结束了,于是打印Shell提示符,而事实上子进程这时还没结束,所以子进程的消息打印到了Shell提示符后面。最后光标停在
This is the child
的下一行,这时用户仍然可以敲命令,即使命令不是紧跟在提示符后面,Shell也能正确读取。
fork
函数的特点概括起来就是“调用一次,返回两次”,在父进程中调用一次,在父进程和子进程中各返回一次。从上图可以看出,一开始是一个控制流程,调用fork
之后发生了分叉,变成两个控制流程,这也就是“fork”(分叉)这个名字的由来了。子进程中fork
的返回值是0,而父进程中fork
的返回值则是子进程的id(从根本上说fork
是从内核返回的,内核自有办法让父进程和子进程返回不同的值),这样当fork
函数返回后,程序员可以根据返回值的不同让父进程和子进程执行不同的代码。
fork
的返回值这样规定是有道理的。fork
在子进程中返回0,子进程仍可以调用getpid
函数得到自己的进程id,也可以调用getppid
函数得到父进程的id。在父进程中用getpid
可以得到自己的进程id,然而要想得到子进程的id,只有将fork
的返回值记录下来,别无它法。
fork
的另一个特性是所有由父进程打开的描述符都被复制到子进程中。父、子进程中相同编号的文件描述符在内核中指向同一个file
结构体,也就是说,file
结构体的引用计数要增加。
用gdb
调试多进程的程序会遇到困难,gdb
只能跟踪一个进程(默认是跟踪父进程),而不能同时跟踪多个进程,但可以设置gdb
在fork
之后跟踪父进程还是子进程。以上面的程序为例:
$ gcc main.c -g $ gdb a.out GNU gdb 6.8-debian Copyright (C) 2008 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html> This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. Type "show copying" and "show warranty" for details. This GDB was configured as "i486-linux-gnu"... (gdb) l 2 #include <unistd.h> 3 #include <stdio.h> 4 #include <stdlib.h> 5 6 int main(void) 7 { 8 pid_t pid; 9 char *message; 10 int n; 11 pid = fork(); (gdb) 12 if(pid<0) { 13 perror("fork failed"); 14 exit(1); 15 } 16 if(pid==0) { 17 message = "This is the child\n"; 18 n = 6; 19 } else { 20 message = "This is the parent\n"; 21 n = 3; (gdb) b 17 Breakpoint 1 at 0x8048481: file main.c, line 17. (gdb) set follow-fork-mode child (gdb) r Starting program: /home/akaedu/a.out This is the parent [Switching to process 30725] Breakpoint 1, main () at main.c:17 17 message = "This is the child\n"; (gdb) This is the parent This is the parent
set follow-fork-mode child
命令设置gdb
在fork
之后跟踪子进程(set follow-fork-mode parent
则是跟踪父进程),然后用run
命令,看到的现象是父进程一直在运行,在(gdb)
提示符下打印消息,而子进程被先前设的断点打断了。
Linux进程控制-fork函数/LinuxC编程一站式学习//