我想验证当我有两个不同的可执行文件依赖于同一个动态加载库并且该库同时具有“状态”(即每个进程应该特定的东西)和常量数据(即,可以分享的东西)。
我设计了以下非常简单的实验:
main.cpp:
#include <iostream>
#include <thread>
#include <chrono>
#include "mylib.h"
int main()
{
MyLib lib;
while (true)
{
lib.printString();
std::this_thread::sleep_for(std::chrono::seconds(5));
}
return 0;
}
lib.h:
#ifndef MYLIB_H
#define MYLIB_H
class MyLib
{
public:
void printString();
private:
int counter = 0;
};
#endif // MYLIB_H
lib.cpp:
#include "mylib.h"
#include <iostream>
// Define the large string in the implementation file
constexpr const char *largeString =
"This is a very large string that will be used to fill up the .text section "
"of the shared library. It needs to be big enough to be noticeable........................................................ ";
void MyLib::printString()
{
std::cout << largeString << std::endl;
std::cout << "Counter: " << ++counter << std::endl;
}
我使用以下命令编译了源代码:
g++ -fPIC -shared -o libmylib.dylib my lib.cpp -std=c++11
g++ -o main main.cpp -L. -lmylib -pthread
最后我执行了两次main。
首先,我在两个进程上使用 vmmap 来查看实际上存在与我的 dylib 相关的不可写区域(可能是相同的)。我得到以下信息(对于第一个过程):
__TEXT 102ec4000-102ec8000 [ 16K 16K 0K 0K] r-x/rwx SM=COW ***/libmylib.dylib
__DATA_CONST 102ec8000-102ecc000 [ 16K 16K 0K 0K] r--/rw- SM=COW ***/libmylib.dylib
__LINKEDIT 102ecc000-102ed0000 [ 16K 16K 0K 0K] r--/rwx SM=COW ***/libmylib.dylib
这些区域不同,但据我了解,这些区域表示虚拟内存,因此,它们仍然映射到相同的物理内存。
我继续使用 dtruss 检查 dylib 是如何加载的,我看到了以下相关部分:
8740/0x15967: 1207 12 11 open("libmylib.dylib\0", 0x0, 0x0) = 3 0
8740/0x15967: 1208 1 0 fcntl(0x3, 0x32, 0x16BCDDFB8) = 0 0
8740/0x15967: 1208 0 0 close(0x3) = 0 0
8740/0x15967: 1213 2 1 stat64("libmylib.dylib\0", 0x16BCDDB10, 0x0) = 0 0
8740/0x15967: 1215 1 1 stat64("libmylib.dylib\0", 0x16BCDD540, 0x0) = 0 0
8740/0x15967: 1221 6 5 open("libmylib.dylib\0", 0x0, 0x0) = 3 0
8740/0x15967: 1224 3 2 mmap(0x0, 0x9AB0, 0x1, 0x40002, 0x3, 0x0) = 0x104574000 0
8740/0x15967: 1224 1 0 fcntl(0x3, 0x32, 0x16BCDD658) = 0 0
8740/0x15967: 1225 0 0 close(0x3) = 0 0
8740/0x15967: 1235 6 5 open("/Users/***/memoryFootprint/libmylib.dylib\0", 0x0, 0x0) = 3 0
8740/0x15967: 1235 1 0 fcntl(0x3, 0x61, 0x16BCDD308) = 0 0
8740/0x15967: 1243 8 7 fcntl(0x3, 0x62, 0x16BCDD308) = 0 0
8740/0x15967: 1254 10 9 mmap(0x104580000, 0x4000, 0x5, 0x40012, 0x3, 0x0) = 0x104580000 0
8740/0x15967: 1257 2 1 mmap(0x104584000, 0x4000, 0x3, 0x40012, 0x3, 0x4000) = 0x104584000 0
8740/0x15967: 1258 1 0 mmap(0x104588000, 0x4000, 0x1, 0x40012, 0x3, 0x8000) = 0x104588000 0
8740/0x15967: 1259 0 0 close(0x3) = 0 0
and a bit further:
8740/0x15967: 1802 6 5 open("/Users/***/memoryFootprint/libmylib.dylib\0", 0x0, 0x0) = 3 0
8740/0x15967: 1803 1 0 __mac_syscall(0x19373BFDD, 0x2, 0x16BCDCE10) = 0 0
8740/0x15967: 1808 5 3 map_with_linking_np(0x16BCDCC90, 0x1, 0x16BCDCCC0) = 0 0
8740/0x15967: 1808 0 0 close(0x3) = 0 0
8740/0x15967: 1809 1 0 mprotect(0x104584000, 0x4000, 0x1) = 0 0
8740/0x15967: 1830 1 0 shared_region_check_np(0xFFFFFFFFFFFFFFFF, 0x0, 0x0) = 0 0
8740/0x15967: 1832 1 0 mprotect(0x104530000, 0x40000, 0x1) = 0 0
8740/0x15967: 1845 2 1 access("/AppleInternal/XBS/.isChrooted\0", 0x0, 0x0) = -1 Err#2
8740/0x15967: 1862 3 1 bsdthread_register(0x193A3DD2C, 0x193A3DD20, 0x4000) = 1073746399 0
8740/0x15967: 1884 1 0 getpid(0x0, 0x0, 0x0) = 8740 0
8740/0x15967: 1888 3 2 shm_open(0x1938D8F51, 0x0, 0x2F62696C) = 3 0
8740/0x15967: 1890 1 0 fstat64(0x3, 0x16BCDDAB0, 0x0) = 0 0
8740/0x15967: 1893 4 2 mmap(0x0, 0x4000, 0x1, 0x40001, 0x3, 0x0) = 0x10457C000 0
8740/0x15967: 1894 0 0 close(0x3) = 0 0
据我所知,所有与 dylib 相关的映射(mmap 调用)都是私有的。只有一个映射是共享的,但我无法判断它是否相关。
我的预期结果是看到两个进程之间共享了部分 dylib,但这不是我从上述实验中理解的。
是否发生了只读部分的共享?
对于大多数具有全局文件系统页面缓存的操作系统,所有最初加载的支持 mmap 库的物理页面都应该共享。 对于只读段(只读数据或可执行段),它们应该在映射的整个生命周期内继续共享。 对于可写部分,数据可能最初是共享的,但随后当拥有进程实际写入页面时进行私有复制(从而进行其他共享者不可见的更改)。 请注意,零填充区域通常是匿名分配的,而不是从文件映射(该库只是说“N 字节的零位于此处”,并且通常不包含 N 字节的零)。
我相信大多数动态链接器的工作原理大致如下,给定需要加载的共享库映像的路径:读取标头以了解共享库二进制文件的布局(即 ELF 标头列出了“部分”以及这些部分如何应映射到地址空间)。 然后对于每个部分,
mmap
将文件支持的部分放入地址空间(每个地址空间的虚拟地址可能不同)。 有些部分是只读的,有些是可执行的,有些是可写的。 零支持部分可能是匿名 mmap 区域。
确保该内存在进程之间共享并不是链接器真正的工作。 操作系统有机会在进程之间共享页面,因为它可能可以看到同一个文件被映射到多个地址空间,因此可以使用内存中的缓存文件内容来共享物理页面。 它可以轻松共享只读页面,并且读写页面最初可以共享(通过写时复制支持)。 请参阅
确定进程“真实”内存使用情况(即私有脏 RSS)的方法?了解有关深入虚拟地址空间并了解支持地址空间的页面所有权差异的一些提示(TL; DR sudo vmmap
在 Mac 上,也许
cat /proc/<pid>/maps
在 Linux 上?)。