进程创建
此处讨论 UNIX 系统的进程创建, 通过一对系统调用: fork(), exec(),
以及第三个系统调用 wait() 来等待创建的子进程运行完成.
fork() 系统调用
该系统调用用来创建新进程.
#include <stdio.h>
#include <stdlib.h>
// UNIX standard library:
#include <unistd.h>
// Defines POSIX operating system API
// such as fork(), exec() and getpid()
int main(int argc, char *argv[]) {
printf("Hello from (pid: %d)\n", (int)getpid());
// 子进程从这里开始运行! 亲子都会接受 fork() 的返回值.
int rc = fork();
// 但是子进程返回值为 0, 而父进程接收到的返回值为子进程的 pid.
if (rc < 0) {
fprintf(stderr, "fork failed\n");
exit(1);
} else if (rc == 0) {
printf("Hello from the child (pid: %d)\n", (int)getpid());
} else {
printf("Hello from parent (pid: %d) of (pid: %d)\n", (int)getpid(), rc);
}
return 0;
}
/*
Hello from (pid: 32711)
Hello from parent (pid: 32711) of (pid: 32712)
Hello from the child (pid: 32712)
*/新创建的进程和调用进程几乎一样, 看起来有两个完全一样的程序在运行, 并且都从 fork() 返回.
也就是子进程不会从 main() 开始执行, 而是从 fork() 返回, 好像自己调用了 fork() 一样.
但子进程不是完全拷贝父进程, 他有自己的机器状态, 并且接收到的 fork() 返回值不同, 为 0.
同时, 运行上述程序的输出是不确定的, 父子进程都有可能先被 CPU 调度运行.
这由 CPU 调度程序决定哪个时刻哪个进程被运行, 而我们不能人为假设谁一定先运行.
wait() 系统调用
有时候父进程需要等待子进程运行完成, 这个任务由 wait()(或waitpid()) 完成.
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h> // The library contains `wait()` and `waitpid()`
#include <unistd.h>
int main(int argc, char *argv[]) {
printf("Hello from (pid: %d)\n", (int)getpid());
int rc = fork();
if (rc < 0) {
fprintf(stderr, "fork failed\n");
exit(1);
} else if (rc == 0) {
printf("Hello from child (pid: %d)\n", (int)getpid());
} else {
int wc = wait(NULL); // 父进程等待子进程
printf("Hello from parent (wc: %d) (pid: %d) of child (pid: %d)\n", wc,
(int)getpid(), rc);
}
return 0;
}
/*
Hello from (pid: 7984)
Hello from child (pid: 7985)
Hello from parent (wc: 7985) (pid: 7984) of child (pid: 7985)
*/通过这样的代码, 无论如何, 子进程都会在父进程之前执行完成并输出.
exec() 系统调用
通过这个系统调用, 可以让子进程和父进程执行不同的程序.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>
#include <unistd.h>
int main(int argc, char *argv[]) {
printf("Hello from (pid: %d)\n", (int)getpid());
int rc = fork();
if (rc < 0) {
fprintf(stderr, "fail to fork");
exit(1);
} else if (rc == 0) {
printf("Hello from child (pid: %d)\n", (int)getpid());
char *execarg[3];
execarg[0] = strdup("wc");
execarg[1] = strdup("exec.c");
execarg[2] = NULL;
execvp(execarg[0], execarg);
printf("Shouldn't print out\n");
} else {
int wc = wait(NULL);
printf("Hello from (wc: %d) (pid: %d), the parent of (pid: %d)\n", wc,
getpid(), rc);
}
return 0;
}
/*
Hello from (pid: 67500)
Hello from child (pid: 67501)
27 89 692 exec.c
Hello from (wc: 67501) (pid: 67500), the parent of (pid: 67501)
*/给定可执行程序的名称(或路径, 见后面关于不同的类型的 exec() 的介绍), exec() 会从可执行程序中加载代码与静态数据,
并用他覆写自己的代码段与静态数据, 重新初始化堆栈等内存空间, 然后操作系统就执行改程序, 将参数用 argv 传递给进程.
因此这个系统调用, 没有创建进程而是把当前运行的程序替换为不同的程序. 然后就好像子进程原来的那个程序从来没有运行过.
(因为原来的代码和数据都已经被替换了)所以自然也没有从 exec() 的返回(失败了返回 -1). 也就是一个夺舍的过程啦.
关于
exec()家族虽然我们一直在谈论
exec(), 但事实上, 并没有一个系统调用叫作exec()!
exec 是一系列不同函数签名的同类调用总称, 不同的 exec 虽然本身功能都一样, 但是调用方法有微妙的不同.具体地,
- 所有函数第一个参数都是可行只文件的名字, 但是:
- exec 后面有
p的, 会自动在环境变量PATH中寻找可执行文件, 不需要写绝对路径.- exec 后面有
e的, 允许手动传入环境变量数组(作为第三个变量), 要指明绝对路径.- 两者都无的, 则不能传入环境变量数组, 并且要指明绝对路径.
- 可执行文件的参数在后面传入, 都要注意以
NULL结尾:
- exec 后面有
l的, 意味着参数是一个列表, 也就是依次从第二个参数开始写入argv的内容. 如execlp("ls", "ls", "-la", NULL);- exec 后面有
v的, 意味着参数是一个数组, 也就是传入一个char * args[], 以NULL结尾 如上文的execvp(execarg[0], execarg);
一个有趣的例子: Shell
事实上, 作为一个用户程序, shell 就是通过不断地 fork 和 exec 来执行我们的命令的.
Shell 在文件系统找到我们输入的可执行程序, 然后 fork 一个新进程, 调用某个 exec 执行这个可执行程序,
随后调用 wait 等待子进程执行完成返回, 然后输出下一个提示符, 等待输入.
通过这么一套流程, 可以实现一些有用的功能:
- 重定向: 在完成子进程创建后, 在子进程执行 exec 之前, 先关闭
stdout, 用系统调用open()打开重定向的文件,
由此即将运行的程序输出就被发送给文件, 而不是打印到屏幕. - 管道: 用类似的方法实现, 但是用了
pipe()系统调用, 一个进程的输出被链接到一个内核管道上(队列)上,
另一个进程的输入也被链接到同一个管道上. 因此一个进程的输出就成为了下一个进程的输入.
更多 API
UNIX 还有更多可以有进程交互的方式, 例如 kill() 系统调用, 可以给进程发送信号,
包括: 要求进程睡眠, 终止或其他有用指令.(不能只观其名, 实际上 kill 功能不仅仅是杀死进程)
总之成个信号子系统提供了一套丰富的给进程传递外部事件的途径, 包括接受和执行这些信号.