0x01 fork
#include <stdio.h>
#include <stdlib.h>
#include <unistd.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());
} else {
printf("hello, I am parent of %d (pid:%d)\n", rc, (int) getpid());
}
return 0;
}
# ./p1
hello world (pid:17)
hello, I am parent of 18 (pid:17)
hello, I am child (pid:18)
从上述代码的执行结果来看:
- 父进程(pid=17)调用了fork()后,返回的是子进程的pid,所以rc=18
- 只打印了一条hello world,说明子进程并不会从main函数开始执行,而是从rc=fork();开始,并且rc=0
小结:调用fork()后,父进程通过fork()函数的返回值可以拿到子进程的pid,子进程直接从fork()系统调用返回,就好像是它自己调用了fork(),并且返回值为0。上述的打印是不固定的,因为父进程和子进程谁先执行完是不确定的。
0x02 wait
#include <stdio.h>
#include <stdlib.h>
#include <unistd.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());
} else {
int wc = wait(NULL);
printf("hello, I am parent of %d (wc:%d) (pid:%d)\n", rc, wc, (int) getpid());
}
return 0;
}
# ./p2
hello world (pid:77)
hello, I am child (pid:78)
hello, I am parent of 78 (wc:78) (pid:77)
父进程调用wait(),延迟自己的执行,直到子进程执行完毕,当子进程结束时,wait()才返回父进程
0x03 exec
#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"); // program: "wc" (word count)
myargs[1] = strdup("p3.c"); // argument: file to count
myargs[2] = NULL; // marks end of array
execvp(myargs[0], myargs); // runs word count
printf("this shouldn't print out");
} else {
int wc = wait(NULL);
printf("hello, I am parent of %d (wc:%d) (pid:%d)\n", rc, wc, (int) getpid());
}
return 0;
}
# ./p3
hello world (pid:94)
hello, I am child (pid:95)
26 107 828 p3.c
hello, I am parent of 95 (wc:95) (pid:94)
子进程调用execvp()来运行字符计数程序wc,它会针对源代码文件p3.c运行wc,输出文件有多少行、多少单词、多少字节
exec()会从可执行程序中加载代码和静态数据,并用它覆写自己的代码段,堆、栈和其他内存空间也会被重新初始化。因此它并没有创建新的进程,而是直接将当前运行的程序(以前的p3)替换为不同的运行程序(wc)。子进程执行exec()之后,几乎就像p3.c从为运行过一样,对exec()的成功调用永远不会返回。
0x04 为什么这样设计API
看了上面3个系统调用,或许我们只能理解wait()的用途,fork()、exec()这两个用于创建进程的API是不是显得比较奇怪?
我们看一下这两个API结合起来可以实现什么:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <sys/wait.h>
int main(int argc, char *argv[]) {
int rc = fork();
if (rc < 0) {
fprintf(stderr, "fork failed\n");
exit(1);
} else if (rc == 0) {
close(STDOUT_FILENO);
open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC, S_IRWXU);
char *myargs[3];
myargs[0] = strdup("wc");
myargs[1] = strdup("p4.c");
myargs[2] = NULL;
execvp(myargs[0], myargs);
} else {
int wc = wait(NULL);
}
return 0;
}
# ./p4
# cat p4.output
26 66 595 p4.c
这段代码实现的功能,就是通过wc计算出p4.c这个文件有多少行、多少单词、多少字节,并且将这些内容写到新的文件p4.output中。
具体步骤如下:
- 父进程fork()出子进程
- 子进程关闭标准输出
- 通过调用execvp()执行wc,计算文件行数、单词数、字节数
- 计算结果输出到文件p4.output中
本质上这段代码实现的功能就等价于 wc p4.c > p4.output
fork()和exec()是非常强大的API,在构建UNIX shell时非常有用。我们甚至在了解他们时觉得很奇怪,其实这些都是经过无数实验得出来的成果。一篇著名论文中也说过:Get it right!抽象和简化都不能替代 Get it right,只要这些API能帮助我们做正确的事,那就有它们存在的理由。