c语言 - 是否可以通过 mmap'ed 匿名内存"punch holes"?



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

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

如果我自己只是mmap一个巨大的匿名映射,我可以根据需要分配区域,那么有没有办法为我处理完的区域在映射中"打孔"呢?有点像madvise(MADV_DONTNEED),但不同的是,页面应该被视为已删除,这样内核实际上不需要将其内容保存在任何地方,而是可以在页面再次出现故障时重用零页面。

我使用的是Linux,在这种情况下,我不会因为使用Linux特定的调用而烦恼。

我在某个时候对这个主题做了很多研究(用于不同的用途)。在我的情况下,我需要一个人烟稀少的大哈希图+不时将其清零的能力。

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

堆芯表解决方案:

然后我采取的最后一种方法是有自己的核心表(每页一位),上面写着自上次擦除以来是否使用过它。这是迄今为止最有效的方法,因为你只会在每一轮实际使用的页面中归零。很明显,它也不是线程安全的,需要您跟踪在用户空间中写入了哪些页面,但如果您需要这种性能,那么这是迄今为止最有效的方法。

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

目前在Linux中实现的唯一选项是在映射中打孔以执行您想要的操作,即mprotect(PROT_NONE),它仍然在内核中对映射进行分段,因此它基本上等效于mmap/munmap,只是其他东西无法从您那里窃取VM范围。您可能想要madvise(MADV_REMOVE)工作,或者像BSD-madvise(MADV_FREE)中所说的那样。它被明确设计为完全按照您的意愿进行操作——这是在不分割映射的情况下回收页面的最便宜的方法。但至少根据我的两种Linux版本的手册页,它并没有完全实现用于所有类型的映射。

免责声明:我对BSD虚拟机系统的内部结构非常熟悉,但这在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 %ldn", 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 %ldn", 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 %ldn", 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 %ldn", 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 %ldn", map[0], ru.ru_minflt);
return 0;
}

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

最新更新