# 分叉,第 2 部分:Fork,Exec,等等
> 原文:<https://github.com/angrave/SystemProgramming/wiki/Forking%2C-Part-2%3A-Fork%2C-Exec%2C-Wait>
## 模式
## 以下'exec'示例有什么作用?
```c
#include <unistd.h>
#include <fcntl.h> // O_CREAT, O_APPEND etc. defined here
int main() {
close(1); // close standard out
open("log.txt", O_RDWR | O_CREAT | O_APPEND, S_IRUSR | S_IWUSR);
puts("Captain's log");
chdir("/usr/include");
// execl( executable, arguments for executable including program name and NULL at the end)
execl("/bin/ls", /* Remaining items sent to ls*/ "/bin/ls", ".", (char *) NULL); // "ls ."
perror("exec failed");
return 0; // Not expected
}
```
上面的代码中没有错误检查(我们假设 close,open,chdir 等按预期工作)。
* open:将使用最低可用文件描述符(即 1);如此标准现在转到日志文件。
* chdir:将当前目录更改为/ usr / include
* execl:用/ bin / ls 替换程序映像并调用其 main()方法
* perror:我们不希望到达这里 - 如果我们这样做,那么 exec 就失败了。
## 微妙的分叉虫
这段代码出了什么问题
```c
#include <unistd.h>
#define HELLO_NUMBER 10
int main(){
pid_t children[HELLO_NUMBER];
int i;
for(i = 0; i < HELLO_NUMBER; i++){
pid_t child = fork();
if(child == -1){
break;
}
if(child == 0){ //I am the child
execlp("ehco", "echo", "hello", NULL);
}
else{
children[i] = child;
}
}
int j;
for(j = 0; j < i; j++){
waitpid(children[j], NULL, 0);
}
return 0;
}
```
我们错误拼写了`ehco`,所以我们不能`exec`它。这是什么意思?我们只是创建了 2 ** 10 个进程,而不是创建 10 个进程,而是对我们的机器进行轰炸。我们怎么能阻止这个?在 exec 之后立即退出,以防 exec 失败,我们不会最终轰炸我们的机器。
## 孩子从父母那里继承了什么?
* 打开文件句柄。如果父母后来寻求回到文件的开头那么这也会影响孩子(反之亦然)。
* 信号处理程序
* 当前的工作目录
* 环境变量
有关详细信息,请参见 [fork 手册页](http://linux.die.net/man/2/fork)。
## 子进程与父进程有什么不同?
进程 ID 不同。在调用`getppid()`的子代中(注意两个'p')将给出与在父代中调用 getpid()相同的结果。有关更多详细信息,请参见 fork 手册页。
## 我该如何等待孩子完成?
使用`waitpid`或`wait`。父进程将暂停,直到`wait`(或`waitpid`)返回。请注意,这个解释掩盖了重新开始的讨论。
## 什么是 fork-exec-wait 模式
常见的编程模式是调用`fork`,然后调用`exec`和`wait`。原始进程调用 fork,它创建一个子进程。然后,子进程使用 exec 开始执行新程序。同时父母使用`wait`(或`waitpid`)等待子进程完成。请参阅下面的完整代码示例。
## 如何启动同时运行的后台进程?
不要等他们!您的父进程可以继续执行代码,而无需等待子进程。注意在实践中,通过在调用 exec 之前调用打开的文件描述符上的`close`,后台进程也可以与父进程和输出流断开连接。
但是,在父完成之前完成的子进程可能会变成僵尸。有关更多信息,请参阅僵尸页面。
## 植物大战僵尸
## 好父母不要让自己的孩子成为僵尸!
当一个孩子完成(或终止)时,它仍占用内核进程表中的一个槽。只有当孩子'等待'时,才能再次使用该插槽。
一个长期运行的程序可以通过不断创建进程来创建许多僵尸,而不会为它们进行`wait`处理。
## 太多僵尸会有什么影响?
最终,内核进程表中没有足够的空间来创建新进程。因此`fork()`会失败并且可能使系统难以/不可能使用 - 例如只需登录就需要新的进程!
## 系统如何帮助预防僵尸?
一旦一个进程完成,它的任何子进程都将被分配给“init” - 第一个进程的 pid 为 1.因此这些孩子会看到 getppid()返回值为 1.这些孤儿最终会完成,并在短时间内成为一个僵尸。幸运的是,init 进程自动等待其所有子进程,从而从系统中删除这些僵尸。
## 我该如何预防僵尸? (警告:简化回答)
等你的孩子!
```c
waitpid(child, &status, 0); // Clean up and wait for my child process to finish.
```
请注意,我们假设获得 SIGCHLD 事件的唯一原因是孩子已经完成(这不完全正确 - 请参阅手册页以获取更多详细信息)。
强大的实现还可以检查中断状态并将上述内容包含在循环中。继续阅读,讨论更强大的实现。
## 我怎样才能异步等待使用 SIGCHLD 的孩子? (高级)
警告:本节使用的信号尚未完全介绍。当子进程完成时,父进程获取信号 SIGCHLD,因此信号处理程序可以等待进程。稍微简化的版本如下所示。
```c
pid_t child;
void cleanup(int signal) {
int status;
waitpid(child, &status, 0);
write(1,"cleanup!\n",9);
}
int main() {
// Register signal handler BEFORE the child can finish
signal(SIGCHLD, cleanup); // or better - sigaction
child = fork();
if (child == -1) { exit(EXIT_FAILURE);}
if (child == 0) { /* I am the child!*/
// Do background stuff e.g. call exec
} else { /* I'm the parent! */
sleep(4); // so we can see the cleanup
puts("Parent is done");
}
return 0;
}
```
然而,上面的例子忽略了几个微妙的要点:
* 不止一个孩子可能已经完成但父母只会获得一个 SIGCHLD 信号(信号没有排队)
* 可以出于其他原因发送 SIGCHLD 信号(例如暂时停止子进程)
收获僵尸的更强大的代码如下所示。
```c
void cleanup(int signal) {
int status;
while (waitpid((pid_t) (-1), 0, WNOHANG) > 0) {}
}
```
## 那么什么是环境变量?
环境变量是系统为所有进程保留的变量。您的系统现在已经设置好了!在 Bash 中,您可以查看其中的一些内容
```
$ echo $HOME
/home/bhuvy
$ echo $PATH
/usr/local/sbin:/usr/bin:...
```
你会如何在 C / C ++中获得这些?您可以使用`getenv`和`setenv`功能
```c
char* home = getenv("HOME"); // Will return /home/bhuvy
setenv("HOME", "/home/bhuvan", 1 /*set overwrite to true*/ );
```
## 是的,那么这些环境变量如何对父母/孩子意味着什么呢?
那么每个进程都会获得自己的环境变量字典,并将其复制到子进程中。这意味着,如果父级更改其环境变量,则不会将其传输给子级,反之亦然。如果你想用不同于父(或任何其他进程)的环境变量执行程序,这在 fork-exec-wait 三部曲中很重要。
例如,您可以编写一个循环遍历所有时区的 C 程序,并执行`date`命令以打印所有本地的日期和时间。环境变量用于各种程序,因此修改它们很重要。
- UIUC CS241 系统编程中文讲义
- 0. 简介
- #Informal 词汇表
- #Piazza:何时以及如何寻求帮助
- 编程技巧,第 1 部分
- 系统编程短篇小说和歌曲
- 1.学习 C
- C 编程,第 1 部分:简介
- C 编程,第 2 部分:文本输入和输出
- C 编程,第 3 部分:常见问题
- C 编程,第 4 部分:字符串和结构
- C 编程,第 5 部分:调试
- C 编程,复习题
- 2.进程
- 进程,第 1 部分:简介
- 分叉,第 1 部分:简介
- 分叉,第 2 部分:Fork,Exec,等等
- 进程控制,第 1 部分:使用信号等待宏
- 进程复习题
- 3.内存和分配器
- 内存,第 1 部分:堆内存简介
- 内存,第 2 部分:实现内存分配器
- 内存,第 3 部分:粉碎堆栈示例
- 内存复习题
- 4.介绍 Pthreads
- Pthreads,第 1 部分:简介
- Pthreads,第 2 部分:实践中的用法
- Pthreads,第 3 部分:并行问题(奖金)
- Pthread 复习题
- 5.同步
- 同步,第 1 部分:互斥锁
- 同步,第 2 部分:计算信号量
- 同步,第 3 部分:使用互斥锁和信号量
- 同步,第 4 部分:临界区问题
- 同步,第 5 部分:条件变量
- 同步,第 6 部分:实现障碍
- 同步,第 7 部分:读者编写器问题
- 同步,第 8 部分:环形缓冲区示例
- 同步复习题
- 6.死锁
- 死锁,第 1 部分:资源分配图
- 死锁,第 2 部分:死锁条件
- 死锁,第 3 部分:餐饮哲学家
- 死锁复习题
- 7.进程间通信&amp;调度
- 虚拟内存,第 1 部分:虚拟内存简介
- 管道,第 1 部分:管道介绍
- 管道,第 2 部分:管道编程秘密
- 文件,第 1 部分:使用文件
- 调度,第 1 部分:调度过程
- 调度,第 2 部分:调度过程:算法
- IPC 复习题
- 8.网络
- POSIX,第 1 部分:错误处理
- 网络,第 1 部分:简介
- 网络,第 2 部分:使用 getaddrinfo
- 网络,第 3 部分:构建一个简单的 TCP 客户端
- 网络,第 4 部分:构建一个简单的 TCP 服务器
- 网络,第 5 部分:关闭端口,重用端口和其他技巧
- 网络,第 6 部分:创建 UDP 服务器
- 网络,第 7 部分:非阻塞 I O,select()和 epoll
- RPC,第 1 部分:远程过程调用简介
- 网络复习题
- 9.文件系统
- 文件系统,第 1 部分:简介
- 文件系统,第 2 部分:文件是 inode(其他一切只是数据...)
- 文件系统,第 3 部分:权限
- 文件系统,第 4 部分:使用目录
- 文件系统,第 5 部分:虚拟文件系统
- 文件系统,第 6 部分:内存映射文件和共享内存
- 文件系统,第 7 部分:可扩展且可靠的文件系统
- 文件系统,第 8 部分:从 Android 设备中删除预装的恶意软件
- 文件系统,第 9 部分:磁盘块示例
- 文件系统复习题
- 10.信号
- 过程控制,第 1 部分:使用信号等待宏
- 信号,第 2 部分:待处理的信号和信号掩码
- 信号,第 3 部分:提高信号
- 信号,第 4 部分:信号
- 信号复习题
- 考试练习题
- 考试主题
- C 编程:复习题
- 多线程编程:复习题
- 同步概念:复习题
- 记忆:复习题
- 管道:复习题
- 文件系统:复习题
- 网络:复习题
- 信号:复习题
- 系统编程笑话