进程控制fork和exec, wait和waitpid

3. 进程控制
3.1. fork函数
#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);

fork 调用失败则返回-1,调用成功的返回值见下面的解释。我们通过一个例子来理解fork 是怎样创

建新进程的。

#include
#include
#include
#include
<sys/types.h>
<unistd.h>
<stdio.h>
<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;
}

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秒,在这1秒期间父进程也很有可能被调度到。所以程序运行的结果基本上是父子进程交
替打印,但这也不是一定的,取决于系统中其它进程的运行情况和内核的调度算法,如果系
统中其它进程非常繁忙则有可能观察到不同的结果。另外,读者也可以把sleep(1); 去掉看程
序的运行结果如何。
8. 这个程序是在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 结构体的引用计数要增加。

exec函数
用fork 创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往
要调用一种exec 函数以执行另一个程序。当进程调用一种exec 函数时,该进程的用户空间代码和数
据完全被新程序替换,从新程序的启动例程开始执行。调用exec 并不创建新进程,所以调用exec 前
后该进程的id并未改变。
其实有六种以exec 开头的函数,统称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 函数只有出错的返回值而没有成功的返回值。
这些函数原型看起来很容易混,但只要掌握了规律就很好记。不带字母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 *const ps_argv[] ={"ps", "-o",
"pid,ppid,pgrp,session,tpgid,comm", NULL};
char *const ps_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节。这些函数之间的关系如下图所示。

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

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 。




  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值