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

时间:2016-05-05 10:17:02

标签: linux memory-management operating-system copy-on-write

我对 copy-on-write 的理解是"每个人都有相同数据的单一共享副本,直到它被写入,然后制作副本&#34 34。

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

2 个答案:

答案 0 :(得分:7)

操作系统可以设置任何"复制写入"它希望的政策,但一般来说,它们都做同样的事情(即最有意义的事情)。

对于类似POSIX的系统(linux,BSD,OSX)来说,有四个区域(你称之为段):dataint x = 1;去哪里),{{ 1}}(其中bss去),int y(这是堆/ malloc)和sbrk

stack完成后,操作系统会为共享父项所有页面的子项设置新的页面映射。然后,在父子页面的页面映射中,所有页面都标记为只读。

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

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

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

实际上,fork部分甚至可以获得更多特殊待遇。在它的初始页面映射中,所有页面都映射到包含全零的单个页面(也就是"零页面")。映射标记为R / O.因此,bss区域的大小可能是千兆字节,它只占用一个物理页面。这个单一的,特殊的零页面在所有进程的所有 bss部分之间共享,无论他们是否与任何关系另一个。

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

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

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

虽然不是原始问题的一部分,但可能会引起相关活动,例如按需加载 [页面]和按需链接 [在fork系统调用之后的符号]。

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

对于ELF,可执行文件有一个特殊的部分,它提供了一个完整的FS路径,称为" ELF解释器",它是一个共享库,通常是{{1} }。

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

在我们走得更远之前,我们需要谈谈"支持商店"对于一个页面。也就是说,如果发生页面错误,我们需要从磁盘加载页面,它来自哪里。对于heap / malloc,这通常是交换磁盘[aka paging disk]。

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

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

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

ELF口译员开展业务。每次它试图执行自己的一部分[a" code" 映射加载,发生页面错误以及从后备存储页面加载的负载(例如ELF解释器文件)并更改了映射到R / O但存在

对于ELF解释器,共享库和可执行文件本身会发生这种情况。

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

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

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

但是,ELF解释器重新定位大多数其他共享库的所有符号附近的任何位置。它查看可执行文件,并再次使用data为可执行文件所需的共享库创建映射(即您在执行时所看到的内容{{ 1}})。

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

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

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

这称为"按需链接"。

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

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

因此,总而言之,我们已经"复制写入","按需加载"和"按需链接"一切都在同一时间进行。这似乎很复杂,但让mmapfree快速,顺利地进行。这增加了一些复杂性,但只有在需要时才会产生额外的开销("按需提供")。

因此,在程序开始启动时,开销活动会在程序的生命周期内根据需要分散,而不是大量的延迟/延迟。

答案 1 :(得分:1)

为了更好地理解,您应该从词汇表中删除术语 segment 。大多数系统在页面上工作;不是段。在64位英特尔领域终于消失了。

您应该问,"写入时副本会影响哪些页面。"

当一个进程写入时,这将是可由多个进程写入和共享的页面。

这可能发生在分叉后。实现分叉的一种方法是创建父进程的地址空间的完整副本。然而,这可能需要付出很多努力,特别是因为大部分时间都是在叉子之后对孩子做一个执行。

另一种方法是让父母和孩子共享相同的记忆。这适用于只读内存,但如果多个进程可以写入同一内​​存,则会出现明显的问题。

这可以通过让进程对读/写内存充电直到进程写入它来解决。在这种情况下,该页面被写入过程取消共享,操作系统会分配一个新的页面框架,将其映射到地址空间,将原始数据复制到该页面,然后允许写入过程继续。