企业🤖AI智能体构建引擎,智能编排和调试,一键部署,支持私有化部署方案 广告
# 分叉,第 1 部分:简介 > 原文:[Forking, Part 1: Introduction](https://github.com/angrave/SystemProgramming/wiki/Forking%2C-Part-1%3A-Introduction) > 校验:[飞龙](https://github.com/wizardforcel) > 自豪地采用[谷歌翻译](https://translate.google.cn/) ## 一句警告 进程分叉是一个非常强大(非常危险)的工具。如果你陷入困境并造成一个分叉炸弹(本页后面会有解释),**你可能关闭整个系统**。为了减少这种情况,可以通过在命令行中键入`ulimit -u 40`将最大进程数限制为较小的数量,例如 40。请注意,此限制仅适用于用户,这意味着如果您分叉了炸弹,那么您将无法杀死刚刚创建的所有进程,因为调用`killall`需要 shell 来 `fork()`...很讽刺对吗?那么我们可以做些什么呢。一种解决方案是在另一个用户(例如 root)之前生成另一个 shell 实例,然后从那里杀死进程。另一种方法是使用内置的`exec`命令来杀死所有用户进程(小心你只有一次机会)。最后你可以重启系统:) 在测试`fork()`代码时,请确保您对所涉及的计算机具有 root 和/或物理访问权限。如果您必须远程处理`fork()`代码,请记住`kill -9 -1`将在紧急情况下为您节省时间。 太长不看:如果您没有为此做好准备,那么会**非常**危险。**警告过你了**。 ## 分叉介绍 ## `fork`做什么? 系统调用`fork`克隆当前进程以创建新进程。它通过复制现有进程的状态创建一个新进程(子进程),但存在一些细微差别(下面讨论)。子进程不是从`main`开始的。相反,它就像父进程一样从`fork()`返回。 ## 什么是最简单的`fork()`示例? 这是一个非常简单的例子...... ```c printf("I'm printed once!\n"); fork(); // Now there are two processes running // and each process will print out the next line. printf("You see this line twice!\n"); ``` ## 为什么这个例子打印 42? 以下程序打印出 42 - 但`fork()`在`printf`之后!为什么? ```c #include <unistd.h> /*fork declared here*/ #include <stdio.h> /* printf declared here*/ int main() { int answer = 84 >> 1; printf("Answer: %d", answer); fork(); return 0; } ``` `printf`行仅执行一次,但是注意到打印内容没有刷新到标准输出(没有打印换行符,我们没有调用`fflush`或更改缓冲模式)。因此,输出文本仍在进程内存中等待发送。执行`fork()`时,将复制整个进程内存,包括缓冲区。因此,子进程以非空输出缓冲区开始,该缓冲区将在程序退出时刷新。 ## 如何编写父子进程不同的代码? 检查`fork()`的返回值。返回值`-1` = 失败; `0` = 在子进程中;正数 = 在父进程中(返回值是子进程 id)。这是一种记住哪种情况的方法: 子进程可以通过调用`getppid()`找到其父进程 - 被复制的原始进程 - 因此不需要来自`fork()`的任何其他返回信息。然而,父进程只能从`fork`的返回值中找出新子进程的 id: ```c pid_t id = fork(); if (id == -1) exit(1); // fork failed if (id > 0) { // I'm the original parent and // I just created a child process with id 'id' // Use waitpid to wait for the child to finish } else { // returned zero // I must be the newly made child process } ``` ## 什么是分叉炸弹? 当您尝试创建无限数量的进程时,就是“分叉炸弹”。一个简单的例子如下所示: ```c while (1) fork(); ``` 这通常会使系统几乎停滞不前,因为它试图将 CPU 时间和内存分配给准备运行的大量进程。注释:系统管理员不喜欢分叉炸弹,并且可能对每个用户可以拥有的进程数量设置上限,或者可能撤销登录权限,因为它会对其他用户的程序产生干扰。您还可以使用`setrlimit()`限制创建的子进程数。 分叉炸弹不一定是恶意的 - 它们偶尔会因学生编码错误而发生。 Angrave 认为,Matrix 三部曲,机器和人终于共同努力击败不断复制的 Agent-Smith,是基于 AI 驱动的分叉炸弹的电影情节。 ## 等待和执行 ## 父进程如何等待子进程完成? 使用`waitpid`(或`wait`)。 ```c pid_t child_id = fork(); if (child_id == -1) { perror("fork"); exit(EXIT_FAILURE);} if (child_id > 0) { // We have a child! Get their exit code int status; waitpid( child_id, &status, 0 ); // code not shown to get exit status from child } else { // In child ... // start calculation exit(123); } ``` ## 我可以让子进程执行另一个程序吗? 是。在分叉后使用[`exec`](http://man7.org/linux/man-pages/man3/exec.3.html)函数之一。 `exec`函数集将进程映像替换为所调用的进程映像。这意味着`exec`调用后的任何代码行都被更换。您希望子进程执行的任何其他工作,都应在`exec`调用之前完成。 [这篇维基百科文章](https://en.wikipedia.org/wiki/Exec_(system_call)#C_language_prototypes)帮助您理解了`exec`家族的名字。 命名方案可以像这样缩写: > 每个的基础是`exec`(执行),后跟一个或多个字母: > > `e` - 指向环境变量的指针数组显式传递给新的进程映像。 > > `l` - 命令行参数单独传递(列表)到函数。 > > `p` - 使用`PATH`环境变量查找要执行的文件参数中指定的文件。 > > `v` - 命令行参数作为指针的数组(向量)传递给函数。 ```c #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> #include <stdlib.h> #include <stdio.h> int main(int argc, char**argv) { pid_t child = fork(); if (child == -1) return EXIT_FAILURE; if (child) { /* I have a child! */ int status; waitpid(child , &status ,0); return EXIT_SUCCESS; } else { /* I am the child */ // Other versions of exec pass in arguments as arrays // Remember first arg is the program name // Last arg must be a char pointer to NULL execl("/bin/ls", "ls","-alh", (char *) NULL); // If we get to this line, something went wrong! perror("exec failed!"); } } ``` ## 执行另一个程序的更简单方法 使用`system`。以下是如何使用它: ```c #include <unistd.h> #include <stdlib.h> int main(int argc, char**argv) { system("ls"); return 0; } ``` `system`调用将分叉,执行参数传递的命令,原始父进程将等待此操作完成。这也意味着`system`是一个阻塞调用:在`system`创建的进程退出之前,父进程无法继续。这可能有用也可能没用。此外,`system`实际上创建了一个 shell,然后给出了字符串,这比直接使用`exec`开销更大。标准 shell 将使用`PATH`环境变量来搜索与命令匹配的文件名。对于许多简单的执行某个命令的问题,使用系统通常就足够了,但很快就会被更复杂或微妙的问题限制,它隐藏了`fork-exec-wait`模式的机制,所以我们鼓励你学习和使用`fork`,`exec`和`waitpid`来代替。 ## 什么是最愚蠢的分叉示例? 一个稍微愚蠢的例子如下所示。它会打印什么?尝试使用程序的多个参数。 ```c #include <unistd.h> #include <stdio.h> int main(int argc, char **argv) { pid_t id; int status; while (--argc && (id=fork())) { waitpid(id,&status,0); /* Wait for child*/ } printf("%d:%s\n", argc, argv[argc]); return 0; } ``` 惊人的并行表观 - O(N) 睡眠排序是今天愚蠢的赢家。首次发表于 [4chan 2011](https://dis.4chan.org/read/prog/1295544154) 。这个糟糕但有趣的排序算法的一个版本如下所示。 ```c int main(int c, char **v) { while (--c > 1 && !fork()); int val = atoi(v[c]); sleep(val); printf("%d\n", val); return 0; } ``` 注意:由于系统调度程序的工作原理,算法实际上不是`O(N)`。虽然每个进程都有以`O(log(N))`运行的并行算法,但遗憾的是这不是其中之一。 ## 子进程与父进程有什么不同? 主要区别包括: * `getpid()`返回的进程 ID。 `getppid()`返回的父进程 ID。 * 当子进程完成时,通过信号 SIGCHLD 通知父进程,反之则不然。 * 子进程不会继承待定信号或计时器警报。有关完整列表,请参见[`fork`手册页](http://man7.org/linux/man-pages/man2/fork.2.html)。 ## 子进程是否共享打开的文件句柄? 是!实际上,两个进程都使用相同的底层内核文件描述符。例如,如果一个进程将随机访问位置倒回到文件的开头,则两个进程都会受到影响。 子进程和父进程分别应该`close`(或`fclose`)它们的文件描述符或文件句柄。 ## 怎样才能找到更多? 阅读手册页! * [`fork`](http://man7.org/linux/man-pages/man2/fork.2.html) * [`exec`](http://man7.org/linux/man-pages/man3/exec.3.html) * [`wait`](http://man7.org/linux/man-pages/man2/wait.2.html)