原文:https://github.com/angrave/SystemProgramming/wiki/Forking%2C-Part-2%3A-Fork%2C-Exec%2C-Wait
#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 手册页。
使用waitpid
或wait
。父进程将暂停,直到wait
(或waitpid
)返回。请注意,这个解释掩盖了重新开始的讨论。
常见的编程模式是调用fork
,然后调用exec
和wait
。原始进程调用 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,因此信号处理程序可以等待进程。稍微简化的版本如下所示。
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 ++中获得这些?您可以使用getenv
和setenv
功能
char* home = getenv("HOME"); // Will return /home/bhuvy
setenv("HOME", "/home/bhuvan", 1 /*set overwrite to true*/ );
那么每个进程都会获得自己的环境变量字典,并将其复制到子进程中。这意味着,如果父级更改其环境变量,则不会将其传输给子级,反之亦然。如果你想用不同于父(或任何其他进程)的环境变量执行程序,这在 fork-exec-wait 三部曲中很重要。
例如,您可以编写一个循环遍历所有时区的 C 程序,并执行date
命令以打印所有本地的日期和时间。环境变量用于各种程序,因此修改它们很重要。