Linux 共享库加载并与其他进程共享代码

Linux shared library loading and sharing the code with other process

假设我有一个共享库a.so,它是我的可执行文件第一次加载的。我的理解是,到 VMA 的中间,共享库文本部分被映射。我有两个问题;

(1) ld.so 是否要将此共享内存文本部分页面加载到物理内存,然后映射到该进程的 VMA?

(2) 假设启动了第二个可执行文件,它使用相同的共享库 a.so。 ld.so是要识别这个共享库已经加载到物理内存了吗?如何理解这一点?

准确地说,保留物理内存或管理或选择虚拟内存和物理内存之间的映射不是ld.so的工作,而是内核的工作。当 ld.so 加载共享库时,它通过 mmap 系统调用执行此操作,内核分配所需的物理内存 (1) 并在两者之间创建虚拟映射库文件和物理内存。 mmap 返回的是映射库的虚拟基地址,动态加载程序随后将使用该地址作为基础来为该库的函数调用提供服务。

Is ld.so going to identify that this shared library is already loaded to the physical memory? How does it work to understand that?

不是ld.so,而是内核会识别这个。这是一个复杂的过程,但为了简单起见,内核会跟踪哪个文件被映射到哪里,并且可以检测何时发出再次映射已映射文件的请求,尽可能避免分配物理内存。

如果同一个文件(即具有相同路径的文件)被多次映射,内核将查看现有的映射,并且如果可能它将重用相同的物理页面以避免浪费内存。所以理想情况下,如果一个共享库被多次加载,它可能只被物理分配一次。

但实际上并没有那么简单。由于内存也可以被写入,所以这种 "sharing" 的物理页面显然只有在需要共享的页面与文件的原始内容没有变化的情况下才会发生(否则映射相同文件或库的不同进程会干扰彼此)。这对于代码部分 (.text) 基本上总是正确的,因为它们通常是只读的,对于其他类似部分(如只读数据)也是如此。如果未修改 RW 部分,也会发生这种情况 (2)。所以简而言之,已经加载的库的 .text 段通常只分配到物理内存中一次。


(1) 实际上,内核首先创建映射,然后仅当进程试图通过映射读取或写入时才分配物理内存。这样可以防止在不需要时浪费内存。

(2) 这种共享物理内存的技术通过 copy-on-write 机制进行管理,其中内核最初映射 "clean" 页并在写入时将它们标记为 "dirty",根据需要复制它们。

Linux 共享库通常与位置无关(由于地址 space 随机化,可执行文件也是如此)。位置无关代码使用 GOT 和 PLT 重定位机制在 运行 时间定位所有具有外部链接的符号。

当动态链接器ld.so加载一个位置独立的可执行文件或共享库时,只有GOT和PLT段必须在进程内存中修改,.text带有代码的段被映射到进程内存中作为读-只要。当另一个进程加载相同的可执行文件或共享库时,它最终会映射相同的页框,其中 .text 段已被其他进程加载,但不是特定于进程地址 space 布局的 GOT 和 PLT .

ld.so 在加载 ELF 文件时本质上是 mmap(NULL, text_segment_len, PROT_EXEC | PROT_READ, MAP_SHARED, elf_file_fd, text_segment_offset)。这会填充内核 page cache(在第一次访问时),以便进行此调用的另一个进程映射页面缓存中已经存在的页面框架。

有关详细信息,请参阅 Position-independent code