if((pid1=fork())<0)和if(pid1=fork()<0)有啥区别么?!因为在Linux下这两个条件运行出的结果完全不同

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了if((pid1=fork())<0)和if(pid1=fork()<0)有啥区别么?!因为在Linux下这两个条件运行出的结果完全不同相关的知识,希望对你有一定的参考价值。

C语言中的运算符是有优先级的。
赋值运算符“=”的级别最低。

fork()函数的返回值有三种情形:
负数:如果出错,则fork()返回-1,此时没有创建新的进程。最初的进程仍然运行。
零:在子进程中,fork()返回0
正数:在负进程中,fork()返回正的子进程的PID

if((pid1=fork())<0)的意思是:执行fork()创建进程,并把返回值赋给pid1;
(1)如果fork()返回负数,也就是创建进程失败,则表达式((pid1=fork())<0)为真,pid = -1,从而会执行后面的语句;
(2)如果返回值为正数或者0,也就是进程创建成功,pid1 >= 0,则不执行后边的语句。

if(pid1=fork()<0)的意思是:执行fork()创建进程,把逻辑表达式fork()<0的值赋给pid1,
(1)如果它的返回值为负数,也就是创建进程失败,则表达式(pid1=fork()<0)为真,pid1 = 1,从而会执行后面的语句;(在if语句中,应该不会直接用pid1作处理的,但else语句中,一般会对pid1进行判断,子进程和父进程分别进行不同的操作,但pid1已经不表示子进程号,从而出错。)
(2)如果fork()返回值为正数或者0,那么表达式为假,pid1 = 0,从而不会执行后边的语句。(此时,情况同上面讲的else分支的情况,即从逻辑上看认为已经创建子进程成功了,而pid1却不是表示子进程号,而是0,从而代码出现错误)

从上面的分析可以看出:
(1)fork()执行成功,则无论是子进程还是父进程都会执行If后面的语句;如果fork()执行失败,则原有进程都不会执行if后面的语句。所以,从逻辑分支的选择上看,二者是等效的。
(2)但pid1 的赋值不同。如果if后面的语句或者else后面的语句中,涉及到对pid1的处理,那么,处理结果大相径庭,就不难理解了啊。
参考技术A 等于号的优先级比小于号的要低。先执行<,再执行=. 参考技术B 运算符有一个优先顺序。。=与<。。所以运算的结果也就不一样了。 参考技术C 运算符有一个优先顺序。。=与<。。所以运算的结果也就不一样了。 参考技术D 括号影响了,运算的优先级。所以结果不同。追问

能解释得具体一点么,谢谢~
是不是区别在于,有括号,先执行了创建子进程,然后子进程把这句判断也复制过去了?
还是。。。。。

追答

对,很明显。先执行括号内的。

execv() 和 fork() 的时间浪费

【中文标题】execv() 和 fork() 的时间浪费【英文标题】:Time waste of execv() and fork() 【发布时间】:2017-02-14 02:33:52 【问题描述】:

我目前正在学习fork()execv(),我对组合的效率有疑问。

我看到了以下标准代码:

pid = fork();
if(pid < 0)
    //handle fork error

else if (pid == 0)
    execv("son_prog", argv_son);
//do father code

我知道fork() 克隆了整个进程(复制整个堆等)并且execv() 用新程序的地址空间替换了当前地址空间。考虑到这一点,使用这种组合是否会变得非常低效?我们正在复制一个进程的整个地址空间,然后立即覆盖它。

所以我的问题: 使用这种组合(而不是其他一些解决方案)实现的优势是什么,使人们仍然使用它,即使我们有浪费?

【问题讨论】:

很久以前,系统实际上一次复制了所有内容。我建议你了解paging、virtual memory和copy on write。 【参考方案1】:

它并没有那么昂贵(相对于直接生成一个进程),尤其是像您在 Linux 中找到的写时复制 forks ,它对于以下方面来说是一种优雅:

    当您真的只想分叉当前进程的克隆时(我发现这对测试非常有用) 当你需要在加载新的可执行文件之前做一些事情时 (重定向文件描述符、使用信号掩码/处置、uid 等)

POSIX 现在有 posix_spawn,它可以有效地让你组合 fork/and-exec(可能比 fork+exec 更有效;如果它更有效,它通常会通过一些更便宜但更少的方式来实现强大的 fork (clone/vfork) 后跟 exec),但它实现 #2 的方式是通过大量相对混乱的选项,这些选项永远不会像允许您运行任意代码那样完整、强大和干净就在加载新进程映像之前。

【讨论】:

【参考方案2】:

事实证明,当进程有几 GB 的可写 RAM 时,所有这些 COW 页面错误一点都不便宜。即使孩子早就打电话给exec(),他们也会犯一次错误。因为fork() 的子级不再被允许分配内存,即使是单线程的情况(你可以感谢苹果),现在安排调用vfork()/exec() 几乎没有什么困难。

vfork()/exec() 模型的真正优势是您可以使用任意当前目录、任意环境变量和任意 fs 句柄(不仅仅是 stdin/stdout/stderr)、任意信号掩码和任意共享内存(使用共享内存系统调用)而没有二十个参数的CreateProcess() API,每隔几年就会获得更多参数。

事实证明,由于/proc,线程处理早期的“哎呀,我泄露了句柄正在被另一个线程打开”的失态在用户空间中无需进程范围的锁定是可以修复的。如果没有新的操作系统版本并说服所有人调用新的 API,那么在巨大的 CreateProcess() 模型中也是如此。

所以你有它。设计事故的结果远比直接设计的解决方案好。

【讨论】:

“因为 fork() 的子级不再允许分配内存,即使是单线程情况”是什么意思?出于好奇,为什么会这样?这与 Apple 有何关系? @JordanMelo:Apple 开始在 libc 中生成调用 malloc() 的后台线程。 malloc() 需要一个锁,所以如果你在孩子中调用 fork() 你可能会死锁,因为没有人可以释放孩子的锁。在 Apple 进行更改之前,您必须使用多线程才能遇到此问题。【参考方案3】:

不再。有一种叫做COW(写时复制)的东西,只有当两个进程(父/子)之一尝试写入共享数据时,它才会被复制。

过去:fork() 系统调用复制了调用进程(父进程)的地址空间来创建一个新进程(子进程)。 将父级地址空间复制到子级是fork() 操作中最昂贵的部分。

现在: 在子进程中调用fork() 之后通常几乎立即调用exec(),它用新程序替换子进程的内存。例如,这就是 shell 通常做的事情。在这种情况下,复制父地址空间所花费的时间在很大程度上被浪费了,因为子进程在调用exec()之前将使用它的内存非常少。

出于这个原因,更高版本的 Unix 利用虚拟内存硬件允许父子进程共享映射到各自地址空间的内存,直到其中一个进程实际修改它。这种技术被称为copy-on-write。为此,在fork() 上,内核会将地址空间映射从父级复制到子级,而不是映射页面的内容,同时将现在共享的页面标记为只读。当两个进程之一尝试写入这些共享页面之一时,该进程会发生页面错误。此时,Unix 内核意识到该页面实际上是一个“虚拟”或“写时复制”副本,因此它为出错的进程创建了一个新的、私有的、可写的页面副本。通过这种方式,各个页面的内容在实际写入之前不会被实际复制。这种优化使子级中的fork() 后跟exec() 更便宜:子级在调用exec() 之前可能只需要复制一页(其堆栈的当前页)。

【讨论】:

有趣 - 我会假设写入 fork 的返回值的调用被忽略(因为这总是会在分叉后立即发生并且会破坏目的)? @DeanLeitersdorf 一个进程在内存中不只有一页。它还有更多功能,虽然您可以复制包含存储在 fork 的返回值中的变量的页面,但您可以通过让子进程首先运行来节省许多不需要的复制操作,这样如果它调用 execv,您就可以保存所有这些副本操作。 @Dean:不,什么都没有被忽略——但是来自fork() 的返回值可能只涉及一个页面(如果有的话;取决于 ABI,它可能永远不会离开寄存器)。与 Emacs 等长寿命进程的地址空间相比,这太小了。 是的,它要么是堆栈的一页(一旦孩子开始做任何事情就会立即取消共享)或者什么都没有,因为返回值只是在 eax 或其他 :)【参考方案4】:

另一个答案是:

然而,在过去的糟糕日子里,fork(2) 需要制作调用者数据空间的完整副本,这通常是不必要的,因为通常紧接着 exec(3) 就完成了。

显然,一个人过去的糟糕时光比其他人记忆中的要年轻得多。

最初的 UNIX 系统没有运行多个进程的内存,也没有 MMU 来将多个进程保存在物理内存中,以便在相同的逻辑地址空间运行:它们将进程交换到磁盘上当前没有运行。

fork 系统调用与将当前进程换出到磁盘几乎完全相同,除了返回值和 通过换入另一个进程来替换剩余的内存中副本。由于无论如何您都必须换掉父进程才能运行子进程,因此 fork+exec 不会产生任何开销。

确实有一段时间 fork+exec 很尴尬:当有 MMU 提供逻辑地址空间和物理地址空间之间的映射但页面错误没有保留足够的信息时,写时复制和数字其他虚拟内存/按需分页方案是可行的。

这种情况已经够痛苦了,不仅对 UNIX 而言,硬件的页面错误处理很快就变得“可重放”了。

【讨论】:

这个答案中的信息确实没有得到它应该得到的赞赏。当然,它几乎更适合作为评论或对另一个答案的补充,或者编辑为完整答案,复制来自其他答案参考的一些信息 - 因为在当前形式下,这个答案更像是一个澄清/附录的其他答案。尽管如此,我还是 +1:我真的很高兴了解到这一点历史信息。【参考方案5】:

使用这种组合(而不是其他一些解决方案)实现的优势是什么,即使我们有浪费,人们仍然使用它?

您必须以某种方式创建一个新流程。用户空间程序实现这一点的方法很少。 POSIX 曾经有vfork() alognside fork(),一些系统可能有自己的机制,比如Linux 特有的clone(),但是从2008 年开始,POSIX 只指定了fork()posix_spawn() 系列。 fork + exec 路线更传统,很好理解,并且几乎没有缺点(见下文)。 posix_spawn 系列被设计为一个特殊用途替代品,用于对fork() 造成困难的上下文中使用;您可以在its specification 的“基本原理”部分找到详细信息。

vfork() 的 Linux 手册页摘录可能很有启发性:

在 Linux 下,fork(2) 是使用写时复制页实现的,因此fork(2) 所招致的唯一损失是复制父页表所需的时间和内存,并为孩子创建一个独特的任务结构。然而,在过去的糟糕日子里,fork(2) 需要制作调用者数据空间的完整副本,这通常是不必要的,因为通常在之后立即完成 exec(3)。因此,为了提高效率,BSD 引入了vfork() 系统调用,它并没有完全复制父进程的地址空间,而是借用了父进程的内存和控制线程,直到调用execve(2) 或发生了退出。父进程在子进程使用其资源时被挂起。 vfork() 的使用很棘手:例如,不修改父进程中的数据取决于知道哪些变量保存在寄存器中。

(已添加重点)

因此,您对浪费的担忧对于现代系统(不仅限于 Linux)来说是没有根据的,但它确实是历史上的一个问题,并且确实有一些机制可以避免它。如今,这些机制中的大多数都已过时。

【讨论】:

POSIX 现在也有posix_spawn 谢谢,@hvd,我显然忽略了 posix_spawn。我已经更新了答案来解决这个问题。 使用 CLONE_VM 克隆后执行 exec 不是更高效吗? @immibis:是的,但这不是 vfork() 吗? @immibis, clone() 是特定于 Linux 的。此外,由于CLONE_VM 标志会导致父子进程实际上运行在同一内存空间中,因此我认为不建议调用任何 exec-family 函数。【参考方案6】:

由 exec() 等创建的进程将从父进程(包括 stdin、stdout、stderr)继承其文件句柄。如果父级在调用 fork() 之后但在调用 exec() 之前更改了这些,则它可以控制子级的标准流。

【讨论】:

以上是关于if((pid1=fork())<0)和if(pid1=fork()<0)有啥区别么?!因为在Linux下这两个条件运行出的结果完全不同的主要内容,如果未能解决你的问题,请参考以下文章

检查两个进程的退出代码

进程的管道通信

wait() 在 Linux 中是如何工作的?

linux 编程问题:子进程1和2为啥也能对管道进行操作?

操作系统第2次实验报告:创建进程

C中的fork()函数