Linux 进程:fork、execvp 与 wait
这篇文章通过一个简单的 C 程序,理解 Linux 中的进程、子进程,以及程序是怎样运行另一个命令的。
一、老师提供的完整代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>
int main(int argc, char *argv[])
{
printf("hello world (pid:%d)\n", (int)getpid());
int rc = fork();
if (rc < 0) {
fprintf(stderr, "fork failed\n");
exit(1);
} else if (rc == 0) {
printf("hello, I am child (pid:%d)\n", (int)getpid());
char *myargs[3];
myargs[0] = strdup("wc");
myargs[1] = strdup("test1.c");
myargs[2] = NULL;
execvp(myargs[0], myargs);
printf("this shouldn't print out\n");
} else {
int wc = wait(NULL);
printf("hello, I am parent of %d (wc:%d) (pid:%d)\n",
rc, wc, (int)getpid());
}
return 0;
}这里假设当前目录中存在 test1.c。如果文件名不同,需要同时修改代码中的文件名。
二、如何编译和运行
假设代码保存为 test1.c:
gcc -o test1 test1.c
./test1其中:
gcc -o test1 test1.c:把源代码test1.c编译成可执行程序test1。./test1:执行当前目录中的test1程序。test1.c是源代码,不会被./test1直接执行。
程序中的子进程之后会执行:
wc test1.cwc 会统计文件的行数、单词数和字节数。例如:
31 88 750 test1.c这表示文件有 31 行、88 个单词、750 个字节。
三、程序的大致流程
可以把整个过程理解成这样:
运行 ./test1
|
| 打印启动信息
|
| fork()
|
+-- 父进程:等待子进程结束
|
+-- 子进程:调用 execvp()
|
+-- 执行 wc test1.c注意:test1.c 只是被 wc 读取和统计,并没有被再次运行,所以不会出现无限循环。
四、fork():创建子进程
int rc = fork();fork() 会把当前进程复制出一个子进程。调用之后,父进程和子进程都会从 fork() 的下一行继续执行。
可以把它想象成:一个正在运行的程序在某个时刻分成了两个进程。
fork() 的返回值在不同进程中不同:
rc < 0 // 创建失败
rc == 0 // 当前代码运行在子进程中
rc > 0 // 当前代码运行在父进程中,rc 是子进程的 PID因此代码使用 if 分出了三种情况:
if (rc < 0) {
// 创建失败
} else if (rc == 0) {
// 子进程执行这里
} else {
// 父进程执行这里
}五、PID:进程的身份证号
getpid()getpid() 会返回当前进程的 PID,也就是进程号。
例如:
hello world (pid:594)
hello, I am child (pid:595)这里可以看出:父进程和子进程是两个不同的进程,因此 PID 不同。
六、execvp():替换当前程序
子进程中准备了一个参数数组:
char *myargs[3];
myargs[0] = strdup("wc");
myargs[1] = strdup("test1.c");
myargs[2] = NULL;它相当于准备执行这条命令:
wc test1.c然后调用:
execvp(myargs[0], myargs);execvp() 不会创建新的进程。它做的是:把当前子进程中运行的程序替换成另一个程序。
也就是说:
子进程 PID 595:原本运行 test1
|
| execvp()
v
子进程 PID 595:开始运行 wcPID 仍然是 595,只是这个进程执行的程序变成了 wc。
这就是为什么不会无限套娃:wc 只是读取文件内容,并不会重新执行这段代码,也不会再次调用这里的 fork()。
如果 execvp() 执行成功,后面的代码不会执行:
printf("this shouldn't print out\n");只有当 execvp() 执行失败时,程序才会继续执行这条 printf。实际代码中更严谨的写法是:
perror("execvp failed");
exit(1);七、wait():父进程等待子进程
父进程执行:
int child_pid = wait(NULL);这表示父进程暂停在这里,等待某个子进程结束。
子进程执行完 wc test1.c 后,父进程才继续执行后面的输出语句。
wait(NULL) 的返回值是已经结束的子进程 PID,不是 wc 输出的行数或单词数。老师的代码把这个变量命名为 wc,但它实际保存的是子进程 PID;如果自己写代码,改名为 child_pid 会更容易理解。
八、运行结果怎么读
一次运行可能看到类似输出:
hello world (pid:594)
hello, I am child (pid:595)
31 88 750 test1.c
hello, I am parent of 595 (wait:595) (pid:594)它表示:
原程序启动,当前进程 PID 是 594。
fork()创建了子进程,子进程 PID 是 595。子进程通过
execvp()执行wc test1.c。wc输出test1.c的统计结果。父进程通过
wait(NULL)等待子进程结束,然后输出最后一行。
九、常见误解
1. ./test1 会执行 test1.c 吗?
不会。
test1.c -- gcc 编译 --> test1 -- ./test1 执行 --> 程序运行./test1 执行的是编译后生成的可执行文件 test1。
2. 子进程进入 test1.c 后会再次 fork 吗?
不会。子进程没有进入并运行 test1.c,而是把 test1.c 作为普通文本文件交给 wc 读取。
3. 为什么不会无限创建进程?
因为代码中只有一次 fork(),而且子进程随后执行的是 wc。wc 不会运行这段 C 代码,因此不会再次调用 fork()。
4. fork() 和 execvp() 是一回事吗?
不是:
fork() = 创建一个子进程
execvp() = 让当前进程换成另一个程序
wait() = 父进程等待子进程结束十、最值得记住的一句话
fork() 负责创建子进程;
execvp() 负责让子进程执行另一个程序;
wait() 负责让父进程等待子进程结束。这三个函数组合起来,是 Linux 中运行外部命令的一种经典方式。很多 shell 的底层工作方式,也可以用“先 fork,再 exec,最后 wait”来理解。
十一、课后练习
可以尝试做下面几件事,加深理解:
把
test1.c改成另一个存在的文件名,观察wc的输出变化。注释掉
wait(NULL),观察父进程和子进程的输出顺序是否变化。把
wc改成ls,并把参数改成:myargs[0] = strdup("ls"); myargs[1] = strdup("-l"); myargs[2] = NULL;这时子进程会执行:
ls -l把文件名改成一个不存在的文件,观察命令报错时的表现。