哪些段受写时复制的影响?

ash*_*ish 4 linux operating-system memory-management copy-on-write

我对复制的理解是"每个人都有一个共享的相同数据副本,直到它被写入,然后复制".

  1. 是由堆和bss段组成的相同数据的共享副本还是仅包含堆?
  2. 哪些内存段将被共享,这取决于操作系统?

Cra*_*tey 8

操作系统可以设置它希望的任何"写入时复制"策略,但通常,它们都做同样的事情(即最有意义的事情).

对于类似POSIX的系统(linux,BSD,OSX)来说,有四个感兴趣的领域(你称之为段):( 去data哪里int x = 1;),bss(int y去哪里),sbrk(这是堆/ malloc),以及stack

fork完成后,OS设置为共享父的所有页面的孩子了新的一页地图.然后,在父级子级的页面映射中,所有页面都标记为只读.

每个页面映射还有一个引用计数,指示共享页面的进程数.在fork之前,refcount将为1,之后将为2.

现在,当任一进程尝试写入R/O页面时,它将出现页面错误.操作系统将看到这是"写入时复制",将为进程创建一个私有页面,从共享中复制数据,将页面标记为可写入该进程并恢复它.

它还会减少引用计数.如果refcount现在[再次] 1,操作系统会将另一个进程中的页面标记为可写和非共享[这消除了另一个进程中的第二页错误 - 加速只是因为此时操作系统知道另一个过程应该可以自由地再次编写].此加速可能取决于操作系统.

实际上,该bss部分得到了更加特殊的待遇.在它的初始页面映射中,所有页面都映射到包含全零的单个页面(也称为"零页面").映射标记为R/O. 因此,该bss区域的大小可能是千兆字节,它只占用一个物理页面.这种单一的,特殊的,零页面之间共享所有 bss的部分全部过程,他们不管有任何彼此在所有关系.

因此,一个进程可以从该区域中的任何页面读取并获得它所期望的:零.只有当进程尝试写入这样的页面时,写入机制上的相同副本才会启动,进程将获得私有页面,映射将被调整,并且进程将恢复.现在可以随意写入页面.

操作系统可以再次选择其策略.例如,在fork之后,共享大多数堆栈页面可能更有效,但是从"当前"页面的私有副本开始,由堆栈指针寄存器的值确定.

exec[在孩子身上] 进行系统调用时,内核必须撤消在fork[降低refcounts] 期间完成的大部分映射,释放孩子的映射等,并恢复父母的原始页面保护(即它将不再共享)它的页面,除非它做另一个fork)


虽然不是原始问题的一部分,但有一些相关的活动可能会引起关注,例如按需加载 [页面]和按需连接 [符号]后exec系统调用.

当进程执行时exec,内核执行上面的清理,并读取可执行文件的一小部分以确定其对象格式.主导格式是ELF,但可以使用内核理解的任何格式(例如OSX可以使用ELF [IIRC],但它也有其他格式].

对于ELF,可执行文件有一个特殊的部分,它提供了一个完整的FS路径,即所谓的"ELF解释器",它是一个共享库,通常是/lib64/ld.linux.so.

内核使用内部形式mmap将其映射到应用程序空间,并为可执行文件本身设置映射.大多数东西都被标记为R/O页面并且 "不存在".

在我们进一步讨论之前,我们需要讨论页面的"后备存储".也就是说,如果发生页面错误,我们需要从磁盘加载页面,它来自哪里.对于heap/malloc,这通常是交换磁盘[aka paging disk].

在linux下,它通常是安装系统时添加的"linux swap"类型的分区.当一个页面被写入必须刷新到磁盘以释放一些物理内存时,它就会被写入.请注意,第一部分中的页面共享算法仍然适用.

无论如何,当可执行文件首次映射到内存时,其后备存储是文件系统中的可执行文件.

因此,内核将应用程序的程序计数器设置为指向ELF解释器的起始位置,并将控制转移给它.

ELF口译员开展业务.每当它试图执行的自身[一个"代码"页],其一部分映射装载,发生页面错误和负载从所述后备存储器页(例如,ELF翻译的文件),并改变所述映射至R/O但是现在.

这发生在ELF解释器,共享库和可执行文件本身.

ELF解释器现在将mmap用于映射libc到应用程序空间[再次,根据需求加载].如果ELF解释器必须修改代码页以重新定位符号[或尝试写入任何具有该文件作为后备存储的data页面,如页面],则会发生保护错误,内核会更改页面的后备存储磁盘文件到交换磁盘上的页面,调整保护,并恢复应用程序.

内核还必须处理ELF解释器(例如)试图写入[说] data从未加载过的页面的情况(即必须首先加载它然后将后备存储更改为交换磁盘)

然后ELF解释器使用部分libc来帮助它完成初始链接活动.它重新安置了允许它完成工作所需的最低限度.

但是,ELF解释器不会在大多数其他共享库的所有符号附近重定位.它去翻可执行文件,并再次使用mmap,创建一个映射的共享库可执行需求(即你看到的,当你这样做ldd executable).

这些映射到共享库和可执行文件的映射可以被认为是"段".

有一个符号跳转表,指向每个共享库中的解释器.但是,ELF解释器只做了很小的改动.

[ 注意:这是一个松散的解释]只有当应用程序试图调用给定函数的跳转条目时[这就是GOT等.人.您可能已经看过的东西]重新安置发生了.跳转条目将控制转移到解释器,解释器定位符号的实际地址并调整GOT,使其现在直接指向符号的最终地址并重做调用,现在将调用实际函数.在随后调用相同的给定函数时,它现在变为直接.

这称为"按需链接".

所有这些mmap活动的副产品是经典的sbrk系统调用几乎没有用.它很快就会与其中一个共享库内存映射冲突.

所以,现代libc不使用它.当malloc需要来自操作系统的更多内存时,它会从匿名中请求更多内存,mmap并跟踪哪些分配属于哪个mmap映射.(即如果释放了足够的内存以构成整个映射,则free可以执行此操作munmap).

因此,总而言之,我们在同一时间进行"复制写入","按需加载"和"按需链接".看来复杂,但让forkexec快速进入,顺利.这增加了一些复杂性,但仅在需要时("按需")才进行额外的开销.

因此,代替在程序开始启动时的大的延迟/延迟,根据需要,开销活动在程序的生命周期中展开.