为什么这个程序会打印出“forked!”四次?

76

这个程序为什么会打印出“forked!”四次?

#include <stdio.h>
#include <unistd.h>

int main(void) {

  fork() && (fork() || fork());

  printf("forked!\n");
  return 0;
}
6个回答

210

其中一个来自于 main(),另外三个来自于每个fork()

请注意所有三个forks()都将被执行。您可能想参考ref:

返回值

成功完成后,fork()将向子进程返回0,并向父进程返回子进程的进程ID。两个进程都将继续从fork()函数执行。否则,将向父进程返回-1,不会创建子进程,并设置errno以指示错误。

请注意,进程ID不能为零,如here所述。


那么到底发生了什么?

我们有:

fork() && (fork() || fork());

所以第一个 fork() 将会返回给父进程它的非零进程 id,而返回 0 给子进程。这意味着逻辑表达式的第一个 fork 在父进程中将会被计算为 true,而在子进程中将会被计算为 false,并且由于 短路求值,它不会调用剩下的两个 fork()
所以,现在我们知道至少会有两个打印(一个来自主进程,一个来自第一个 fork())。
接下来,在父进程中执行第二个 fork(),它会执行并向父进程返回一个非零值,在子进程中返回一个零值。
因此,现在父进程不会继续执行最后一个 fork()(由于短路),而子进程将执行最后一个 fork,因为 || 的第一个操作数为 0。
这意味着我们将获得另外两个打印。
因此,总共我们会得到四个打印。

短路运算

在这里,“短路运算”基本上意味着如果 && 的第一个操作数为零,则不评估其他操作数。根据同样的逻辑,如果 || 的一个操作数为1,则不需要评估其他操作数。这是因为其他操作数不能改变逻辑表达式的结果,所以它们不需要执行,从而节省时间。

请参见下面的示例。


进程

请记住,父进程会创建子进程,子进程又会创建其他进程,如此循环,导致进程之间形成了层次结构(可以说是一棵树)。

考虑到这一点,值得一看的是类似问题,以及答案。


图示

我还制作了这张图,希望对您有所帮助。我假设每次调用pid的fork()返回值分别为3、4和5。

fork nodes 请注意,某些fork()上方有一个红色的X,这意味着它们由于逻辑表达式的短路评估而未被执行。

位于顶部的fork()不会被执行,因为运算符&&的第一个操作数为0,因此整个表达式的结果将为0,因此没有必要执行&&的其余操作数。

位于底部的fork()也不会被执行,因为它是||的第二个操作数,其中第一个操作数是非零数字,因此表达式的结果已经计算为true,无论第二个操作数是什么。

接下来的图片显示了进程层次结构: Process hierarchy 基于前面的图示。


短路示例

#include <stdio.h>

int main(void) {

  if(printf("A printf() results in logic true\n"))
    ;//empty body

  if(0 && printf("Short circuiting will not let me execute\n"))
    ;
  else if(0 || printf("I have to be executed\n"))
    ;
  else if(1 || printf("No need for me to get executed\n"))
    ;
  else
  printf("The answer wasn't nonsense after all!\n");

  return 0;
}

输出:

A printf() results in logic true
I have to be executed

86

第一个fork()在调用进程中返回非零值(称其为p0),在子进程中返回0(称其为p1)。

在p1中,短路运算符&&被执行,进程调用printf并终止。在p0中,进程必须评估表达式的其余部分,然后再次调用fork(),从而创建一个新的子进程(p2)。

在p0中,fork()返回一个非零值,短路运算符||被执行,进程调用printf并终止。

在p2中,fork()返回0,因此必须评估||的其余部分,即最后一个fork();这导致为p2创建一个子进程(称其为p3)。

P2然后执行printf并终止。

P3然后执行printf并终止。

然后执行4个printf


3
请问您能否解释一下 "shortcircuit for && is taken" 是什么意思?短路求值是指,如果在进行逻辑与运算时,第一个操作数的结果已经可以确定整个表达式的结果,那么就不再计算第二个操作数,直接返回结果。这种情况被称为 "短路"。 "shortcircuit for && is taken" 的意思是,在执行逻辑与运算时,使用了短路求值的策略。 - Rawan Lezzeik
8
@rona-altico,请查看我回答中关于短路逻辑的链接。它基本上意味着如果“&&”的第一个操作数为零,则其他操作数不会被评估。同样的逻辑,如果“||”的一个操作数为1,则其余操作数无需评估。这是因为其余操作数无法改变逻辑表达式的结果,因此它们不需要被执行,从而节省时间。现在理解得更好了吗?顺便说一句,这是一个很好的问题,我不知道为什么会有人给它投反对票。我给你点个赞。 - gsamaras
3
我很难理解为什么你想要使用 fork(),但却不知道什么是短路。这是一个学校的问题吗? - Sebastian Mach
2
@G.Samaras:我猜它被踩是因为这是众多重复问题之一,并且在在这里提问之前甚至没有尝试谷歌搜索。 - chrk
4
@G.Samaras: 你不明白为什么会有人投反对票吗?这是一个非常糟糕的问题。已经存在一个准确的重复问题,而他却没有解释他期望得到什么不同的输出。为什么这个问题会有41个赞我完全不理解;通常这种情况很快就会降至-3或-4。 - Lightness Races in Orbit
显示剩余3条评论

14
对于所有的投票者,这来自于一个合并但不同的问题。请指责 Stack Overflow。谢谢。

你可以将这个问题分解为三行代码,第一行和最后一行都只是简单地将进程数量加倍。

fork() && fork() || fork();

运算符具有短路特性,因此您将得到以下结果:

       fork()
      /      \
    0/        \>0
 || fork()     && fork()
     /\            /   \
    /  \         0/     \>0
   *    *     || fork()  *
                /   \
               *     *

所以总共有4*5=20个进程,每个进程打印一行。

注意:如果由于某些原因fork()失败(例如,您对进程数量进行了限制),它会返回-1,那么您可能会得到不同的结果。


3
这回答了一个使用fork() && fork() || fork();的问题,而这里的问题是使用fork() && (fork() || fork());。正如在此处讨论的那样进行了合并:“http://meta.stackoverflow.com/questions/281729/my-top-answer-was-deleted-and-my-worst-question-is-still-not-deleted”。您可能需要编辑您的答案,通知未来的读者。 - gsamaras
@gsamaras,为什么它已经合并了。我是提出原始问题的人,它涵盖了C的几个方面(例如逻辑运算符的结合性)。我认为Karols的答案非常完整。如果可能,请撤销合并。 - Amit Singh Tomar
@AmitSinghTomar 因为这是元谈话的结果。我想你在发布之前已经阅读了我的答案,但我没有看到点赞,这是否意味着有什么问题? :/ - gsamaras
@gsamaras,老实说我不担心赞成票或积分。我想要的是正确的事情应该被解决。我相信问题和它们的答案都有各自的优点或者它证明的观点。将它们合并成一个问题看起来有点奇怪,而且会损失信息。 - Amit Singh Tomar
@AmitSinghTomar 显然投票不是重点,正确性才是。我理解你的观点,但这是由大佬们在 Meta 上决定的。 - gsamaras
显示剩余6条评论

9
执行 fork() && (fork() || fork()),会发生什么?
每个 fork 命令会产生 2 个进程,分别是 pid(父进程)和 0(子进程)。
第一次执行 fork:
- 父进程的返回值是非空的 pid => 执行 && (fork() || fork()) - 第二次执行 fork 的父进程返回值非空的 pid,停止执行 || 部分 => 输出 forked - 第二次执行 fork 的子进程返回值为 0,执行 || fork() - 第三次执行 fork 的父进程输出 forked - 第三次执行 fork 的子进程输出 forked - 子进程的返回值是 0,停止执行 && 部分 => 输出 forked 总共:4 个 forked

8

我喜欢已经提交的所有答案。也许如果你在printf语句中添加更多变量,你就可以更容易地看到发生了什么。

#include<stdio.h>
#include<unistd.h>

int main(){

   long child = fork() && (fork() || fork());
   printf("forked! PID=%ld Child=%ld\n", getpid(), child);
   return 0;
}

在我的电脑上,它产生了以下输出:

forked! PID=3694 Child = 0
forked! PID=3696 Child = 0
forked! PID=3693 Child = 1
forked! PID=3695 Child = 1

5
每次调用fork()返回的值是什么?试想一下:long f1,f2,f3; (f1 = fork()) && ((f2 = fork()) || (f3 = fork()));,然后打印出PID和三个单独的值。 - Rob Parker

5

这段代码:

fork();
fork() && fork() || fork();
fork();

它会得到20个进程,并且Printf函数会运行20次。

而对于

fork() && fork() || fork();

printf函数将会运行5次。


2
这回答了使用 fork() && fork() || fork(); 的问题,而这里的问题使用了 fork() && (fork() || fork());。正如在这里讨论的那样,进行了合并:"http://meta.stackoverflow.com/questions/281729/my-top-answer-was-deleted-and-my-worst-question-is-still-not-deleted"。你可能想要编辑你的答案,通知未来的读者。 - gsamaras

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