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

Dol*_*000 12 c linux mmap

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

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

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

我正在使用Linux,在这种情况下,我并不打算使用特定于Linux的调用.

Ser*_* L. 7

在某些方面,我对这个主题做了很多研究(针对不同的用途).在我的情况下,我需要一个非常稀疏填充的大型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);
Run Code Online (Sandbox Code Playgroud)

最后一次调用是性能明智的,等同于后续munmap/mmap/MAP_FIXED,但是线程安全.

性能方面,该解决方案的问题在于,必须在子序列写访问中再次出现故障,该子访问会发出中断和上下文更改.这只有在首先出现故障很少的页面时才有效.

memset 解:

如果大部分映射必须取消映射,那么在具有这样的废话性能后,我决定手动将内存归零memset.如果大约70%以上的页面已经出现故障(如果不是,那么它们是在第一轮之后memset),那么这比重新映射这些页面更快.

mincore 解:

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

内容表解决方案:

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

  • 您的“ mmap”解决方案实际上似乎正是我想要的。在我的特定情况下,速度并不是那么重要,因为新的引用相对较少,但是我想保存未使用的内存。实际上我没有想到我可以简单地在现有映射上使用“ mmap”,然后在通过“ pmap”进行检查时对其进行测试,以验证这样做不会破坏映射。其他所有的答案都很好! (2认同)

Art*_*Art 4

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;
}
Run Code Online (Sandbox Code Playgroud)

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