Skip to content

Latest commit

 

History

History
220 lines (159 loc) · 8.81 KB

16.md

File metadata and controls

220 lines (159 loc) · 8.81 KB

分叉,第 1 部分:简介

原文:Forking, Part 1: Introduction

校验:飞龙

自豪地采用谷歌翻译

一句警告

进程分叉是一个非常强大(非常危险)的工具。如果你陷入困境并造成一个分叉炸弹(本页后面会有解释),你可能关闭整个系统。为了减少这种情况,可以通过在命令行中键入ulimit -u 40将最大进程数限制为较小的数量,例如 40。请注意,此限制仅适用于用户,这意味着如果您分叉了炸弹,那么您将无法杀死刚刚创建的所有进程,因为调用killall需要 shell 来 fork()...很讽刺对吗?那么我们可以做些什么呢。一种解决方案是在另一个用户(例如 root)之前生成另一个 shell 实例,然后从那里杀死进程。另一种方法是使用内置的exec命令来杀死所有用户进程(小心你只有一次机会)。最后你可以重启系统:)

在测试fork()代码时,请确保您对所涉及的计算机具有 root 和/或物理访问权限。如果您必须远程处理fork()代码,请记住kill -9 -1将在紧急情况下为您节省时间。

太长不看:如果您没有为此做好准备,那么会非常危险。警告过你了

分叉介绍

fork做什么?

系统调用fork克隆当前进程以创建新进程。它通过复制现有进程的状态创建一个新进程(子进程),但存在一些细微差别(下面讨论)。子进程不是从main开始的。相反,它就像父进程一样从fork()返回。

什么是最简单的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?

以下程序打印出 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模式的机制,所以我们鼓励你学习和使用forkexecwaitpid来代替。

什么是最愚蠢的分叉示例?

一个稍微愚蠢的例子如下所示。它会打印什么?尝试使用程序的多个参数。

#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)它们的文件描述符或文件句柄。

怎样才能找到更多?

阅读手册页!