校验:飞龙
自豪地采用谷歌翻译
进程分叉是一个非常强大(非常危险)的工具。如果你陷入困境并造成一个分叉炸弹(本页后面会有解释),你可能关闭整个系统。为了减少这种情况,可以通过在命令行中键入ulimit -u 40
将最大进程数限制为较小的数量,例如 40。请注意,此限制仅适用于用户,这意味着如果您分叉了炸弹,那么您将无法杀死刚刚创建的所有进程,因为调用killall
需要 shell 来 fork()
...很讽刺对吗?那么我们可以做些什么呢。一种解决方案是在另一个用户(例如 root)之前生成另一个 shell 实例,然后从那里杀死进程。另一种方法是使用内置的exec
命令来杀死所有用户进程(小心你只有一次机会)。最后你可以重启系统:)
在测试fork()
代码时,请确保您对所涉及的计算机具有 root 和/或物理访问权限。如果您必须远程处理fork()
代码,请记住kill -9 -1
将在紧急情况下为您节省时间。
太长不看:如果您没有为此做好准备,那么会非常危险。警告过你了。
系统调用fork
克隆当前进程以创建新进程。它通过复制现有进程的状态创建一个新进程(子进程),但存在一些细微差别(下面讨论)。子进程不是从main
开始的。相反,它就像父进程一样从fork()
返回。
这是一个非常简单的例子......
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 - 但fork()
在printf
之后!为什么?
#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:
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
}
当您尝试创建无限数量的进程时,就是“分叉炸弹”。一个简单的例子如下所示:
while (1) fork();
这通常会使系统几乎停滞不前,因为它试图将 CPU 时间和内存分配给准备运行的大量进程。注释:系统管理员不喜欢分叉炸弹,并且可能对每个用户可以拥有的进程数量设置上限,或者可能撤销登录权限,因为它会对其他用户的程序产生干扰。您还可以使用setrlimit()
限制创建的子进程数。
分叉炸弹不一定是恶意的 - 它们偶尔会因学生编码错误而发生。
Angrave 认为,Matrix 三部曲,机器和人终于共同努力击败不断复制的 Agent-Smith,是基于 AI 驱动的分叉炸弹的电影情节。
使用waitpid
(或wait
)。
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
函数之一。 exec
函数集将进程映像替换为所调用的进程映像。这意味着exec
调用后的任何代码行都被更换。您希望子进程执行的任何其他工作,都应在exec
调用之前完成。
这篇维基百科文章帮助您理解了exec
家族的名字。
命名方案可以像这样缩写:
每个的基础是
exec
(执行),后跟一个或多个字母:
e
- 指向环境变量的指针数组显式传递给新的进程映像。
l
- 命令行参数单独传递(列表)到函数。
p
- 使用PATH
环境变量查找要执行的文件参数中指定的文件。
v
- 命令行参数作为指针的数组(向量)传递给函数。
#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
。以下是如何使用它:
#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
来代替。
一个稍微愚蠢的例子如下所示。它会打印什么?尝试使用程序的多个参数。
#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 。这个糟糕但有趣的排序算法的一个版本如下所示。
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
手册页。
是!实际上,两个进程都使用相同的底层内核文件描述符。例如,如果一个进程将随机访问位置倒回到文件的开头,则两个进程都会受到影响。
子进程和父进程分别应该close
(或fclose
)它们的文件描述符或文件句柄。
阅读手册页!