我对 写 时 复制的 理解是:“每个人都有相同数据的单个共享副本,直到被写入,然后再创建一个副本”。
操作系统可以设置所需的任何“写时复制”策略,但是通常,它们都执行相同的操作(即最有意义的操作)。
松散地,对于类似POSIX的系统(Linux,BSD,OSX),有四个感兴趣的领域(您所说的段):(data哪里int x = 1;),bss(哪里int y),sbrk(这是堆/ malloc)和stack
data
int x = 1;
bss
int y
sbrk
stack
当fork完成后,OS设置了新的一页地图对孩子说股所有的父母的页面。然后,在父级 和 子级的页面映射中,所有页面都标记为只读。
fork
每个页面映射还具有一个参考计数,该参考计数指示共享该页面的进程数。在派生之前,引用计数为1,之后为2。
现在,当 任何一个 进程尝试写入R / O页面时,都会出现页面错误。操作系统将看到这是用于“写入时复制”,将为该进程创建一个私有页面,从共享中复制数据,将该页面标记为该进程可写并继续。
它还会降低引用计数。如果现在refcount [再次]为1,则OS会将 另一个 进程中的页面标记为可写且不可共享[这消除了另一个进程中的第二个页面错误- 加速是因为这时OS知道另一个过程应该可以自由地再次写[]。这种加速可能取决于操作系统。
实际上,该bss部分将获得 更多 特殊待遇。在它的初始页面映射中,所有页面都映射到包含所有零(也称为“零页面”)的 单个 页面。该映射标记为R / O。因此,该bss区域的大小可能为千兆字节,并且仅会占用一个物理页面。这种单一的,特殊的,零页面之间共享 所有 bss的部分 全部 过程,他们不管有 任何 在所有的相互关系。
因此,一个进程可以从该区域的任何页面读取并获得期望的值:零。只有当进程尝试写入此类页面时,才会启动相同的写入机制副本,该进程获得私有页面,调整映射,然后恢复该进程。现在可以自由地将其写入页面。
操作系统可以再次选择其策略。例如,在派生之后,共享 大多数 堆栈页可能会更有效,但是从“当前”页的私有副本开始,这是由堆栈指针寄存器的值确定的。
当exec在子fork节点上执行系统调用时,内核必须撤消在[降低引用计数],释放子节点的映射等过程中完成的许多映射,以及还原父节点的原始页面保护(即它将不再共享)它的页面,除非它另做一个fork)
exec
尽管这不是您原始问题的一部分,但可能涉及到一些相关的活动,例如 ,按需加载 [页面]和 按需exec调用[符号]在syscall 之后 链接 。
当进程执行an时exec,内核将执行上述清理,并读取可执行文件的一小部分以确定其对象格式。主要格式是ELF,但是可以使用内核理解的任何格式(例如OSX可以使用ELF [IIRC],但也有其他格式)。
对于ELF,可执行文件具有一个特殊的部分,该部分提供指向所谓的“ ELF解释器”的完整FS路径,该路径是共享库,通常为/lib64/ld.linux.so。
/lib64/ld.linux.so
内核使用的内部形式mmap将其映射到应用程序空间,并为可执行文件本身设置映射。大多数东西都被标记为R / O页面 并且 “不存在”。
mmap
在继续之前,我们需要讨论页面的“后备存储”。也就是说,如果发生页面错误,我们需要从磁盘从何处加载页面。对于堆/ malloc,通常是交换磁盘[aka分页磁盘]。
在linux下,通常是安装系统时添加的“ linux swap”类型的分区。当写入页面时必须将其刷新到磁盘上以释放一些物理内存时,它会被写入那里。请注意,第一部分中的页面共享算法仍然适用。
无论如何,当可执行文件首先 映射 到内存时,其后备存储是文件系统中的可执行文件。
因此,内核将应用程序的程序计数器设置为指向ELF解释器的起始位置,并将控制权转移给它。
ELF解释器负责其业务。每当它试图执行的自身[一个“代码”页],其一部分 映射 但 不 装载,发生页面错误和负载从所述后备存储器页(例如,ELF翻译的文件),并改变所述映射至R / O但 存在 。
ELF解释器,共享库和可执行文件本身就会发生这种情况。
现在,ELF解释器将mmap用于映射libc到应用程序空间(同样,受需求负载的影响)。如果ELF解释器必须修改代码页以重新定位符号[或尝试写入任何将文件作为后备存储的文件,例如data页面],则会发生保护错误,内核会将页面的后备存储更改为磁盘上的 文件 到交换磁盘上的页面,调整保护,然后恢复应用程序。
libc
内核还必须处理ELF解释器(例如)试图写入[说] data尚未加载的页面的情况(即,它必须先加载它,然后将后备存储更改为交换磁盘)。
ELF解释器然后使用的部分内容libc来帮助其完成初始链接活动。它重新定位了允许其完成工作所需的最低要求。
但是,ELF解释器 不会 在大多数其他共享库的所有符号附近重新定位。它 会 去翻可执行文件,并再次使用mmap,创建一个 映射 的共享库可执行需求(即你看到的,当你这样做ldd executable)。
ldd executable
这些到共享库和可执行文件的映射可以被认为是“段”。
每个共享库中都有一个指向跳转的解释器的符号表。但是,ELF解释器所做的更改很少。
[ 注意: 这是一个宽松的解释]仅当应用程序尝试调用给定函数的跳转条目时[这就是GOT等。等 您可能已经看到的东西]是否发生重定位。跳转条目将控制权转移给解释器,解释器查找符号的 真实 地址并调整GOT,以便它现在直接指向符号的最终地址并重做调用,该调用现在将调用实函数。在随后调用相同的给定函数时,它现在可以直接执行。
这称为“按需链接”。
所有这些mmap活动的副产品是经典sbrksyscall,几乎没有用处。它将很快与共享库内存映射之一发生冲突。
因此,现代libc不使用它。当malloc需要从OS获得更多内存时,它将从匿名服务器请求更多内存,mmap并跟踪哪些分配属于哪个mmap映射。(即,如果释放了足够的内存来构成整个映射,则free可以执行munmap)。
malloc
free
munmap
因此,总而言之,我们同时进行“按需复制”,“按需加载”和“按需链接”。看来复杂,但让fork和exec快速进入,顺利。这增加了一些复杂性,但是仅在需要时(“按需”)才执行额外的开销。
因此,根据需要,开销活动不是在程序开始启动时出现较大的停顿/延迟,而是在程序的整个生命周期内分散开来。