共享区域与私有区域
每个进程都有自己的私有虚拟地址空间,避免了受到其他进程的错误读写。但是,通常的c程序几乎都使用到标准库函数,例如printf或者scanf,如果每个进程都要为这些常用库函数在物理内存保留一份拷贝,这样对内存就非常浪费了。
为了解决上述问题,可以将常用库函数设定为共享对象,共享对象在物理内存上只有一份拷贝,多个进程可以把自身虚拟内存的一个区域映射到该共享对象上,这些区域就叫共享区域,如果一个进程在自己的共享区域进行写操作,在其他进程的共享区域内能看到相应的改变,并且这种改变也会反映到磁盘上。
除了共享对象,还存在私有对象,一个进程可以将自身虚拟内存的一个区域映射到私有对象上,该区域就叫私有区域,对私有区域做出改变,对于其他进程来说是不可见的,而且对这种改变不会反映到磁盘上。
写时拷贝
私有对象使用写时拷贝被映射到虚拟内存上。私有对象开始时的存在方式基本和共享对象一样,即私有对象在物理内存上只有一份拷贝,多个进程将自己虚拟内存的一个区域(这些区域的地址范围不必相同)映射到私有对象上,各进程对应私有对象的页表条目也被标记为只读,并且区域结构被标记为私有的写时拷贝。各进程共享在物理内存上的私有对象,但如果一个进程修改了私有区域的某一页,会触发一个保护故障,内核的故障处理程序就会为这个页面创建一份新的拷贝,并更新页表条目指向这个新的拷贝,然后恢复这个页面的写权限。当故障处理程序返回时,cpu在新创建的页面上重新执行这个写操作。
fork和execve
父进程调用fork创建子进程时,内核为子进程分配一个唯一的pid,并拷贝了父进程的mm_struct,区域结构(vm_area_struct)以及页表,将父子进程每个页面设置为只读,区域结构设置为私有的写时拷贝。fork返回会,子进程就具有了和父进程相同的虚拟地址空间。当其中一个进程对自己的虚拟内存进行写操作时,写时拷贝机制就会为该进程创建新页面。
假设子进程接着执行execve("a.out",NULL,NULL),该函数会加载并运行可执行目标文件a.out中的程序,用a.out程序去替代子进程,加载的过程如下:
1)删除子进程虚拟内存用户部分的区域结构。
2)为a.out程序的代码段,数据段,堆栈区域创建新的区域结构,区域结构被设置为私有的写时拷贝。
3)如果a.out与共享对象链接,则这些对象都是动态链接到这个程序的,然后映射到虚拟内存的共享区域。
4)execve设置程序计数器PC,使其指向代码段的入口点。