企业🤖AI智能体构建引擎,智能编排和调试,一键部署,支持私有化部署方案 广告
# 分叉,第 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`命令以打印所有本地的日期和时间。环境变量用于各种程序,因此修改它们很重要。