考虑一个使用大量页面大小的内存区域(比如64 kB左右)的程序,每个区域都是相当短暂的。 (在我的特定情况下,这些是绿色线程的备用堆栈。)
如何最好地分配这些区域,以便一旦该区域不再使用,它们的页面可以返回到内核?天真的解决方案显然是单独mmap
每个区域,并且一旦我完成它们就再次munmap
。不过,我觉得这是一个坏主意,因为它们有很多。我怀疑VMM可能会在一段时间后开始严重缩放;但即使没有,我仍然对理论案例感兴趣。
如果我只是mmap
我自己一个巨大的匿名映射,我可以根据需要分配区域,那么有没有办法通过映射为我完成的区域打孔?有点像madvise(MADV_DONTNEED)
,但区别在于应该将页面视为已删除,以便内核实际上不需要将其内容保留在任何位置,但只要再次出现故障就可以重复使用归零页面。
我正在使用Linux,在这种情况下,我不会因为使用特定于Linux的电话而烦恼。
答案 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。