Linux下fork与写时拷贝技术(COW)详解

1. 写时拷贝的概念

Linux在使用fork()函数进程创建时,传统fork()的做法是系统把所有的资源复制给新创建的进程,这种方式不仅单一,而且效率低下。因为所拷贝的数据或别的资源可能是可以共享的。现在Linux的fork()使用写时拷贝页来实现新进程的创建,它是一种可推迟甚至避免数据拷贝的技术,刚开始时内核并不会复制整个地址空间,而是让父子进程共享地址空间,只有在写时才复制地址空间,使得父子进程都拥有独立的地址空间,即资源的复制是在只有需要写入时才会发生,因此而称之为Copy on Write(COW)。在此之前都是以读的方式去和父进程共享资源,这样,在页根本不会被写入的场景下,fork()立即执行exec(),无需对地址空间进行复制,fork()的实际开销就是复制父进程的一个页表和为子进程创建一个进程描述符,也就是说只有当进程空间中各段的内存内容发生变化时,父进程才将其内容复制一份传给子进程,大大提高了效率。

那么子进程的物理空间没有代码,怎么去取指令执行exec系统调用呢?

其实,在fork()之后,exec()之前,子进程和父进程是共享物理空间(内存区)的,子进程的代码段,数据段和堆栈都指向父进程物理空间,即两者的虚拟空间不同,但物理空间其实是同一个,当父进程或者子进程有需要修改段的行为时,再为子进程分配相应段的物理空间,若不是exec则内核会给子进程的数据段,堆栈段分配相应的物理空间,至此二者各自有各自的物理空间,互不影响。而代码段则继续共享父进程的物理空间,因为两者的代码完全相同,但如果是因为exec,,由于二者的执行的代码不同,则也需为子进程分配代码段的物理空间。

2. 详细

现在有一个父进程P1,这是一个主体,那么它是有灵魂也就身体的。现在在其虚拟地址空间(有相应的数据结构表示)上有:正文段,数据段,堆,栈这四个部分,相应的,内核要为这四个部分分配各自的物理块。即:正文段块,数据段块,堆块,栈块。至于如何分配,这是内核去做的事,在此不详述。

3. 关于fork函数

#include<unistd.h>
pid_t fork(void);
//返回:在子进程中返回0,在父进程中返回子进程的id,出错返回-1.

fork在子进程中返回0而不是父进程的ID的原因在于:任何子进程只有一个父进程,而且子进程总是可以通过调用getppid取得父进程的ID。相反,父进程可以有许多子进程,而且无法获得各个子进程的进程ID。如果父进程想要跟踪所有子进程的ID,那么它必须记录每次调用fork的返回值,所以父进程返回的是子进程的进程ID
fork有两个典型的用法:
1.一个进程创建一个自身的拷贝,这样每个拷贝都可以在另一个拷贝执行其他任务的同时处理各自的某个操作。这是网络服务器的典型用法。
2.一个进程想要执行另一个程序。既然创建新进程的唯一方法为调用fork,该进程于是首先调用fork创建一个自身的拷贝,然后其中一个拷贝(通常为子进程)调用exec把自身替换成新的程序。这是诸如shell之类程序的典型用法。

4. 关于exec函数

#include<unistd.h>
int execl(const char *pathname, const char *arg0,.../* (char *)0 */);
int execv(const char *pathname, char *const argv[]);
int execle(const char *pathname, const char *arg0,.../* (char *)0,char *const envp[] */);
int execve(const char *pathname, char *const argv[], char *const envp[]);
int execlp(const char *filename, const char *arg0,.../* (char *)0 */);
int execvp(const char *filename, char *const argv[]);
//所有六个函数返回:-1——失败,无返回——成功

从上面我们已经知道了fork会创建一个子进程。子进程的是父进程的副本。
exec函数的作用就是:装载一个新的程序(可执行映像)覆盖当前进程内存空间中的映像,从而执行不同的任务。

exec系列函数在执行时会直接替换掉当前进程的地址空间。
我去画张图来理解一下:
在这里插入图片描述

现在P1用fork()函数为进程创建一个子进程P2
内核:

  • 复制P1的正文段,数据段,堆,栈这四个部分,注意是其内容相同。
  • 为这四个部分分配物理块,P2的:正文段->PI的正文段的物理块,其实就是不为P2分配正文段块,让P2的正文段指向P1的正文段块,数据段->P2自己的数据段块(为其分配对应的块),堆->P2自己的堆块,栈->P2自己的栈块。

如下图所示:同左到右大的方向箭头表示复制内容。

在这里插入图片描述

5.Copy On Write技术原理

内核只为新生成的子进程创建虚拟空间结构,它们来复制于父进程的虚拟究竟结构,但是不为这些段分配物理内存,它们共享父进程的物理空间,当父子进程中有更改相应段的行为发生时,再为子进程相应的段分配物理空间。

在这里插入图片描述

Copy On Write技术好处是什么?

  • COW技术可减少分配和复制大量资源时带来的瞬间延时。
  • COW技术可减少不必要的资源分配。比如fork进程时,并不是所有的页面都需要复制,父进程的代码段和只读数据段都不被允许修改,所以无需复制。

Copy On Write技术缺点是什么?

  • 如果在fork()之后,父子进程都还需要继续进行写操作,那么会产生大量的分页错误(页异常中断page-fault),这样就得不偿失。

几句话总结Linux的Copy On Write技术:

  • fork出的子进程共享父进程的物理空间,当父子进程有内存写入操作时,read-only内存页发生中断,将触发的异常的内存页复制一份(其余的页还是共享父进程的)。
  • fork出的子进程功能实现和父进程是一样的。如果有需要,我们会用exec()把当前进程映像替换成新的进程文件,完成自己想要实现的功能。

6.关于vfork函数

vfork():这个做法更加火爆,内核连子进程的虚拟地址空间结构也不创建了,直接共享了父进程的虚拟空间,当然了,这种做法就顺水推舟的共享了父进程的物理空间。
在这里插入图片描述
PS:实际上COW技术不仅仅在Linux进程上有应用,其他例如C++的String在有的IDE环境下也支持COW技术,即例如:

string str1 = "hello world";
string str2 = str1;

之后执行代码:

str1[1]='q';
str2[1]='w';

执行修改后,此时str1的地址会发生变化,而str2的地址还是原来的。即在复制对象时,并不真正为新对象开辟内存空间,而是在新对象的内存映射表中设立一个指针,指向源对象,这样在进行读操作时因为并不修改对象,并不会给源对象带来影响,当某一时刻要对某一对象进行修改时,即写操作时,再将对象复制到新的内存空间中去,在这上面执行修改,以避免相互之间的影响。这样做的一个好处也是尽可能提高效率。
这就是C++中的COW技术的应用,不过VS2005似乎已经不支持COW。

参考链接:

https://blog.csdn.net/bad_good_man/article/details/49364947

https://www.cnblogs.com/biyeymyhjob/archive/2012/07/20/2601655.html

https://blog.csdn.net/weixin_33701617/article/details/88716535?depth_1-utm_source=distribute.pc_relevant.none-task&utm_source=distribute.pc_relevant.none-task

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值