目录
创建进程
进程地址空间
为什么要用虚拟地址呢?
什么是进程地址空间?
为什么要写时拷贝呢?
创建进程
前面提到使用fork可以创建子进程,现在介绍fork创建子进程的细节。
fork创建子进程的时候,子进程的内核数据结构和代码都会继承自父进程,而数据以写时拷贝的方式共享或独立,这里共享或独立指的是 当父子进程没有人修改全局数据的时候,父子进程共享该数据,一旦父进程或者子进程修改数据的时候,就会发生写时拷贝,数据就被拷贝出了一份,于是父子进程的数据就独立了。
比如如下代码:
编译运行发现结果如下:
可以看见fork之前的代码子进程没有执行,难道子进程只是继承了父进程的部分代码吗?
答案是子进程继承了父进程所有的代码,子进程只能在fork之后开始执行是因为子进程也继承了程序计数器EIP寄存器中的内容,这个寄存器保存了当前CPU执行到的指令的下一条指令,于是子进程便从EIP存储的指令对应的代码开始执行。
于是有fork的用法:
创建子进程和父进程执行不同的代码块
一个进程要执行一个不同的程序,可以创建子进程,然后对子进程进行程序替换
fork也会失败,失败原因主要是系统中进程数量太多或者用户创建的进程数超过限制 。
前面提到写时拷贝,这是由OS的内存管理模块完成的,那么写时拷贝是什么?回答之前,得知道在哪里进行的拷贝,这与进程地址空间有关。
进程地址空间
首先回顾C程序的地址空间是这样的:
数据段即数据区,数据区又分为已初始化全局数据区,未初始化全局数据区
但是这个程序地址空间并不是物理地址,而是虚拟地址 。
为什么要用虚拟地址呢?
首先硬件不能阻拦你访问,只能被动读取和写入,一旦你失误写入数据,就可能会影响到系统的其他进程,所以实现OS时,不应该让用户直接访问物理地址,而是使用虚拟地址,让OS将物理地址保护起来,让环境稳定运行。如何保护内存呢?在访问地址时,OS发现页表没有映射关系,非法访问就会被OS拦截,OS就会向进程发送信号,从而终止进程。
使用虚拟地址还有其他好处,OS将进程管理和内存管理通过地址空间,进行功能模块的解耦,当需要使用内存空间的时候,OS才进行在内存管理模块执行申请内存的代码,然后在页表建立映射关系,而OS在调度进程时不用马上执行内存管理的代码,也即两种管理各种独立了。
而且使用同一套虚拟地址,可以让进程、程序以统一的视角看待内存,方便编译和加载程序,简化进程设计和实现。
注意:当程序未被加载到内存称为进程的时候,程序内部就有地址了,比如程序的链接阶段,不同目标文件全局变量,函数等需要确定地址,这个地址就是虚拟的地址。
什么是进程地址空间?
当每个进程在启动时,都会让OS给他创建一个地址空间叫做进程地址空间,OS用内核数据结构struct mm_struct描述进程地址空间,因为是使用同一套虚拟地址,所以每个进程都认为自己独占了系统的内存资源,进程怎么和进程地址空间关联起来呢,描述进程的PCB结构体task_struct中有有成员指针指向struct mm_struct(描述进程地址空间的结构体),而 OS为进程构建页表,这个进程地址空间通过页表与物理内存建立映射关系。
查看源码如下:
查看mm_struct 的定义:
mm_struct中的mmap(节点指针)是一个链表,链表连接者节点,节点的结构体vm_area_struct 中有vm_start 和vm_end用来划分进程地址空间的区域,还有vm_page_prot表示访问权限等成员。
最后画草图总结一下是这样:
现在就可以理解写时拷贝了,首先父进程创建子进程的时候,子进程继承了父进程的内核数据结构,包括描述进程结构体task_struct ,进程地址空间mm_struct,页表等等,子进程和父进程的代码和数据都是共享的,在内存上它们都是只有一份都是一样的。
如图:
当父进程或者子进程需要修改数据时,谁先修改,谁就先发生写时拷贝,比如子进程修改了全局数据,OS就将在物理内存上把该存储的数据拷贝一份,并在页表建立新的映射到子进程的进程地址空间,于是父进程和子进程这一份数据就不再共享,而是各自独立了,如图:
为什么要写时拷贝呢?
为什么不直接在物理内存上分别存储父子进程的两份数据呢?
1.原因在于父进程的数据,对于子进程来说可能不需要,或者子进程只是读取数据,不会对数据做修改,那么采用写时拷贝就可以节省空间。
2.理想的拷贝是父进程或子进程要写入的数据进行拷贝,不写入的数据不拷贝,但是如何判断数据是否会被写入呢?这种理想化技术难以实现。
3.倘若将父进程的数据全部拷贝给子进程,这样的话这些工作都是在调用fork函数完成的,直接增加了fork函数执行的时间,增加了调用fork函数的成本。
4.这种延迟拷贝策略,可以提高内存的使用率,倘若父子进程的这份共享数据未被写入时,那么就不会被拷贝,物理内存的未使用空间就更多,就可以腾出空间给其他进程使用。