Skip to content

Files

Latest commit

5b18633 · May 10, 2020

History

History
187 lines (130 loc) · 6.95 KB

17.md

File metadata and controls

187 lines (130 loc) · 6.95 KB

分叉,第 2 部分:Fork,Exec,等等

原文:https://github.com/angrave/SystemProgramming/wiki/Forking%2C-Part-2%3A-Fork%2C-Exec%2C-Wait

模式

以下'exec'示例有什么作用?

#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 就失败了。

微妙的分叉虫

这段代码出了什么问题

#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 手册页

子进程与父进程有什么不同?

进程 ID 不同。在调用getppid()的子代中(注意两个'p')将给出与在父代中调用 getpid()相同的结果。有关更多详细信息,请参见 fork 手册页。

我该如何等待孩子完成?

使用waitpidwait。父进程将暂停,直到wait(或waitpid)返回。请注意,这个解释掩盖了重新开始的讨论。

什么是 fork-exec-wait 模式

常见的编程模式是调用fork,然后调用execwait。原始进程调用 fork,它创建一个子进程。然后,子进程使用 exec 开始执行新程序。同时父母使用wait(或waitpid)等待子进程完成。请参阅下面的完整代码示例。

如何启动同时运行的后台进程?

不要等他们!您的父进程可以继续执行代码,而无需等待子进程。注意在实践中,通过在调用 exec 之前调用打开的文件描述符上的close,后台进程也可以与父进程和输出流断开连接。

但是,在父完成之前完成的子进程可能会变成僵尸。有关更多信息,请参阅僵尸页面。

植物大战僵尸

好父母不要让自己的孩子成为僵尸!

当一个孩子完成(或终止)时,它仍占用内核进程表中的一个槽。只有当孩子'等待'时,才能再次使用该插槽。

一个长期运行的程序可以通过不断创建进程来创建许多僵尸,而不会为它们进行wait处理。

太多僵尸会有什么影响?

最终,内核进程表中没有足够的空间来创建新进程。因此fork()会失败并且可能使系统难以/不可能使用 - 例如只需登录就需要新的进程!

系统如何帮助预防僵尸?

一旦一个进程完成,它的任何子进程都将被分配给“init” - 第一个进程的 pid 为 1.因此这些孩子会看到 getppid()返回值为 1.这些孤儿最终会完成,并在短时间内成为一个僵尸。幸运的是,init 进程自动等待其所有子进程,从而从系统中删除这些僵尸。

我该如何预防僵尸? (警告:简化回答)

等你的孩子!

waitpid(child, &status, 0); // Clean up and wait for my child process to finish.

请注意,我们假设获得 SIGCHLD 事件的唯一原因是孩子已经完成(这不完全正确 - 请参阅手册页以获取更多详细信息)。

强大的实现还可以检查中断状态并将上述内容包含在循环中。继续阅读,讨论更强大的实现。

我怎样才能异步等待使用 SIGCHLD 的孩子? (高级)

警告:本节使用的信号尚未完全介绍。当子进程完成时,父进程获取信号 SIGCHLD,因此信号处理程序可以等待进程。稍微简化的版本如下所示。

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 信号(例如暂时停止子进程)

收获僵尸的更强大的代码如下所示。

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 ++中获得这些?您可以使用getenvsetenv功能

char* home = getenv("HOME"); // Will return /home/bhuvy
setenv("HOME", "/home/bhuvan", 1 /*set overwrite to true*/ );

是的,那么这些环境变量如何对父母/孩子意味着什么呢?

那么每个进程都会获得自己的环境变量字典,并将其复制到子进程中。这意味着,如果父级更改其环境变量,则不会将其传输给子级,反之亦然。如果你想用不同于父(或任何其他进程)的环境变量执行程序,这在 fork-exec-wait 三部曲中很重要。

例如,您可以编写一个循环遍历所有时区的 C 程序,并执行date命令以打印所有本地的日期和时间。环境变量用于各种程序,因此修改它们很重要。