扩展我在您之前的相关问题中提到的建议,我认为以下(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_NONE
,MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE
映射匹配原始数据文件的大小。
为了降低实际 RAM 的成本——使用MAP_NORESERVE
你既不使用 RAM 也不使用 SWAP 进行映射,但对于千兆字节的数据,页表条目本身需要相当大的 RAM——你也可以尝试使用MAP_HUGETLB
。它会使用大页面,因此条目会显着减少,但我不确定当正常页面大小的孔最终被打入映射时是否存在问题;你可能不得不一直使用大页面。
这是您的“用户空间”将用于访问数据的“完整”映射。
有一个PROT_READ
or PROT_READ | PROT_WRITE
,MAP_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()
而不是另一个内存映射?由于所需的虚拟内存结构的内核开销。