是否有可能通过mmap的匿名内存“打洞”?

考虑一个程序,它使用大量的大概页面大小的内存区域(比如说64 kB左右),每个内存区域都很短暂。 (在我的情况下,这些是绿线的备用堆栈。)

一个最好的做法是分配这些区域,这样一旦这个区域不再被使用,他们的页面就可以返回到内核中了。 天真的解决scheme显然是简单地将每个区域分别munmap ,然后在完成后再将它们重新munmap 。 不过,我觉得这是一个坏主意,因为有这么多。 我怀疑VMM可能会在一段时间后开始严重缩放; 但即使没有,我仍然对这个理论案例感兴趣。

如果我只是把自己映射成一个巨大的匿名映射,我可以根据需要分配这些区域,是否有办法通过映射为我所完成的区域“打洞”? 有点像madvise(MADV_DONTNEED) ,但不同之处在于页面应该被认为是被删除的,这样内核实际上并不需要把它们的内容保留在任何地方,只要再次发生故障就可以重用零页面。

我正在使用Linux,在这种情况下我不会使用特定于Linux的调用。

我不明白为什么大量调用mmap / munmap应该是不好的。 内核中映射的查找性能应该是O(log n)。

你现在唯一的选择,就好像在Linux中实现的那样,是在映射中打洞,做你想要的是mprotect(PROT_NONE) ,它仍然在内核中分割映射,所以它大致等同于mmap / munmap除了别的什么东西都不能偷你的VM范围。 你可能想要madvise(MADV_REMOVE)工作,或者在BSD中调用madvise(MADV_FREE) 。 这是明确的设计,做你想要的东西 – 最便宜的方式来回收页面而不分割映射。 但至少根据我的两种Linux版本的手册页,并没有完全适用于各种映射。

免责声明:我大多熟悉BSD VM系统的内部,但在Linux上这应该是非常相似的。

正如在下面的评论中的讨论,令人惊讶的是, MADV_DONTNEED似乎有诀窍:

 #include <sys/types.h> #include <sys/mman.h> #include <sys/time.h> #include <sys/resource.h> #include <stdio.h> #include <unistd.h> #include <err.h> int main(int argc, char **argv) { int ps = getpagesize(); struct rusage ru = {0}; char *map; int n = 15; int i; if ((map = mmap(NULL, ps * n, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0)) == MAP_FAILED) err(1, "mmap"); for (i = 0; i < n; i++) { map[ps * i] = i + 10; } printf("unnecessary printf to fault stuff in: %d %ld\n", map[0], ru.ru_minflt); /* Unnecessary call to madvise to fault in that part of libc. */ if (madvise(&map[ps], ps, MADV_NORMAL) == -1) err(1, "madvise"); if (getrusage(RUSAGE_SELF, &ru) == -1) err(1, "getrusage"); printf("after MADV_NORMAL, before touching pages: %d %ld\n", map[0], ru.ru_minflt); for (i = 0; i < n; i++) { map[ps * i] = i + 10; } if (getrusage(RUSAGE_SELF, &ru) == -1) err(1, "getrusage"); printf("after MADV_NORMAL, after touching pages: %d %ld\n", map[0], ru.ru_minflt); if (madvise(map, ps * n, MADV_DONTNEED) == -1) err(1, "madvise"); if (getrusage(RUSAGE_SELF, &ru) == -1) err(1, "getrusage"); printf("after MADV_DONTNEED, before touching pages: %d %ld\n", map[0], ru.ru_minflt); for (i = 0; i < n; i++) { map[ps * i] = i + 10; } if (getrusage(RUSAGE_SELF, &ru) == -1) err(1, "getrusage"); printf("after MADV_DONTNEED, after touching pages: %d %ld\n", map[0], ru.ru_minflt); return 0; } 

我测量ru_minflt作为一个代理,看看我们需要分配多少页(这不完全正确,但下一句更有可能)。 我们可以看到,我们在第三个printf中得到新的页面,因为map[0]的内容是0。

我在某个时候对这个主题做了大量的研究(用于不同的用途)。 在我的情况下,我需要一个非常稀疏的大散列图+能够零时差。

mmap解决方案

最简单的解决方案(这是便携式, madvise(MADV_DONTNEED)是特定于Linux)以零映射像这样映射到一个新的映射。

  void * mapping = mmap(MAP_ANONYMOUS); // use the mapping // zero certain pages mmap(mapping + page_aligned_offset, length, MAP_FIXED | MAP_ANONYMOUS); 

最后一次调用与后续的munmap/mmap/MAP_FIXED相当,但是线程安全。

性能方面,这个解决方案的问题在于页面必须在发生中断和上下文改变的子序列写入访问中再次出错。 这只有在很少页面出现故障时才有效。

memset解决方案:

如果大部分映射必须被取消映射,那么在这样的垃圾性能之后,我决定用memset手动调整内存。 如果大约70%以上的页面已经发生错误(如果不是,那么它们是在第一轮memset ),那么这会更快,然后重新映射这些页面。

mincore解决方案:

我的下一个想法实际上只是在那些以前出现故障的页面上memset 。 这个解决方案不是线程安全的 。 调用mincore来确定一个页面是否出现故障,然后有选择地将它们memset为零是一个显着的性能改进,直到超过50%的映射出现故障,此时整个映射的memset变得更简单了( mincore是一个系统调用,需要一个上下文更改)。

incore表解决方案:

然后,我采取的最后一个方法是拥有自己的核心表(每页一位),说明自上次擦除后是否使用了该表。 这是迄今为止最有效的方法,因为实际上每次只能实际调零页面。 它显然也不是线程安全的,并且需要你跟踪哪些页面已经被写入到用户空间中,但是如果你需要这个性能,那么这是迄今为止最有效的方法。