III Linux系统编程 30进程3 进程控制.docx
- 文档编号:18618839
- 上传时间:2023-08-20
- 格式:DOCX
- 页数:16
- 大小:50.10KB
III Linux系统编程 30进程3 进程控制.docx
《III Linux系统编程 30进程3 进程控制.docx》由会员分享,可在线阅读,更多相关《III Linux系统编程 30进程3 进程控制.docx(16页珍藏版)》请在冰点文库上搜索。
IIILinux系统编程30进程3进程控制
第 30 章 进程
3. 进程控制
3.1. fork函数
#include
#include
pid_tfork(void);
fork调用失败则返回-1,调用成功的返回值见下面的解释。
我们通过一个例子来理解fork是怎样创建新进程的。
例 30.3. fork
#include
#include
#include
#include
intmain(void)
{
pid_tpid;
char*message;
intn;
pid=fork();
if(pid<0){
perror("forkfailed");
exit
(1);
}
if(pid==0){
message="Thisisthechild\n";
n=6;
}else{
message="Thisistheparent\n";
n=3;
}
for(;n>0;n--){
printf(message);
sleep
(1);
}
return0;
}
$./a.out
Thisisthechild
Thisistheparent
Thisisthechild
Thisistheparent
Thisisthechild
Thisistheparent
Thisisthechild
$Thisisthechild
Thisisthechild
这个程序的运行过程如下图所示。
图 30.4. fork
1.父进程初始化。
2.父进程调用fork,这是一个系统调用,因此进入内核。
3.内核根据父进程复制出一个子进程,父进程和子进程的PCB信息相同,用户态代码和数据也相同。
因此,子进程现在的状态看起来和父进程一样,做完了初始化,刚调用了fork进入内核,还没有从内核返回。
4.现在有两个一模一样的进程看起来都调用了fork进入内核等待从内核返回(实际上fork只调用了一次),此外系统中还有很多别的进程也等待从内核返回。
是父进程先返回还是子进程先返回,还是这两个进程都等待,先去调度执行别的进程,这都不一定,取决于内核的调度算法。
5.如果某个时刻父进程被调度执行了,从内核返回后就从fork函数返回,保存在变量pid中的返回值是子进程的id,是一个大于0的整数,因此执下面的else分支,然后执行for循环,打印"Thisistheparent\n"三次之后终止。
6.如果某个时刻子进程被调度执行了,从内核返回后就从fork函数返回,保存在变量pid中的返回值是0,因此执行下面的if(pid==0)分支,然后执行for循环,打印"Thisisthechild\n"六次之后终止。
fork调用把父进程的数据复制一份给子进程,但此后二者互不影响,在这个例子中,fork调用之后父进程和子进程的变量message和n被赋予不同的值,互不影响。
7.父进程每打印一条消息就睡眠1秒,这时内核调度别的进程执行,在1秒这么长的间隙里(对于计算机来说1秒很长了)子进程很有可能被调度到。
同样地,子进程每打印一条消息就睡眠1秒,在这1秒期间父进程也很有可能被调度到。
所以程序运行的结果基本上是父子进程交替打印,但这也不是一定的,取决于系统中其它进程的运行情况和内核的调度算法,如果系统中其它进程非常繁忙则有可能观察到不同的结果。
另外,读者也可以把sleep
(1);去掉看程序的运行结果如何。
8.这个程序是在Shell下运行的,因此Shell进程是父进程的父进程。
父进程运行时Shell进程处于等待状态(第 3.3 节“wait和waitpid函数”会讲到这种等待是怎么实现的),当父进程终止时Shell进程认为命令执行结束了,于是打印Shell提示符,而事实上子进程这时还没结束,所以子进程的消息打印到了Shell提示符后面。
最后光标停在Thisisthechild的下一行,这时用户仍然可以敲命令,即使命令不是紧跟在提示符后面,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之后跟踪父进程还是子进程。
以上面的程序为例:
$gccmain.c-g
$gdba.out
GNUgdb6.8-debian
Copyright(C)2008FreeSoftwareFoundation,Inc.
LicenseGPLv3+:
GNUGPLversion3orlater //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. $./upper THISISTHEFILE,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”的输出。
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- III Linux系统编程 30进程3 进程控制 Linux 系统 编程 30 进程 控制
![提示](https://static.bingdoc.com/images/bang_tan.gif)