0%

实验二-fork、wait、exec

fork

fork()被用于创建一个子进程。这个函数返回0代表创建成功,返回负数代表创建失败。

举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <stdlib.h>

int main(){
int apple = 5; //5个苹果
pid_t pid;

pid = fork(); //创建子进程
if(pid < 0){ //如果没有创建成功
printf("Error: creating child process");
exit(EXIT_FAILURE); //退出
}

if(pid == 0){// 如果创建成功
//在这个if中的所有操作都是子进程的。
apple -= 4; //吃掉4个苹果
}else if(pid >= 0){ // 如果不在子进程中
printf("There are %d apples", apple); //输出苹果的数量
}
}

注:在 C 语言中,pid_t 是一种数据类型,它用来表示进程 ID(Process ID),在头文件 <sys/types.h> 中定义。pid_t 不是 int尽管在许多系统上 pid_t 被定义为 int但这并不是必然的,因为它可以是不同平台上不同的类型。

当我们创建一个子进程的时候,这段代码,包括它当前的所有参数都被复制给了子进程。这样一来,子进程的操作无论怎么更改都是在子进程里生效。见下图

输出:

1
There are 5 apples

这些参数里面,唯一不同的是pid,子进程的pid是0,而父进程的pid没有赋值。我们不能说,子进程的作用域只在那个if语句中,在if外面就不是子进程了。其实不然,我们可以试一下下面的代码来证明子进程和父进程都拥有苹果,而且互不干扰。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>

int main(){
int apple = 5; //5个苹果
pid_t pid;

pid = fork(); //创建子进程
if(pid < 0){ //如果没有创建成功
printf("Error: creating child process");
exit(EXIT_FAILURE); //退出
}

if(pid == 0){// 如果创建成功
//在这个if中的所有操作都是子进程的。
apple -= 4; //吃掉4个苹果
}
printf("There are %d apples\n", apple); //输出苹果的数量
}

输出

1
2
There are 5 apples
There are 1 apples

前一个是父进程的苹果,没有被吃掉,后一个是子进程的苹果,吃掉了四个。即使是在if语句外面也是可以被执行的。if仅仅只是用于辅助判断哪些代码可以在子进程执行,哪些代码可以在父进程执行而已

fork的进一步了解

wait

搭配fork()使用。本质上就是为了回收已经执行完毕的子进程,释放子进程的资源

  1. 没有用wait()主进程不会受到影响,但是子进程即使终止了资源也不会被回收,从而会变成僵尸进程

  2. 进程一旦调用了 wait,就 立即阻塞自己,由wait自动分析是否当前进程的某个子进程已经退出,如果让它找到了这样一个已经变成僵尸的子进程,wait 就会收集这个子进程的信息, 并把它彻底销毁后返回;如果没有找到这样一个子进程,wait就会一直阻塞在这里,直到有一个出现为止。

  3. wait()的返回值是子进程的ID。

如果你对第一次写 hello world 有印象,你一定记得代码最后的return 0;。课本上只会跟你说:“return 0代表这个函数的返回值是0”,但是你无法理解为什么要返回0,返回给谁。现在明白了父进程和子进程的概念,你就知道这个0是返回给父进程的, 是告诉父进程子进程的退出状态。

要接住这个返回值,我们要定义一个整型status。后面通过这个变量来确定子进程退出的状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>

int main(){
pid_t pid;
pid = fork();
//输入的是
if(pid < 0){
printf("Error: creating child process");
}

if(pid == 0){
return 0; //尝试换掉这个返回值看看输出的结果。
}else{
int status;
int id = wait(&status);
printf("%d\n", status);
printf("Child process return status is %d\n", WEXITSTATUS(status));
printf("Child process id is %d", id);
}
}

输出

1
2
3
0
Child process return status is 0
Child process id is 99932

WEXITSTATUS是预设的函数,用来查看函数的返回值的。还有很多其他的函数可供使用, 详情点这里

这里有一个很大的问题,一定不要弄混淆了

  1. wait()的返回值不是子进程的返回值, 而是子进程的id。

  2. status不是子进程的返回值,而是进程的状态。所以子进程无论return什么,只要正常返回,status都是0

另外需要说明的是wait()函数并不仅仅是在等到子进程结束,而是在子进程状态转移的时候返回

exec

exec()是一系列函数的总称,主要的作用是执行某个文件。它们分别是execl(), execlp(), execle(), execv(), execvp(), execvpe()。我后面会补充一期文件系统的复习。现在只要假设exec()可以用来跑一个程序就好。

这么多函数,我应该用哪一个?别慌。以exec作为前缀,我们可以这样确定要用哪一个函数:

  • l代表输入的是参数,而v代表输入的是数组。
  • e表示需要设置环境,不带e表示不需要。
  • p表示输入的是可执行文件的名字,不带表示输入的是可执行文件的路径(函数会根据$PATH的路径来判断可执行文件的位置)。 下面这段代码介绍了execlp()execvp()的区别。 程序会输出两段一样的结果,分别是子进程和父进程的运行结果。不同的是,父进程用execvp(), 根据上面的判断这是一个接受参数数组和可执行文件名的函数。你可以试着自己换不同的函数来尝试他们的效果。
  • NULL在函数中的作用是告诉函数参数到此为止。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>

int main(){
char args1[] = "ls";
char args2[] = "-la";

char* command[] = {args1, args2, NULL};
pid_t pid1;

pid1 = fork();
if(pid1 < 0){
printf("Error: creating child process");
}

if(pid1 == 0){
//execlp 接收n个参数,其中第一个是可执行文件名,后面的都是可执行参数。
//最后一个参数必须为NULL。
execlp(args1, args1, args2, NULL);
}else{
wait(NULL);
//execvp 接收两个参数,一个是可执行文件名,一个是执行参数的数组。
//数组最后的元素必须为NULL。
execvp(args1, command);
}
}

总结一下:

  1. 要创建子进程我们要用fork(),这个函数的返回值告诉我们子进程是否创建成功。
  2. 要在子进程中运行可执行文件要用exec(),这个函数有多个变种,用哪一个根据手头有的变量确定。
  3. wait()被父进程用于等待子进程的状态。