III Linux系统编程_ 30进程_3 进程控制.doc

上传人:asd****56 文档编号:79333692 上传时间:2023-03-21 格式:DOC 页数:17 大小:101.50KB
返回 下载 相关 举报
III Linux系统编程_ 30进程_3 进程控制.doc_第1页
第1页 / 共17页
III Linux系统编程_ 30进程_3 进程控制.doc_第2页
第2页 / 共17页
点击查看更多>>
资源描述

《III Linux系统编程_ 30进程_3 进程控制.doc》由会员分享,可在线阅读,更多相关《III Linux系统编程_ 30进程_3 进程控制.doc(17页珍藏版)》请在taowenge.com淘文阁网|工程机械CAD图纸|机械工程制图|CAD装配图下载|SolidWorks_CaTia_CAD_UG_PROE_设计图分享下载上搜索。

1、第30章进程3.进程控制3.1.fork函数#include #include pid_t fork(void);fork调用失败则返回-1,调用成功的返回值见下面的解释。我们通过一个例子来理解fork是怎样创建新进程的。例30.3.fork#include #include #include #include int main(void)pid_t pid;char *message;int n;pid = fork();if (pid 0; n-) printf(message);sleep(1);return 0;$ ./a.out This is the childThis is th

2、e parentThis is the childThis is the parentThis is the childThis is the parentThis is the child$ This is the childThis is the child这个程序的运行过程如下图所示。图30.4.fork1. 父进程初始化。2. 父进程调用fork,这是一个系统调用,因此进入内核。3. 内核根据父进程复制出一个子进程,父进程和子进程的PCB信息相同,用户态代码和数据也相同。因此,子进程现在的状态看起来和父进程一样,做完了初始化,刚调用了fork进入内核,还没有从内核返回。4. 现在有两个

3、一模一样的进程看起来都调用了fork进入内核等待从内核返回(实际上fork只调用了一次),此外系统中还有很多别的进程也等待从内核返回。是父进程先返回还是子进程先返回,还是这两个进程都等待,先去调度执行别的进程,这都不一定,取决于内核的调度算法。5. 如果某个时刻父进程被调度执行了,从内核返回后就从fork函数返回,保存在变量pid中的返回值是子进程的id,是一个大于0的整数,因此执下面的else分支,然后执行for循环,打印This is the parentn三次之后终止。6. 如果某个时刻子进程被调度执行了,从内核返回后就从fork函数返回,保存在变量pid中的返回值是0,因此执行下面的i

4、f (pid = 0)分支,然后执行for循环,打印This is the childn六次之后终止。fork调用把父进程的数据复制一份给子进程,但此后二者互不影响,在这个例子中,fork调用之后父进程和子进程的变量message和n被赋予不同的值,互不影响。7. 父进程每打印一条消息就睡眠1秒,这时内核调度别的进程执行,在1秒这么长的间隙里(对于计算机来说1秒很长了)子进程很有可能被调度到。同样地,子进程每打印一条消息就睡眠1秒,在这1秒期间父进程也很有可能被调度到。所以程序运行的结果基本上是父子进程交替打印,但这也不是一定的,取决于系统中其它进程的运行情况和内核的调度算法,如果系统中其它进

5、程非常繁忙则有可能观察到不同的结果。另外,读者也可以把sleep(1);去掉看程序的运行结果如何。8. 这个程序是在Shell下运行的,因此Shell进程是父进程的父进程。父进程运行时Shell进程处于等待状态(第3.3节 “wait和waitpid函数”会讲到这种等待是怎么实现的),当父进程终止时Shell进程认为命令执行结束了,于是打印Shell提示符,而事实上子进程这时还没结束,所以子进程的消息打印到了Shell提示符后面。最后光标停在This is the child的下一行,这时用户仍然可以敲命令,即使命令不是紧跟在提示符后面,Shell也能正确读取。fork函数的特点概括起来就是“

6、调用一次,返回两次”,在父进程中调用一次,在父进程和子进程中各返回一次。从上图可以看出,一开始是一个控制流程,调用fork之后发生了分叉,变成两个控制流程,这也就是“fork”(分叉)这个名字的由来了。子进程中fork的返回值是0,而父进程中fork的返回值则是子进程的id(从根本上说fork是从内核返回的,内核自有办法让父进程和子进程返回不同的值),这样当fork函数返回后,程序员可以根据返回值的不同让父进程和子进程执行不同的代码。fork的返回值这样规定是有道理的。fork在子进程中返回0,子进程仍可以调用getpid函数得到自己的进程id,也可以调用getppid函数得到父进程的id。在

7、父进程中用getpid可以得到自己的进程id,然而要想得到子进程的id,只有将fork的返回值记录下来,别无它法。fork的另一个特性是所有由父进程打开的描述符都被复制到子进程中。父、子进程中相同编号的文件描述符在内核中指向同一个file结构体,也就是说,file结构体的引用计数要增加。用gdb调试多进程的程序会遇到困难,gdb只能跟踪一个进程(默认是跟踪父进程),而不能同时跟踪多个进程,但可以设置gdb在fork之后跟踪父进程还是子进程。以上面的程序为例:$ gcc main.c -g$ gdb a.outGNU gdb 6.8-debianCopyright (C) 2008 Free S

8、oftware Foundation, Inc.License GPLv3+: GNU GPL version 3 or later This is free software: you are free to change and redistribute it.There is NO WARRANTY, to the extent permitted by law. Type show copyingand show warranty for details.This GDB was configured as i486-linux-gnu.(gdb) l2#include 3#inclu

9、de 4#include 56int main(void)78pid_t pid;9char *message;10int n;11pid = fork();(gdb) 12if(pid0) 13perror(fork failed);14exit(1);1516if(pid=0) 17message = This is the childn;18n = 6;19 else 20message = This is the parentn;21n = 3;(gdb) b 17Breakpoint 1 at 0x8048481: file main.c, line 17.(gdb) set fol

10、low-fork-mode child(gdb) rStarting program: /home/akaedu/a.out This is the parentSwitching to process 30725Breakpoint 1, main () at main.c:1717message = This is the childn;(gdb) This is the parentThis is the parentset follow-fork-mode child命令设置gdb在fork之后跟踪子进程(set follow-fork-mode parent则是跟踪父进程),然后用r

11、un命令,看到的现象是父进程一直在运行,在(gdb)提示符下打印消息,而子进程被先前设的断点打断了。3.2.exec函数用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。其实有六种以exec开头的函数,统称exec函数:#include int execl(const char *path, const char *arg, .);int exe

12、clp(const char *file, const char *arg, .);int execle(const char *path, const char *arg, ., char *const envp);int execv(const char *path, char *const argv);int execvp(const char *file, char *const argv);int execve(const char *path, char *const argv, char *const envp);这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回,如

13、果调用出错则返回-1,所以exec函数只有出错的返回值而没有成功的返回值。这些函数原型看起来很容易混,但只要掌握了规律就很好记。不带字母p(表示path)的exec函数第一个参数必须是程序的相对路径或绝对路径,例如/bin/ls或./a.out,而不能是ls或a.out。对于带字母p的函数: 如果参数中包含/,则将其视为路径名。 否则视为不带路径的程序名,在PATH环境变量的目录列表中搜索这个程序。带有字母l(表示list)的exec函数要求将新程序的每个命令行参数都当作一个参数传给它,命令行参数的个数是可变的,因此函数原型中有.,.中的最后一个可变参数应该是NULL,起sentinel的作用

14、。对于带有字母v(表示vector)的函数,则应该先构造一个指向各参数的指针数组,然后将该数组的首地址当作参数传给它,数组中的最后一个指针也应该是NULL,就像main函数的argv参数或者环境变量表一样。对于以e(表示environment)结尾的exec函数,可以把一份新的环境变量表传给它,其他exec函数仍使用当前的环境变量表执行新程序。exec调用举例如下:char *const ps_argv =ps, -o, pid,ppid,pgrp,session,tpgid,comm, NULL;char *const ps_envp =PATH=/bin:/usr/bin, TERM=co

15、nsole, 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是真正

16、的系统调用,其它五个函数最终都调用execve,所以execve在man手册第2节,其它函数在man手册第3节。这些函数之间的关系如下图所示。图30.5.exec函数族一个完整的例子:#include #include int main(void)execlp(ps, ps, -o, pid,ppid,pgrp,session,tpgid,comm, NULL);perror(exec ps);exit(1);执行此程序则得到:$ ./a.out PID PPID PGRP SESS TPGID COMMAND 6614 6608 6614 6614 7199 bash 7199 6614 7

17、199 6614 7199 ps由于exec函数只有错误返回值,只要返回了一定是出错了,所以不需要判断它的返回值,直接在后面调用perror即可。注意在调用execlp时传了两个ps参数,第一个ps是程序名,execlp函数要在PATH环境变量中找到这个程序并执行它,而第二个ps是第一个命令行参数,execlp函数并不关心它的值,只是简单地把它传给ps程序,ps程序可以通过main函数的argv0取到这个参数。调用exec后,原来打开的文件描述符仍然是打开的37。利用这一点可以实现I/O重定向。先看一个简单的例子,把标准输入转成大写然后打印到标准输出:例30.4.upper/* upper.c

18、 */#include int main(void)int ch;while(ch = getchar() != EOF) putchar(toupper(ch);return 0;运行结果如下:$ ./upperhello THEREHELLO THERE(按Ctrl-D表示EOF)$使用Shell重定向:$ cat file.txtthis is the file, file.txt, it is all lower case.$ ./upper file.txtTHIS IS THE FILE, FILE.TXT, IT IS ALL LOWER CASE.如果希望把待转换的文件名放在命

19、令行参数中,而不是借助于输入重定向,我们可以利用upper程序的现有功能,再写一个包装程序wrapper。例30.5.wrapper/* wrapper.c */#include #include #include #include int main(int argc, char *argv)int fd;if (argc != 2) fputs(usage: wrapper filen, stderr);exit(1);fd = open(argv1, O_RDONLY);if(fd0) perror(open);exit(1);dup2(fd, STDIN_FILENO);close(fd

20、);execl(./upper, upper, NULL);perror(exec ./upper);exit(1);wrapper程序将命令行参数当作文件名打开,将标准输入重定向到这个文件,然后调用exec执行upper程序,这时原来打开的文件描述符仍然是打开的,upper程序只负责从标准输入读入字符转成大写,并不关心标准输入对应的是文件还是终端。运行结果如下:$ ./wrapper file.txtTHIS IS THE FILE, FILE.TXT, IT IS ALL LOWER CASE.3.3.wait和waitpid函数一个进程在终止时会关闭所有文件描述符,释放在用户空间分配的内

21、存,但它的PCB还保留着,内核在其中保存了一些信息:如果是正常终止则保存着退出状态,如果是异常终止则保存着导致该进程终止的信号是哪个。这个进程的父进程可以调用wait或waitpid获取这些信息,然后彻底清除掉这个进程。我们知道一个进程的退出状态可以在Shell中用特殊变量$?查看,因为Shell是它的父进程,当它终止时Shell调用wait或waitpid得到它的退出状态同时彻底清除掉这个进程。如果一个进程已经终止,但是它的父进程尚未调用wait或waitpid对它进行清理,这时的进程状态称为僵尸(Zombie)进程。任何进程在刚终止时都是僵尸进程,正常情况下,僵尸进程都立刻被父进程清理了,

22、为了观察到僵尸进程,我们自己写一个不正常的程序,父进程fork出子进程,子进程终止,而父进程既不终止也不调用wait清理子进程:#include #include int main(void)pid_t pid=fork();if(pid0) /* parent */while(1);/* child */return 0; 在后台运行这个程序,然后用ps命令查看:$ ./a.out &1 6130$ ps uUSER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMANDakaedu 6016 0.0 0.3 5724 3140 pts/0 Ss

23、08:41 0:00 bashakaedu 6130 97.2 0.0 1536 284 pts/0 R 08:44 14:33 ./a.outakaedu 6131 0.0 0.0 0 0 pts/0 Z 08:44 0:00 a.out akaedu 6163 0.0 0.0 2620 1000 pts/0 R+ 08:59 0:00 ps u在./a.out命令后面加个&表示后台运行,Shell不等待这个进程终止就立刻打印提示符并等待用户输命令。现在Shell是位于前台的,用户在终端的输入会被Shell读取,后台进程是读不到终端输入的。第二条命令ps u是在前台运行的,在此期间Shell

24、进程和./a.out进程都在后台运行,等到ps u命令结束时Shell进程又重新回到前台。在第33章 信号和第34章 终端、作业控制与守护进程将会进一步解释前台(Foreground)和后台(Backgroud)的概念。父进程的pid是6130,子进程是僵尸进程,pid是6131,ps命令显示僵尸进程的状态为Z,在命令行一栏还显示。如果一个父进程终止,而它的子进程还存在(这些子进程或者仍在运行,或者已经是僵尸进程了),则这些子进程的父进程改为init进程。init是系统中的一个特殊进程,通常程序文件是/sbin/init,进程id是1,在系统启动时负责启动各种系统服务,之后就负责清理子进程,只

25、要有子进程终止,init就会调用wait函数清理它。僵尸进程是不能用kill命令清除掉的,因为kill命令只是用来终止进程的,而僵尸进程已经终止了。思考一下,用什么办法可以清除掉僵尸进程?wait和waitpid函数的原型是:#include #include pid_t wait(int *status);pid_t waitpid(pid_t pid, int *status, int options);若调用成功则返回清理掉的子进程id,若调用出错则返回-1。父进程调用wait或waitpid时可能会: 阻塞(如果它的所有子进程都还在运行)。 带子进程的终止信息立即返回(如果一个子进程已

26、终止,正等待父进程读取其终止信息)。 出错立即返回(如果它没有任何子进程)。这两个函数的区别是: 如果父进程的所有子进程都还在运行,调用wait将使父进程阻塞,而调用waitpid时如果在options参数中指定WNOHANG可以使父进程不阻塞而立即返回0。 wait等待第一个终止的子进程,而waitpid可以通过pid参数指定等待哪一个子进程。可见,调用wait和waitpid不仅可以获得子进程的终止信息,还可以使父进程阻塞等待子进程终止,起到进程间同步的作用。如果参数status不是空指针,则子进程的终止信息通过这个参数传出,如果只是为了同步而不关心子进程的终止信息,可以将status参数

27、指定为NULL。例30.6.waitpid#include #include #include #include #include int main(void)pid_t pid;pid = fork();if (pid 0; i-) printf(This is the childn);sleep(1);exit(3); else int stat_val;waitpid(pid, &stat_val, 0);if (WIFEXITED(stat_val)printf(Child exited with code %dn, WEXITSTATUS(stat_val);else if (WIF

28、SIGNALED(stat_val)printf(Child terminated abnormally, signal %dn, WTERMSIG(stat_val);return 0;子进程的终止信息在一个int中包含了多个字段,用宏定义可以取出其中的每个字段:如果子进程是正常终止的,WIFEXITED取出的字段值非零,WEXITSTATUS取出的字段值就是子进程的退出状态;如果子进程是收到信号而异常终止的,WIFSIGNALED取出的字段值非零,WTERMSIG取出的字段值就是信号的编号。作为练习,请读者从头文件里查一下这些宏做了什么运算,是如何取出字段值的。习题1、请读者修改例30.6 “waitpid”的代码和实验条件,使它产生“Child terminated abnormally”的输出。37 事实上,在每个文件描述符中有一个close-on-exec标志,如果该标志为1,则调用exec时关闭这个文件描述符。该标志默认为0,可以用fcntl函数将它置1,本书不讨论该标志为1的情况。

展开阅读全文
相关资源
相关搜索

当前位置:首页 > 技术资料 > 其他杂项

本站为文档C TO C交易模式,本站只提供存储空间、用户上传的文档直接被用户下载,本站只是中间服务平台,本站所有文档下载所得的收益归上传人(含作者)所有。本站仅对用户上传内容的表现方式做保护处理,对上载内容本身不做任何修改或编辑。若文档所含内容侵犯了您的版权或隐私,请立即通知淘文阁网,我们立即给予删除!客服QQ:136780468 微信:18945177775 电话:18904686070

工信部备案号:黑ICP备15003705号© 2020-2023 www.taowenge.com 淘文阁