有关Linux中匿名映射内存的问题
我正在玩弄使用虚拟内存系统的想法,允许我对一些数字数据进行透明数据转换(例如,int来浮动)。 其基本思想是,我正在编写mmaps数据文件库,并同时映射适当大小的匿名区域来存放转换后的数据,并将此指针返回给用户。
匿名区域是读/写保护的,这样每当用户通过指针访问数据时,每个新页面都会导致段错误,我可以捕获该错误,然后透明地转换mmaped文件中的数据并修复允许获得继续。 整个事情的这一部分到目前为止效果很好。
但是,有时我会映射非常大的文件(数百GB),并且通过匿名内存代理访问它,很快您将开始吃掉交换空间,因为匿名页面将丢弃到磁盘。 我的想法是,如果在将转换后的数据写入匿名页面后,我可以明确地将匿名页面上的脏位设置为false,那么操作系统将会放弃它们,并在稍后重新访问时按需填零。
为此,我认为我必须将脏位设置为false,并说服操作系统设置页面在被换出时被读保护,以便重新捕获后续的段错误并重新转换数据一经请求。 在做了一些研究之后,我认为没有内核黑客攻击是不可能的,但我想我会问,看看是否有人对虚拟内存系统有更多了解,知道这可能会实现。
以下是一个想法(完全未经测试):对于转换后的数据,根据需要使用mmap
和munmap
单个页面。 由于页面由匿名内存支持,因此它们在未映射时应该丢弃。 Linux将把相邻的映射合并成一个VMA,所以这可能具有可接受的开销。
当然,需要有一种机制来触发解映射。 你可以维护一个LRU结构,当你需要带一个新的页面的时候赶走一个旧页面,从而保持映射区域的大小不变。
根据我之前提到的相关问题中提到的建议,我认为以下(特定于Linux的,绝对不可移植的)方案应该可靠地工作:
使用socketpair(AF_UNIX, SOCK_DGRAM, 0, &sv)
和SIGSEGV
信号处理程序设置数据报套接字对。 (即使其他进程可能会截断数据文件,您也不需要担心SIGBUS
。)
信号处理程序使用write()
来写入size_t addr = siginfo->si_addr;
到它的插座的末端。 然后信号处理程序从它写入的套接字read()
一个字节(阻塞 - 这基本上只是一个可靠的sleep()
- 因此请记住处理EINTR
),然后返回。
请注意,即使在同一时间或接近同一时间存在多个线程错误,也没有竞争条件。 这些信号只需要重新映射,直到映射被修复。
如果套接字通信出现任何问题,可以使用sigaction()
和.sa_handler = SIG_DFL
来恢复默认的SIGSEGV
信号处理程序,这样当同一个信号被重新整理时,整个程序就像平常一样死亡。
一个单独的线程读取套接字对的另一端以查找有SIGSEGV
错误的地址,是否需要所有的映射和文件I / O,最后将零字节写入套接字对的同一端以让真正的信号处理程序知道映射现在应该是固定的。
这基本上是“真正的”信号处理程序,没有实际信号处理程序的缺点。 请记住,同一个线程将继续重新绘制相同的信号,直到映射被修复,因此单独的线程和SIGSEGV
信号之间的任何竞态条件都是不相关的。
有一个PROT_NONE
, MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE
MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE
MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE
映射匹配原始数据文件的大小。
为了降低实际RAM中的成本 - 使用MAP_NORESERVE
您既不使用RAM也不使用SWAP进行映射,但对于千兆字节的数据,页表项本身需要大量的RAM - 您也可以尝试使用MAP_HUGETLB
。 它会使用巨大的页面,因此显着减少条目数量,但是我不确定当正常的页面大小的空洞最终被打入映射时是否存在问题; 你可能不得不一直使用巨大的页面。
这是“用户空间”将用于访问数据的“完整”映射。
有一个PROT_READ
或PROT_READ | PROT_WRITE
PROT_READ | PROT_WRITE
, MAP_PRIVATE | MAP_ANONYMOUS
MAP_PRIVATE | MAP_ANONYMOUS
映射用于原始或肮脏(分别)转换后的数据。 如果您的“用户空间”几乎总是修改数据,则始终可以将转换后的数据视为“脏”,但除此之外,您可以通过首先映射转换后的数据PROT_READ
来避免不必要的未修改数据写入; 如果它出错, mprotect()
它PROT_READ | PROT_WRITE
PROT_READ | PROT_WRITE
并将其标记为脏(所以需要转换并保存回文件)。 我将分别称这两个阶段为“干净”和“脏”映射。
当专用线程将一个洞打入“干净”页面的“完整”映射时,首先将mmap(NULL, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, ...)
一个合适大小的新内存区域read()
将数据从所需的数据文件导入数据文件,转换数据, mprotect(..., PROT_READ)
如果分开“干净”和“脏”映射,最后mremap(newly_mapped, size, size, MREMAP_MAYMOVE | MREMAP_FIXED, new_ptr)
遍历“完整”映射的部分。
请注意,为了避免任何意外,您应该使用全局pthread_mutex_t
,该函数在这些mremap()
和其他任何mmap()
调用的持续时间内被抓取,以避免内核意外地将错误的线程mremap()
给错误的线程。 互斥体将防止任何其他线程进入。 (否则,内核可能会将另一个线程所请求的小地图放入临时洞中。)
当抛弃“clean”页面时,你可以调用mmap(NULL, length, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE, -1, 0)
来获得一个合适长度的新映射,然后获取上面提到的全局互斥量,以及mremap()
通过“干净”页面的新地图; 内核做了一个隐式的munmap()
。 解锁互斥锁。
当放弃“脏”页面时,您可以调用mmap(NULL, length, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE, -1, 0)
*两次以获得两个长度合适的新地图*。 然后抓取上面提到的全局互斥体,并在第一个新映射中对肮脏数据进行mremap()
。 (基本上它只是用来找出合适的地址来移动脏数据。)然后, mremap()
第二个新映射到脏数据所在的位置。解锁互斥量。
使用单独的线程处理故障状况可避免所有异步信号安全功能问题。 read()
, write()
和sigaction()
都是异步信号安全的。
您只需要一个全局pthread_mutex_t
来避免内核将最近移动的空洞( mremap()
ped从内存区域) mremap()
给另一个线程; 您也可以使用它来保护您的内部数据结构(如果支持多个并发文件映射,则为指针链)。
应该没有竞争条件(除了其他线程使用由上述互斥体处理的mmap()
或mremap()
)。 当一个“脏”页面或页面组被移走时,其他线程无法访问它,然后它被转换并保存; 即使完全并发访问另一个线程应该完美处理:页面将被简单地从文件重新读取,并重新转换。 (如果这种情况经常发生,您可能希望缓存最近保存的页面组。)
我建议使用大页面组,比如2M或更多,而不是单页,以减少开销。 最佳大小取决于您的应用程序访问模式,但巨大的页面大小(如果您的架构支持)是一个非常好的起点。
如果您的数据结构不与页面或页面组对齐,则应缓存完整转换的第一个和最后一个记录(仅部分位于页面或页面组内)。 它通常使转换回存储格式变得更容易。
如果您知道或可以检测到文件中的典型访问模式,则可能应该使用posix_fadvise()
来告诉内核; POSIX_FADV_WILLNEED
和POSIX_FADV_DONTNEED
是最有用的。 它有助于内核避免在页面缓存中保留实际数据文件的不必要页面。
最后,您可能会考虑添加第二个专用线程,用于将脏记录异步转换回磁盘。 如果你注意确保两个线程在第一个线程想要重新读回第二个线程仍在写入磁盘的记录时不会感到困惑,那么应该没有其他问题 - 但是异步写入很可能会增加您的吞吐量和大多数访问模式,除非您无论如何都是I / O绑定,或者相对而言RAM非常短。
为什么使用read()
和write()
而不是另一个内存映射? 由于所需虚拟内存结构的内核开销。
上一篇: Questions about anonymous mapped memory in linux
下一篇: Do TCP sockets automatically close after some time if no data is sent?