在for循环中,fork()会发生什么可视化的变化。

89
我一直在尝试理解fork()的行为。这次是在一个for循环中。请注意以下代码:
#include <stdio.h>

void main()
{
   int i;

   for (i=0;i<3;i++)
   {
      fork();

      // This printf statement is for debugging purposes
      // getppid(): gets the parent process-id
      // getpid(): get child process-id

      printf("[%d] [%d] i=%d\n", getppid(), getpid(), i);
   }

   printf("[%d] [%d] hi\n", getppid(), getpid());
}

这里是输出:

[6909][6936] i=0
[6909][6936] i=1
[6936][6938] i=1
[6909][6936] i=2
[6909][6936] hi
[6936][6938] i=2
[6936][6938] hi
[6938][6940] i=2
[6938][6940] hi
[1][6937] i=0
[1][6939] i=2
[1][6939] hi
[1][6937] i=1
[6937][6941] i=1
[1][6937] i=2
[1][6937] hi
[6937][6941] i=2
[6937][6941] hi
[6937][6942] i=2
[6937][6942] hi
[1][6943] i=2
[1][6943] hi

我是一个视觉型的人,所以我真正理解事物的唯一途径就是通过绘制图表。我的讲师说会有8个hi语句。我编写并运行了代码,确实有8个hi语句。但我真的不理解它。所以我画了下面这张图表:

enter image description here

图表已更新以反映评论 :)

观察结果:

  1. 父进程(主进程)必须迭代循环3次。然后调用printf
  2. 在每个父for循环的迭代中,都会调用fork()
  3. 在每次fork()调用之后,i被递增,因此每个子进程在递增之前从i开始执行for循环
  4. 在每个for循环结束时,都会打印“hi”

以下是我的问题:

  • 我的图表正确吗?
  • 为什么输出中有两个i=0的实例?
  • fork()后,每个子进程会携带哪个i的值?如果携带相同的i值,那么“分叉”何时停止?
  • 2^n - 1是否总是用来计算被分叉的子进程数量的方法?所以,这里n=3,意味着2^3 - 1 = 8 - 1 = 7个子进程,这是正确的吗?

为什么不运行它并在 fork() 后打印出 i、PID 和父进程的 PID。这样跟踪发生的事情应该很容易。 - Basic
3
@基本操作,那是我所做的第一件事。我甚至使用了getpid()和getppid(),这就是为什么我相信我的图表是正确的原因。但我真的希望有人能验证一下。 - lucidgold
这是一个非常好的图表。你是用dot/graphviz制作的吗? - Steven Lu
2
我曾经使用过Microsoft Visio。但现在我使用的是LibreOffice Draw,它非常类似于Open Office Draw,并且两者都是开源项目,所以是免费的! - lucidgold
1
你是如何默认禁用缓冲的?在gfg ide上,没有使用fflush时输出结果不同 - http://ide.geeksforgeeks.org/0TWiEZ,而使用fflush时输出结果不同 - http://ide.geeksforgeeks.org/0JKaH5。 - Udayraj Deshmukh
3个回答

51

以下是如何理解它,从for循环开始:

  1. 循环在父进程中开始,i == 0

  2. 父进程 fork(),创建子进程1。

  3. 现在你有两个进程。两者都打印 i=0

  4. 循环重新在两个进程中开始,现在 i == 1

  5. 父进程和子进程1 fork(),创建了子进程2和3。

  6. 现在你有四个进程。所有四个进程都打印 i=1

  7. 循环重新在所有四个进程中开始,现在 i == 2

  8. 父进程和子进程1到3都 fork(),创建了子进程4到7。

  9. 现在你有八个进程。所有八个进程都打印 i=2

  10. 循环重新在所有八个进程中开始,现在 i == 3

  11. 循环在所有八个进程中终止,因为 i < 3 不再成立。

  12. 所有八个进程都打印 hi

  13. 所有八个进程终止。

所以你会看到 0 被打印了两次,1 被打印了四次,2 被打印了八次,hi 被打印了八次。


2
太棒了,我的图表是正确的。然而,这就解释了为什么有两个i=0的实例(因为我总是打印当前的i值)。即使i=0被传递,下一个分支只有在i被增加时才会执行!谢谢! - lucidgold
4
是的,fork() 函数只是复制进程,并且它们都继续以相同的方式运行。它们两个刚刚从 fork() 调用返回,并且直到下一次遇到 fork() 调用时才会再次调用。唯一的区别是,子进程中 fork() 返回了0,而父进程中返回了其他值,但就每个进程而言,它们都刚刚从 fork() 返回,然后继续执行。 - Crowman
根据您的解释,它将成为一棵平衡树。不是吗?但执行顺序会有所变化,因为顺序不确定,但本质上是平衡的。是吗? - Naseer
假设这些进程是树的节点,为了论证而论,那么我的问题是:我们知道顺序是不可预测的,但我们确信如果分叉不失败,这些节点迟早会被激活,从而打印出值。因此应该有平衡的树存在,不是吗?@PaulGriffiths - Naseer
1
@khan:不确定如何更详细地解释答案。只需画出来即可。从一个节点开始,然后每次为您可以看到的每个节点添加一个子节点。最终将得到八个节点:一个有三个子节点,一个有两个子节点,两个有一个子节点,四个没有子节点。 - Crowman
显示剩余3条评论

12
  1. 是的,这是正确的(见下文)
  2. 不,i++在调用fork之后执行,因为这是for循环的工作方式。
  3. 如果一切顺利,是的。但请记住,fork可能会失败。

关于第二个问题的解释:

for (i = 0;i < 3; i++)
{
   fork();
}

与之相似:

i = 0;
while (i < 3)
{
    fork();
    i++;
}

因此,在分叉的进程(父进程和子进程)中,i的值是增量之前的值。但是,在fork()之后立即执行增量,所以在我看来,该图可以被视为正确。


那么,由于在调用fork()之后i被递增,那么子进程中的i将是父进程中i的最后一个值?所以我的图表是不正确的? - lucidgold
@2501:我知道,但在视觉上对我来说没有意义。所以如果i=0被传递给每个子进程,那么“分叉”什么时候停止呢? - lucidgold
1
@lucidgold 我认为你的图表是正确的,因为分叉停止是因为i紧接着调用被增加了。Yu Hao解释得很好。 - 2501
1
@lucidgold:因为你在fork()之后才使用了printf()。在父进程和第一个子进程中,i都将是0,它们都会执行该printf()调用。你在图表中缺少的是,第一个红色子进程应该有一个i == 0,但是该框不会进行fork()。第二个红色子进程也是一样,它应该有一个i == 1的框来进行printf(),但是该框也不会进行fork() - Crowman
1
@lucidgold:因为在创建第一个子进程时,第一个fork()已经发生了。第一个子进程将在下一次循环迭代中第一次fork(),当i == 1时。但是它会在执行此操作之前完成循环的第一次迭代(因为它是在该迭代的中间创建的),因此它将printf() i=0 - Crowman
显示剩余4条评论

5

逐个回答你的问题:

我的图表正确吗?

是的,基本上是正确的。这也是一张非常好的图表。

也就是说,如果你将i=0等标签解释为完整的循环迭代,则它是正确的。然而,该图表没有显示,在每个fork()之后,当前循环迭代中fork()调用后的部分也会被复制的子进程执行。

为什么输出中有两个i=0实例?

因为你在fork()后有printf(),所以它会被父进程和刚刚复制的子进程都执行。如果你将printf()移到fork()之前,它只会被父进程执行(因为子进程还不存在)。

fork()后每个子进程携带的i值是多少?如果携带相同的i值,那么“分叉”何时停止?

fork()不会改变i的值,因此子进程看到的值与其父进程相同。

要记住关于fork()的一件事,那就是它只被调用一次,但会返回两次——一次在父进程中,一次在新克隆的子进程中。

对于一个更简单的例子,请考虑以下代码:

printf("This will be printed once.\n");
fork();
printf("This will be printed twice.\n");
fork();
printf("This will be printed four times.\n");
fork();
printf("This will be printed eight times.\n");
fork() 创建的子进程是其父进程(几乎)完全相同的副本,因此从其自身的视角来看,它“记得”成为其父进程,并继承了所有父进程的状态(包括所有变量值,调用堆栈和执行的指令)。除了系统元数据(例如由 getpid() 返回的进程 ID)之外,唯一的立即差异是 fork() 的返回值,在子进程中将为零,在父进程中将为非零值(实际上是子进程的 ID)。

是否总是这样,2^n - 1 将是 fork 出的子进程数?所以,这里 n=3,这意味着有 2^3 - 1 = 8 - 1 = 7 个子进程,这是正确的吗?

每个执行 fork() 的进程都会变成两个进程(除非在异常错误条件下,其中 fork() 可能会失败)。如果父进程和子进程继续执行相同的代码(即它们不检查 fork() 的返回值或自己的进程 ID,并根据其分支到不同的代码路径),那么每个后续的 fork 都会使进程数加倍。所以,是的,在三个 fork 后,你将总共拥有 2³ = 8 个进程。

优秀的回答。我已经根据您的意见更新了我的图表。谢谢! - lucidgold

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接