进程控制
原文链接:http://www.orlion.ga/1044/
一、fork函数
#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);
fork调用失败返回-1。下面通过一个例子来理解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; }
输出
fork-www.orlion.ga
程序运行过程:
1.父进程初始化
2.父进程调用fork,这是一个系统调用,因此进入内核
3.内核根据父进程复制出一个子进程,父进程和子进程的PCB信息相同,用户态和数据也相同。因此子进程现在的状态看起来和父进程一样,做完了初始化,刚调用了fork进入内核,还没有从内核中返回
4.现在有两个一模一样的进程看起来都调用了fork进入内核等待从内核返回(实际上fork值调用了一次),此外系统中还有很多别的进程也等待从内核返回。是父进程先返回还是子进程先返回,还是这两个进程都等待,先去调度执行别的进程,这都不一定,取决于内核的调度算法
5.如果某个时刻父进程被调度执行了,从内核返回后就从fork函数返回,保存在变量pid中的返回值是子进程的id,是一个大于0的整数,因此执行下面的else分支,然后执行for循环,打印"This is the parent\n"三次之后终止
6.如果某个时刻子进程被调度执行了,从内核返回后就从fork函数返回,保存在变量pid中的返回值是0,因此执行下面的if (pid == 0)分支,然后执行for循环,打印"This is the child\n"六次之后终止。fork调用把父进程的数据复制一份给子进程,但此后二者互不影响,在这个例子中,fork调用之后父进程和子进程的变量message和n被赋予不同的值,互不影响。
7.父进程每打印一条消息就睡眠1秒,这时内核调度别的进程执行,在1秒这么长的间隙里子进程有可能被调度到。同样的,子进程每打印一条消息就睡眠1秒,在这1秒期间父进程也很有可能被调度到。所以程序运行的结果基本上是父子进程交替打印
8.这个程序是在Shell下运行的,因此Shell进程是父进程的父进程。父进程运行时Shell进程处于等待状态,当父进程终止时Shell进程认为命令执行结束了,于是打印Shell提示符,而事实上子进程这时还没有结束,所以子进程的消息打印到了Shell提示符的后面。最后光标停在This is the child的下一行,这时用户仍然可以敲命令,即使命令不是紧跟在提示符后面,Shell也能正确读取。
fork函数的特点是"调用一次,返回两次",在父进程和子进程中各返回一次。fork的另一个特点是所有由父进程打开的描述符都复制到了子进程中。父、子进程中相同的编号的文件描述符在内核中指向同一个file结构体,也就是说file结构体的引用计数要增加。
2、exec函数
用fork创建子进程后执行的是和父进程相同的程序,子进程往往要调用一种exec函数以执行另一个程序。当进程调用exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变
有六种exec开头的函数:
#include <unistd.h> int execl(const char *path, const char *arg, ...); int execlp(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[]);
这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回,如果调用出错则返回-1,所以exec函数只有出错的返回值而没有成功返回的值。
例:
#include <unistd.h> #include <stdlib.h> int main(void) { execlp("ps", "ps", "-o", "pid,ppid,pgrp,session,tpgid,comm", NULL); perror("exec ps"); exit(1); }
结果:
因为exec只有错误返回值,只要有返回值一定是出错了,所以不需要判断它的返回值,直接在后面调用perror即可。
system是在单独的进程中执行命令完了还会回到自己的程序中,而exec函数是直接在自己的程序中执行新的程序,新的程序会把自己的程序覆盖,除非调用出错,否则再也回不到exec后面的代码,就是说自己的程序就变成了exec调用的那个程序了。
3、wait和waitpid函数
一个进程在终止时会关闭所有的文件描述符,释放在用户空间分配的内存,但它的PCB还保留着,内核在其中保存了一些信息:如果是正常终止则保存着退出状态,如果是异常终止则保存着导致该进程终止的信号是哪个。这个进程的父进程可以调用wait和waitpid获取这些信息,然后彻底清除掉这个进程。进程的退出状态可以在Shell中用$?查看,因为Shell是它的父进程,当它终止时Shell调用wait或waitpid得到它的退出状态同时彻底清除掉这个进程。
如果一个进程终止了,但是父进程尚未调用wait或waitpid对它进行清理,这时的进程状态称为僵尸(Zombie)进程。任何进程在刚终止时都是僵尸进程,正常情况下,僵尸进程都立刻被父进程清理了,为了观察到僵尸进程,下面写一个不正常的程序,父进程fork出子进程,子进程终止,而父进程既不终止也不调用wait请求进程:
#include <unistd.h> #include <stdlib.h> int main(void) { pid_t pid=fork(); if(pid<0) { perror("fork"); exit(1); } if(pid>0) { /* parent */ while(1); } /* child */ return 0; }
在后台运行这个程序:
在./zombie后边加&表示后台运行,Shell不等待这个进程终止就立刻打印提示符并等待用户输命令。现在Shell是位于前台的,用户在终端的输入会被Shell读取,后台进程是读不到终端输入的。第二条命令ps u是在前台运行的,在此期间Shell进程和zombie进程都在后台运行,等到ps u命令结束时Shell进程又重新回到前台。
父进程的pid是5818,子进程是僵尸进程pid是5819,ps命令显示僵尸进程的状态为z,在命令行一栏还显示<defunct>.
如果一个父进程终止而它的子进程还存在,则这些子进程的父进程改为init进程。init是系统中的一个特殊进程,通常程序文件是/sbin/init,进程id是1,在系统启动时负责启动各种系统服务,之后就负责清理子进程,只要有子进程终止,init就会调用wait函数清理它。
僵尸进程是不能用kill命令清除掉的,因为kill命令只是用来终止进程的,而僵尸进程已经终止了。
wait和waitpid原型:
#include <sys/types.h> #include <sys/wait.h> pid_t wait(int *status); pid_t waitpid(pid_t pid, int *status, int options);
若调用成功则返回清理掉的子进程id,若调用出错则返回-1.父进程调用wait或waitpid时可能会:
-
阻塞(如果它的所有子进程都还在运行)
-
带子进程的终止信息立即返回(如果一个子进程已终止,正等待父进程读取其终止信息)。
-
出错立即返回(如果它没有任何子进程)。
这两个函数的区别是:
-
如果父进程的所有子进程都还在运行,调用wait将使父进程阻塞,而调用waitpid时如果在options参数中指定WNOHANG可以使父进程不阻塞而立即返回0.
-
wait等待第一个终止的子进程,而waitpid可以通过指定pid参数指定等待哪一个子进程。
可见,调用wait和waitpid不仅可以获得子进程的终止信息,还可以使父进程阻塞等待父进程终止,起到进程间同步的作用。如果参数status不是空指针,则子进程的终止信息通过这个参数传出,如果只是为了同步而不关心子进程的终止信息,可以将status参数指定为NULL。
#include <sys/types.h> #include <sys/wait.h> #include <unistd.h> #include <stdio.h> #include <stdlib.h> int main(void) { pid_t pid; pid = fork(); if (pid < 0) { perror("fork failed"); exit(1); } if (pid == 0) { int i; for (i = 3; i > 0; i--) { printf("This is the child\n"); } exit(3); } else { int stat_val; waitpid(pid, &stat_val, 0); if (WIFEXITED(stat_val)) printf("Child exited with code %d\n", WEXITSTATUS(stat_val)); else if (WIFSIGNALED(stat_val)) printf("Child terminated abnormally, signal %d\n", WTERMSIG(stat_val)); } return 0; }
子进程的终止信息在一个int中包含了多个字段,用宏定义可以取出其中的每个字段:如果子进程时正常终止的,WIFEXITED取出的字段非零,WEXITSATTUS取出的字段值就是子进程的退出状态;如果子进程是收到信号而异常终止的,WISIGNLED取出的字段值非零,WTERMSIG取出的字段值就是信号的编号。