7

我正在尝试使用虚拟内存系统来允许我对我拥有的一些数字数据进行透明的数据转换(例如 int 到 float)。基本思路是我写的库mmaps你想要的数据文件,同时mmaps一个合适大小的匿名区域来保存转换后的数据,这个指针返回给用户。

匿名区域是读/写保护的,所以每当用户通过指针访问数据时,每个新页面都会导致一个段错误,我可以捕获它,然后透明地转换 mmaped 文件中的数据并修复允许的权限访问以继续。到目前为止,整个事情的这一部分效果很好。

但是,有时我映射非常大的文件(数百 GB),并且通过匿名内存代理访问它,很快你就会开始消耗交换空间,因为匿名页面被放到磁盘上。我的想法是,如果我可以在将转换后的数据写入匿名页面后将其显式设置为 false,则操作系统只会删除它们并在稍后重新访问它们时按需填零。

不过,要使其正常工作,我认为我必须将脏位设置为 false说服操作系统在页面被换出时将页面设置为读保护,这样我就可以重新捕获随后的段错误并重新转换数据一经请求。在做了一些研究之后,我认为如果没有内核黑客,这是不可能的,但我想我会问一下,看看是否有人对虚拟内存系统了解更多,知道一种可以实现的方法。

4

2 回答 2

2

这是一个想法(尽管完全未经测试):对于转换后的数据,mmap以及munmap您需要的各个页面。由于页面由匿名内存支持,因此在未映射时应将其丢弃。Linux 会将相邻的映射合并到单个 VMA 中,因此这可能具有可接受的开销。

当然,需要有一种机制来触发取消映射。您可以维护一个 LRU 结构并在需要引入新页面时逐出旧页面,从而保持映射区域的大小不变。

于 2012-07-29T17:48:13.767 回答
2

扩展我在您之前的相关问题中提到的建议,我认为以下(Linux 特定,绝对不可移植)方案应该非常可靠地工作:

  • socketpair(AF_UNIX, SOCK_DGRAM, 0, &sv)使用和 的信号处理程序设置数据报套接字对SIGSEGV。(您无需担心SIGBUS,即使其他进程可能会截断数据文件。)

  • 信号处理程序用于write()将其写入size_t addr = siginfo->si_addr;套接字的末尾。然后信号处理程序read()从它写入的套接字中取出一个字节(阻塞——这基本上只是一个可靠的sleep()——所以记住要处理EINTR),然后返回。

    请注意,即使有多个线程同时或几乎同时出现故障,也没有竞争条件。在映射修复之前,信号只会被重新提升。

    如果套接字通信出现任何问题,您可以使用sigaction()with.sa_handler = SIG_DFL来恢复默认SIGSEGV信号处理程序,这样当重新引发相同的信号时,整个进程都会正常终止。

  • 一个单独的线程读取套接字对的另一端以查找出错的地址SIGSEGV,执行所有必要的映射和文件 I/O,最后将零字节写入套接字对的同一端,让真正的信号处理程序知道映射现在应该修复。

    这基本上是“真正的”信号处理程序,没有实际信号处理程序的缺点。请记住,同一个线程将不断地重新引发同一个信号,直到映射被修复,所以单独的线程和SIGSEGV信号之间的任何竞争条件都是无关紧要的。

  • 有一个PROT_NONEMAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE映射匹配原始数据文件的大小。

    为了降低实际 RAM 的成本——使用MAP_NORESERVE你既不使用 RAM 也不使用 SWAP 进行映射,但对于千兆字节的数据,页表条目本身需要相当大的 RAM——你也可以尝试使用MAP_HUGETLB。它会使用大页面,因此条目会显着减少,但我不确定当正常页面大小的孔最终被打入映射时是否存在问题;你可能不得不一直使用大页面。

    这是您的“用户空间”将用于访问数据的“完整”映射。

  • 有一个PROT_READor PROT_READ | PROT_WRITEMAP_PRIVATE | MAP_ANONYMOUS映射原始或脏(分别)转换的数据。如果您的“用户空间”几乎总是修改数据,则您始终可以将转换后的数据视为“脏数据”,否则您可以通过首先PROT_READ仅映射转换后的数据来避免不必要的未修改数据写入;如果出现故障,mprotect()则将其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)是“完整”映射部分。

    请注意,为避免任何意外,您应该使用global pthread_mutex_t,在这些mremap()s 和mmap()其他地方的任何调用期间抓取它,以避免内核意外地将打孔给错误的线程。互斥锁将防止任何其他线程进入。(否则,内核可能会将另一个线程请求的小映射放入临时孔中。)

  • 当丢弃“干净”页面时,您调用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)交给另一个线程的情况;您还可以使用它来保护您的内部数据结构(指针链,如果您支持多个并发文件映射)。

不应该有竞争条件(除了其他线程使用mmap()or mremap(),由上面提到的互斥锁处理)。当一个“脏”页面或页面组被移走时,在转换和保存之前,其他线程将无法访问它;即使是另一个线程的完美并发访问也应该得到完美处理:页面将简单地从文件中重新读取,并重新转换。(如果这种情况经常发生,您可能希望缓存最近保存的页组。)

我确实建议使用大页面组,比如 2M 或更多,而不是单页,以减少开销。最佳大小取决于您的应用程序访问模式,但巨大的页面大小(如果您的架构支持)是一个非常好的起点。

如果您的数据结构未与页面或页面组对齐,则应缓存完整转换的第一条和最后一条记录(仅部分位于页面或页面组内)。它通常使转换回存储格式更容易。

如果您知道或可以检测到文件中的典型访问模式,您可能应该使用posix_fadvise()来告诉内核;POSIX_FADV_WILLNEED并且POSIX_FADV_DONTNEED最有用。它有助于内核避免在页面缓存中保留实际数据文件的不必要页面。

最后,您可能会考虑添加第二个特殊线程,用于异步转换脏记录并将其写回磁盘。如果您注意确保当第一个线程想要重新读回仍然由第二个线程写入磁盘的记录时两个线程不会混淆,那么那里也不应该有其他问题——但是异步写入大多数访问模式可能会增加您的吞吐量,除非您无论如何都受 I/O 限制,或者 RAM 真的很短(相对而言)。

为什么使用read()andwrite()而不是另一个内存映射?由于所需的虚拟内存结构的内核开销。

于 2012-07-31T01:02:00.273 回答