# 分叉，第 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`命令以打印所有本地的日期和时间。环境变量用于各种程序，因此修改它们很重要。