进程的创建(fork、vfork)

一、fork()

1.fork()的概述

fork()允许父进程创建一个子进程,拷贝父进程的内核栈、PCB等,如果内核允许,将对子进程的PCB进行相应的修改(如修改子进程的进程描述符pid),拷贝父进程的执行代码段、数据段、堆栈内容,之后父进程和子进程各自拥有独立的内存空间,执行各自的程序代码。简单说,子进程拷贝父进程的资源后,拥有自己的PCB及虚拟地址空间,开始执行自己的程序代码。

代码段通过fork()的返回值判断父子进程:父进程返回子进程pid,子进程返回0,创建子进程失败返回-1。创建子进程失败的原因可能是:超过系统允许创建的最大进程数;针对真实用户进程数目达到规定的最大数目。

父进程的fork()返回子进程的pid,可以方便地监听子进程的状态,比如可以通过wait获取子进程的退出状态。子进程可以调用getpid()获取自己的进程pid,可以通过getppid()获取父进程的pid。

fork()之后是先执行父进程还是先执行子进程取决于系统调用的先后,可以通过在父进程sleep()让子进程限制性有效代码。当然这并不是最好的方法。

2.父子进程之间的文件共享

在fork()之间打开文件并获取文件的句柄,fork()之后父子进程共享该句柄,并且共享文件状态。比如共享文件的文件偏移量(通过read、write、lseek修改)、文件的状态标志(open、fcntl的F_SETFL改变)。文件偏移量的共享父子进程的write()不会相互覆盖,但父子进程的输出会随意混杂,解决的办法就是父进程wait()阻塞等待子进程执行完毕并退出,或通过进程间的同步进行相应的控制。值得一提的是,shell创建的子进程后,shell执行wait()等待子进程执行完毕,子进程执行期间独占标准输出(控制台)。

3.fork()的内存语义

在早期的UNIX实现中,fork()会傻瓜式地对父进程代码段、数据段、堆栈段进行拷贝创建子进程,内核为子进程分配物理块,完成虚拟内存-物理内存的页表映射。然而大量的IO操作必然浪费大量的时间,且fork()之后常常伴随着exec(),新进程替换旧进程的代码段,重新分配代码段、数据段、堆栈,之前的映射时间将全部浪费。

大部分现代UNIX(类UNIX)采用两种方法避免这种浪费:

  • 内核将每个进程的代码段标记为只读,父子进程实现代码段的共享,子进程创建的时候将代码段的页表项映射到和父进程相同的物理块。
  • 使用“写时复制”技术。子进程创建的时候复制父进程的代码段、数据段、堆栈的页表项,当父进程对页表项指向的内存块进行修改时,先将物理内存块的内容复制到另一个物理内存块,并将子进程相应的页表项映射更新到新的物理内存块。当子进程要对也页表项指向的内存块进行修改时,先将其映射的物理块拷贝到另一个可用的物理内存块,更新子进程的页表项,在新的物理块进行内容的修改。

3.fork()的使用场景

创建多个进程是任务分解的有效方法之一。如服务器负责监听客户端的请求,为处理客户端的每一个请求而创建一个新的子进程。

二、vfork()

1.vfork()的概述

vfork()的引入是为了解决早期fork()完全复制效率低的问题,然而在UNIX引入写时复制技术之后,对vfork()的需求剔除殆尽,而且vfork()的语义较为怪异,常会引入各种bug,因此应尽量避免使用vfork(),除非能带来性能的巨大提升(然而这种情况实属概率极小)

类似于fork(),vfork()可以为父进程创建一个子进程,然而vfork()是为了建立子进程后立即执行exec而设立的。

vfork因具有两个特性而更具效率

  • 无需为子进程复制父进程的虚拟地址空间,子进程创建后共享父进程的虚拟内存,直到调用exit()或_exit()后退出。
  • 子进程退出之前,将暂停执行父进程。

因此,子进程几乎将会更改父进程的虚拟内存,除非子进程在exec()之前不影响父进程的虚拟内存,然而这种情况也少之又少。

三、fork之后的竞争条件

在Linux2.2.19中,fork()之后99.97%继续执行父进程,其他的0.03%是因为父进程的时间片用完进而开始执行子进程。也就是说如果程序设定的基于父进程先执行的,那么程序通常可以继续执行,不过大概每3000次就会出现一次错误。
在内核稳定版 2.4 系列中,一度曾试验性地推出了一个“fork()之后由子进程先运行”的补丁,虽然这一改变之后又为 2.4 系列内核所舍弃,不过后来还是在 Linux 2.6 中采用,因此,程序假定于 2.2.19 内核的行为会在内核 2.6 中遭到推翻。
支持fork()之后子进程先执行的原因是:如果父进程先执行,因为写时复制技术,父进程堆栈及数据段的变化将会导致内核为子进程申请新的可用物理块并进行内容复制,然而子进程执行之后调用exec(),之前复制的内容将全部作废
支持fork()之后父进程先执行的原因是:父进程创建子进程后,大多数情况还拥有CPU时间片,这时候进行进程调度,需要保存父进程的上下文环境,并切换到子进程的上下文环境,这个过程同样很浪费时间。

总之,值得强调的是:两种行为间的性能差异很小,对于大部分应用程序并无影响。因此无论内核版本是什么,都不应对fork之后父子进程执行的顺序做任何假设。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值