fork系统调用详解

目录

写在前面的话

fork()介绍

Linux 的写时复制(COW,copy on write)技术

fork之后的文件描述符

使用fork创建守护进程

写在前面的话

最开始了解fork的时候,着急着应用,也没太去深究fork()的原理,因此也踩了些坑,比如,在fork之后,对类中定义好的一个变量,或对一个全局的变量(对父子进程全局),父子进程中的运行输出结果不一样,与最开始的子进程共享父进程资源预期结果不一样。在这里总结一下,希望后续看到的筒子做学问切记马马虎虎

一个简单小问题的引入

在详细展开前,请大家先来看这样一段代码:

int main()
{
	int val  = 0;
	pid_t pid ;
	pid = fork();
	if(pid > 0)
	{
		val += 3; //(1)
		printf("val in father: %d, val address: %d\n", val, &val);
		
	}
	else if(pid == 0)
	{
		val += 33;//(2)
		printf("val in child: %d, val address: %d\n", val, &val);
	}
	return 0;	
}

对于上述的代码片段,我们考虑以下几个场景的输出:
1、注释代码段(1),输出结果是什么?
2、注释代码段(2),输出结果是什么?
3、都不注释,输出结果是什么?
这里我将运行结果对应序号给出:
1、

val in father: 0, val address: -1172397192
val in child: 33, val address: -1172397192

2、

val in father: 3, val address: -1757223528
val in child: 0, val address: -1757223528

3、

val in father: 3, val address: -1759703768
val in child: 33, val address: -1759703768

可以看到,每次父子进程中的val值都不同,但地址值打印却是相同的。这是为什么呢?这里先不急着给出解释,先给大家说明一下,为什么要考虑上述三种情况,实际上应该考虑四种,即父子进程都不进行更改的情况,但该场景肯定是值和地址值都相同,此处就不再展示。这里之所以考虑三种,当我们不了解fork时,我们应该要去推测(1)父进程写是否会影响子进程的值;(2)子进程写是否会影响父进程的该变量值;(3)同时写会怎样?;

从程序运行结果看:
1、父子进程的val变量独立,父进程的操作不影响子进程,子进程的操作也不影响父进程;
2、父子进程的val变量显示值都一样;
为什么会出现这样的情况呢,别慌,我们来看看fork()函数的介绍。

fork()介绍

fork()的原型如下:

#include <unistd.h>
pid_t fork(void);

父进程调用fork()后,会返回两次,一次由父进程返回,返回值是子进程的进程id(pid),一次由子进程返回,返回值是0。
仅靠这些肯定不足以解释我们看到的现象,因此,我在《Unix环境高级编程》中找到了如下描述:
子进程和父进程继续执行fork调用之后的指令,子进程是父进程的副本。例如,子进程获得父进程的数据空间,堆和栈的副本,但子进程并不共享这些存储空间部分。父进程和子进程共享正文段。
由于fork之后经常跟随着exec,所以现在很多实现并不执行一个父进程的数据段、堆栈的完全副本,作为替代,使用了写时复制技术。这些区域由父进程和子进程共享,而且内核将他们的访问权限改变为制度。如果父进程和子进程中的人一个试图修改这些区域,则内核只为修改区域的那块内存制作一个副本,通常是虚拟存储一同中的一“页”。
也就是说,父进程的数据空间,堆栈,被子进程copy了一部分,copy之后,父进程的保留自己的,子进程的也有自己的,只是刚好一模一样而已。而且操作系统很聪明地,为了没必要的内存分配和数据复制,只有在对内存执行写操作时,复制才会发生。
看到这里我们是不是已经清晰了很多,别慌,还有一个问题没有解决,为什么数据已经改变了,也就是说已经执行了写时复制了,两个变量的地址为什么还一样呢?这就不得不再去深究以下写时复制技术了;

Linux 的写时复制(COW,copy on write)技术

  • C程序/Linux的存储空间布局
    在了解写时复制之前,先给大家简要第介绍一下c程序的存储空间布局,以便大家在理解更加透彻,这里只简单介绍,详细的存储空间布局介绍请参考本人的另一篇博文。
    典型的C程序一般由如下几个部分组成:正文段,初始化数据段,未初始化数据段,栈,堆。如图:
    在这里插入图片描述
  • 写时复制技术
    由于完整地详述篇幅过长,这里附上给各位推荐一篇关于该技术的原创po文,感兴趣的也可以自行去查究。
    简单的来说就是,fork之后还未进行任何操作之前的两个进程使用的物理空间是相同的,但当父子进程中由更改相应段的行为发生时,就会为子进程相应的段分配物理空间。但这里我们看到在上文提到《unix环境高级编程》知道子进程是做了拷贝的,那么拷贝的只可能是虚拟空间,所以父子进程的虚拟空间不同,并且有这么一句:
    父子进程共享正文段。这句话是什么意思呢。
    实际上我们程序fork后常跟exec来执行其他可执行程序,而在exec之前,父子进程是共享正文段的,也就是执行代码段相同,但exec之后,内核就会给子进程的代码段分配独立的物理空间,子进程就能独立执行其他的程序了。

在fork之后exec之前两个进程用的是相同的物理空间(内存区),子进程的代码段(正文段)、数据段、堆栈都是指向父进程的物理空间,也就是说,两者的虚拟空间不同,但其对应的物理空间是同一个。当父子进程中有更改相应段的行为发生时,再为子进程相应的段分配物理空间,如果不是因为exec,内核会给子进程的数据段、堆栈段分配相应的物理空间(至此两者有各自的进程空间,互不影响),而代码段(子进程)继续共享父进程的物理空间(两者的代码完全相同)。而如果是因为exec,由于两者执行的代码不同,子进程的代码段也会分配单独的物理空间。
到此,我们的问题基本解决完毕,但在此留给大家一个思考题:
程序取地址符取到的地址,跟Linux的虚拟空间地址和物理地址的关系是什么呢。感兴趣的课自行去查究,由于篇幅过长不再详述。
Linux写时复制技术

fork之后的文件描述符

在这里补充说明一下这个话题,因为我们常有父进程创建的文件句柄的情况。那么父进程中打开的文件描述符,子进程中是一种什么样的状态呢。
实际上,在fork的时候,在内核进程表(见下图)会创建一个新的进程表项。新的进程表项有很多属性和原来的进程相同,比如堆指针,栈指针,标志寄存器的值(还记得上面提到的为什么地址显示是相同的吗),但也有很多属性是不同的,比如,该进程的PPID被设置成进程中的PID,信号位图被清除(原进程设置的信号处理函数不再对新进程起作用),在父进程中打开的文件描述符,在子进程中也是打开的,并且文件描述符的引用计数会加1,并且父进程的用户根目录,当前工作目录等变量的引用计数会加1。

下图来自:进程表项
在这里插入图片描述

使用fork创建守护进程

详细见我的另一篇文章
fork创建守护进程

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值