//gnu.org/licenses/gpl.html>
Thisisfreesoftware:
youarefreetochangeandredistributeit.
ThereisNOWARRANTY,totheextentpermittedbylaw.Type"showcopying"
and"showwarranty"fordetails.
ThisGDBwasconfiguredas"i486-linux-gnu"...
(gdb)l
2#include
3#include
4#include
5
6intmain(void)
7{
8pid_tpid;
9char*message;
10intn;
11pid=fork();
(gdb)
12if(pid<0){
13perror("forkfailed");
14exit
(1);
15}
16if(pid==0){
17message="Thisisthechild\n";
18n=6;
19}else{
20message="Thisistheparent\n";
21n=3;
(gdb)b17
Breakpoint1at0x8048481:
filemain.c,line17.
(gdb)setfollow-fork-modechild
(gdb)r
Startingprogram:
/home/akaedu/a.out
Thisistheparent
[Switchingtoprocess30725]
Breakpoint1,main()atmain.c:
17
17message="Thisisthechild\n";
(gdb)Thisistheparent
Thisistheparent
setfollow-fork-modechild命令设置gdb在fork之后跟踪子进程(setfollow-fork-modeparent则是跟踪父进程),然后用run命令,看到的现象是父进程一直在运行,在(gdb)提示符下打印消息,而子进程被先前设的断点打断了。
3.2. exec函数
用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。
当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。
调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。
其实有六种以exec开头的函数,统称exec函数:
#include
intexecl(constchar*path,constchar*arg,...);
intexeclp(constchar*file,constchar*arg,...);
intexecle(constchar*path,constchar*arg,...,char*constenvp[]);
intexecv(constchar*path,char*constargv[]);
intexecvp(constchar*file,char*constargv[]);
intexecve(constchar*path,char*constargv[],char*constenvp[]);
这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回,如果调用出错则返回-1,所以exec函数只有出错的返回值而没有成功的返回值。
这些函数原型看起来很容易混,但只要掌握了规律就很好记。
不带字母p(表示path)的exec函数第一个参数必须是程序的相对路径或绝对路径,例如"/bin/ls"或"./a.out",而不能是"ls"或"a.out"。
对于带字母p的函数:
∙如果参数中包含/,则将其视为路径名。
∙否则视为不带路径的程序名,在PATH环境变量的目录列表中搜索这个程序。
带有字母l(表示list)的exec函数要求将新程序的每个命令行参数都当作一个参数传给它,命令行参数的个数是可变的,因此函数原型中有...,...中的最后一个可变参数应该是NULL,起sentinel的作用。
对于带有字母v(表示vector)的函数,则应该先构造一个指向各参数的指针数组,然后将该数组的首地址当作参数传给它,数组中的最后一个指针也应该是NULL,就像main函数的argv参数或者环境变量表一样。
对于以e(表示environment)结尾的exec函数,可以把一份新的环境变量表传给它,其他exec函数仍使用当前的环境变量表执行新程序。
exec调用举例如下:
char*constps_argv[]={"ps","-o","pid,ppid,pgrp,session,tpgid,comm",NULL};
char*constps_envp[]={"PATH=/bin:
/usr/bin","TERM=console",NULL};
execl("/bin/ps","ps","-o","pid,ppid,pgrp,session,tpgid,comm",NULL);
execv("/bin/ps",ps_argv);
execle("/bin/ps","ps","-o","pid,ppid,pgrp,session,tpgid,comm",NULL,ps_envp);
execve("/bin/ps",ps_argv,ps_envp);
execlp("ps","ps","-o","pid,ppid,pgrp,session,tpgid,comm",NULL);
execvp("ps",ps_argv);
事实上,只有execve是真正的系统调用,其它五个函数最终都调用execve,所以execve在man手册第2节,其它函数在man手册第3节。
这些函数之间的关系如下图所示。
图 30.5. exec函数族
一个完整的例子:
#include
#include
intmain(void)
{
execlp("ps","ps","-o","pid,ppid,pgrp,session,tpgid,comm",NULL);
perror("execps");
exit
(1);
}
执行此程序则得到:
$./a.out
PIDPPIDPGRPSESSTPGIDCOMMAND
66146608661466147199bash
71996614719966147199ps
由于exec函数只有错误返回值,只要返回了一定是出错了,所以不需要判断它的返回值,直接在后面调用perror即可。
注意在调用execlp时传了两个"ps"参数,第一个"ps"是程序名,execlp函数要在PATH环境变量中找到这个程序并执行它,而第二个"ps"是第一个命令行参数,execlp函数并不关心它的值,只是简单地把它传给ps程序,ps程序可以通过main函数的argv[0]取到这个参数。
调用exec后,原来打开的文件描述符仍然是打开的[37]。
利用这一点可以实现I/O重定向。
先看一个简单的例子,把标准输入转成大写然后打印到标准输出:
例 30.4. upper
/*upper.c*/
#include
intmain(void)
{
intch;
while((ch=getchar())!
=EOF){
putchar(toupper(ch));
}
return0;
}
运行结果如下:
$./upper
helloTHERE
HELLOTHERE
(按Ctrl-D表示EOF)
$
使用Shell重定向:
$catfile.txt
thisisthefile,file.txt,itisalllowercase.
$./upperTHISISTHEFILE,FILE.TXT,ITISALLLOWERCASE.
如果希望把待转换的文件名放在命令行参数中,而不是借助于输入重定向,我们可以利用upper程序的现有功能,再写一个包装程序wrapper。
例 30.5. wrapper
/*wrapper.c*/
#include
#include
#include
#include
intmain(intargc,char*argv[])
{
intfd;
if(argc!
=2){
fputs("usage:
wrapperfile\n",stderr);
exit
(1);
}
fd=open(argv[1],O_RDONLY);
if(fd<0){
perror("open");
exit
(1);
}
dup2(fd,STDIN_FILENO);
close(fd);
execl("./upper","upper",NULL);
perror("exec./upper");
exit
(1);
}
wrapper程序将命令行参数当作文件名打开,将标准输入重定向到这个文件,然后调用exec执行upper程序,这时原来打开的文件描述符仍然是打开的,upper程序只负责从标准输入读入字符转成大写,并不关心标准输入对应的是文件还是终端。
运行结果如下:
$./wrapperfile.txt
THISISTHEFILE,FILE.TXT,ITISALLLOWERCASE.
3.3. wait和waitpid函数
一个进程在终止时会关闭所有文件描述符,释放在用户空间分配的内存,但它的PCB还保留着,内核在其中保存了一些信息:
如果是正常终止则保存着退出状态,如果是异常终止则保存着导致该进程终止的信号是哪个。
这个进程的父进程可以调用wait或waitpid获取这些信息,然后彻底清除掉这个进程。
我们知道一个进程的退出状态可以在Shell中用特殊变量$?
查看,因为Shell是它的父进程,当它终止时Shell调用wait或waitpid得到它的退出状态同时彻底清除掉这个进程。
如果一个进程已经终止,但是它的父进程尚未调用wait或waitpid对它进行清理,这时的进程状态称为僵尸(Zombie)进程。
任何进程在刚终止时都是僵尸进程,正常情况下,僵尸进程都立刻被父进程清理了,为了观察到僵尸进程,我们自己写一个不正常的程序,父进程fork出子进程,子进程终止,而父进程既不终止也不调用wait清理子进程:
#include
#include
intmain(void)
{
pid_tpid=fork();
if(pid<0){
perror("fork");
exit
(1);
}
if(pid>0){/*parent*/
while
(1);
}
/*child*/
return0;
}
在后台运行这个程序,然后用ps命令查看:
$./a.out&
[1]6130
$psu
USERPID%CPU%MEMVSZRSSTTYSTATSTARTTIMECOMMAND
akaedu60160.00.357243140pts/0Ss08:
410:
00bash
akaedu613097.20.01536284pts/0R08:
4414:
33./a.out
akaedu61310.00.000pts/0Z08:
440:
00[a.out]
akaedu61630.00.026201000pts/0R+08:
590:
00psu
在./a.out命令后面加个&表示后台运行,Shell不等待这个进程终止就立刻打印提示符并等待用户输命令。
现在Shell是位于前台的,用户在终端的输入会被Shell读取,后台进程是读不到终端输入的。
第二条命令psu是在前台运行的,在此期间Shell进程和./a.out进程都在后台运行,等到psu命令结束时Shell进程又重新回到前台。
在第 33 章信号和第 34 章终端、作业控制与守护进程将会进一步解释前台(Foreground)和后台(Backgroud)的概念。
父进程的pid是6130,子进程是僵尸进程,pid是6131,ps命令显示僵尸进程的状态为Z,在命令行一栏还显示。
如果一个父进程终止,而它的子进程还存在(这些子进程或者仍在运行,或者已经是僵尸进程了),则这些子进程的父进程改为init进程。
init是系统中的一个特殊进程,通常程序文件是/sbin/init,进程id是1,在系统启动时负责启动各种系统服务,之后就负责清理子进程,只要有子进程终止,init就会调用wait函数清理它。
僵尸进程是不能用kill命令清除掉的,因为kill命令只是用来终止进程的,而僵尸进程已经终止了。
思考一下,用什么办法可以清除掉僵尸进程?
wait和waitpid函数的原型是:
#include
#include
pid_twait(int*status);
pid_twaitpid(pid_tpid,int*status,intoptions);
若调用成功则返回清理掉的子进程id,若调用出错则返回-1。
父进程调用wait或waitpid时可能会:
∙阻塞(如果它的所有子进程都还在运行)。
∙带子进程的终止信息立即返回(如果一个子进程已终止,正等待父进程读取其终止信息)。
∙出错立即返回(如果它没有任何子进程)。
这两个函数的区别是:
∙如果父进程的所有子进程都还在运行,调用wait将使父进程阻塞,而调用waitpid时如果在options参数中指定WNOHANG可以使父进程不阻塞而立即返回0。
∙wait等待第一个终止的子进程,而waitpid可以通过pid参数指定等待哪一个子进程。
可见,调用wait和waitpid不仅可以获得子进程的终止信息,还可以使父进程阻塞等待子进程终止,起到进程间同步的作用。
如果参数status不是空指针,则子进程的终止信息通过这个参数传出,如果只是为了同步而不关心子进程的终止信息,可以将status参数指定为NULL。
例 30.6. waitpid
#include
#include
#include
#include
#include
intmain(void)
{
pid_tpid;
pid=fork();
if(pid<0){
perror("forkfailed");
exit
(1);
}
if(pid==0){
inti;
for(i=3;i>0;i--){
printf("Thisisthechild\n");
sleep
(1);
}
exit(3);
}else{
intstat_val;
waitpid(pid,&stat_val,0);
if(WIFEXITED(stat_val))
printf("Childexitedwithcode%d\n",WEXITSTATUS(stat_val));
elseif(WIFSIGNALED(stat_val))
printf("Childterminatedabnormally,signal%d\n",WTERMSIG(stat_val));
}
return0;
}
子进程的终止信息在一个int中包含了多个字段,用宏定义可以取出其中的每个字段:
如果子进程是正常终止的,WIFEXITED取出的字段值非零,WEXITSTATUS取出的字段值就是子进程的退出状态;如果子进程是收到信号而异常终止的,WIFSIGNALED取出的字段值非零,WTERMSIG取出的字段值就是信号的编号。
作为练习,请读者从头文件里查一下这些宏做了什么运算,是如何取出字段值的。
习题
1、请读者修改例 30.6“waitpid”的代码和实验条件,使它产生“Childterminatedabnormally”的输出。