UNIX系统通过一对系统调用fork()
和exec()
来创建新进程,通过wait()
等待其创建的子进程执行完成。
5.1 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) { // fork failed
fprintf(stderr, "fork failed\n");
exit(1);
} else if (rc == 0) { // child (new process)
printf("hello, I am child (pid:%d)\n", (int) getpid());
} else { // parent goes down this path (main)
printf("hello, I am parent of %d (pid:%d)\n", rc, (int) getpid());
}
return 0;
}
对于操作系统来说,看起来有两个一样的程序在运行,并且都从fork()
调用中返回。子进程不会从main()
函数开始,而是直接从fork()
调用中返回,就好像自己调用了fork()
。
子进程(child)并不是完全拷贝了父进程(parent)。子进程拥有自己的地址空间、寄存器、程序计数器等,但从fork()
的返回值不同。父进程获得的返回值是子进程的PID,子进程获得的是0。
输出不是确定的(deterministic)。单CPU的系统上,两者都有先运行的可能。
CPU调度程序(scheduler)决定了某个时刻哪个进程被执行。
5.2 wait()系统调用
父进程等待子进程执行完毕。也可使用waitpid()
。
#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) { // fork failed; exit
fprintf(stderr, "fork failed\n");
exit(1);
} else if (rc == 0) { // child (new process)
printf("hello, I am child (pid:%d)\n", (int) getpid());
} else { // parent goes down this path (main)
int rc_wait = wait(NULL);
printf("hello, I am parent of %d (rc_wait:%d) (pid:%d)\n", rc, rc_wait, (int) getpid());
}
return 0;
}
增加了wait()
调用后,输出结果变得确定了。即使父进程先运行,会马上调用wait()
,停下来等待子进程执行结束后才返回。附注中提到,有些情况下会在子进程退出前返回,细节见man
。
5.3 最后是exec()系统调用
有多个变体,个人看起来好像只是传参方式不一样而已。让子进程执行和父进程不一样的程序。而fork()
只能运行相同程序。
#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) { // fork failed; exit
fprintf(stderr, "fork failed\n");
exit(1);
} else if (rc == 0) { // child (new process)
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 { // parent goes down this path (main)
int rc_wait = wait(NULL);
printf("hello, I am parent of %d (rc_wait:%d) (pid:%d)\n", rc, rc_wait, (int) getpid());
}
return 0;
}
exec()
会从可执行程序中加载代码和静态数据,覆盖原本的代码段和静态数据,重新初始化堆和栈等内存空间。没有创建新进程,而是直接替换为不同的程序。成功调用不会返回。
5.4 为什么这么设计API
事实证明,分离fork()
和exec()
的做法在构建UNIX shell的时候非常有用,这给了shell在fork之后exec之前运行代码的机会。
做对事(Get it right)。抽象和简化都不能替代做对事。
Lampson – 《Hints for Computer Systems Design》
有许多方式来设计创建进程的API,但fork()
和exec()
的组合既简单又极其强大。
shell找到可执行程序,调用fork()
创建新进程,调用exec()
的某个变体来执行这个可执行程序,调用wait()
等待该命令完成。子进程执行结束后,shell从wait()
中返回并再次输出一个提示符,等待用户输入下一条命令。
重定向则在调用exec()
前先关闭标准输出(standard output),然后打开文件。其实也可以用dup2()
之类的,似乎比较方便。
close(STDOUT_FILENO);
open("./p4.output", O_CREAT|O_WRONLY|O_TRUNC, S_IRWXU);
UNIX管道也是用类似方法实现的,但用的是pipe()
系统调用。一个进程的输出被链接到一个内核管道(pipe)上(队列),另一个进程的输入也被链接到了同一个管道上。前一个进程的输出无缝地作为后一个进程的输入。
5.5 其他API
UNIX中还有其他许多和进程交互的方式,比如通过kill()
系统调用向进程发送信号(signal),包括要求睡眠、终止等。