目录
写在前面的话
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创建守护进程