是否可以通过mmap的匿名内存“打孔”?

时间:2014-02-12 08:35:22

标签: c linux mmap

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

如何最好地分配这些区域,以便一旦该区域不再使用,它​​们的页面可以返回到内核?天真的解决方案显然是单独mmap每个区域,并且一旦我完成它们就再次munmap。不过,我觉得这是一个坏主意,因为它们有很多。我怀疑VMM可能会在一段时间后开始严重缩放;但即使没有,我仍然对理论案例感兴趣。

如果我只是mmap我自己一个巨大的匿名映射,我可以根据需要分配区域,那么有没有办法通过映射为我完成的区域打孔?有点像madvise(MADV_DONTNEED),但区别在于应该将页面视为已删除,以便内核实际上不需要将其内容保留在任何位置,但只要再次出现故障就可以重复使用归零页面。

我正在使用Linux,在这种情况下,我不会因为使用特定于Linux的电话而烦恼。

2 个答案:

答案 0 :(得分:5)

我在某些方面对这个主题进行了大量研究(针对不同的用途)。在我的情况下,我需要一个非常稀疏填充的大型hashmap +偶尔将其归零的能力。

mmap解决方案

最简单的解决方案(可移植,madvise(MADV_DONTNEED)是特定于Linux的)将这样的映射归零,以mmap上面的新映射。

 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是一个系统调用,需要一个上下文更改)。

更多表解决方案:

我接下来的最后一个方法就是拥有自己的核心表(每页一位),说明自上次擦除后是否已经使用过。这是迄今为止最有效的方法,因为您实际上只会将实际使用的每一轮中的页面归零。它显然也不是线程安全的,并且要求您跟踪在用户空间中写入的页面,但如果您需要此性能,那么这是迄今为止最有效的方法。

答案 1 :(得分:3)

我不明白为什么要对mmap / munmap进行大量调用应该是那么糟糕。内核中用于映射的查找性能应为O(log n)。

现在似乎在Linux中实现的唯一选择是在映射中打孔以执行您想要的mprotect(PROT_NONE)并且仍在对内核中的映射进行分段,因此它大部分等同于{ {1}} / mmap,除了其他东西无法从您那里窃取VM范围。你可能希望munmap工作或者在BSD中调用 - madvise(MADV_REMOVE)。这明确地设计为完全按照您的意愿执行 - 在不分割映射的情况下回收页面的最便宜方式。但至少根据我的两种Linux版本的手册页,它并没有完全实现各种映射。

免责声明:我对BSD VM系统的内部结构非常熟悉,但在Linux上应该非常相似。

正如下面评论中的讨论一样,令人惊讶的是madvise(MADV_FREE)似乎可以解决问题:

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; } 作为代理,以查看我们需要分配多少页面(这不完全正确,但下一句话更有可能)。我们可以看到我们在第三个printf中获得了新页面,因为ru_minflt的内容是0。