【问题标题】:Is it possible to "punch holes" through mmap'ed anonymous memory?是否可以通过 mmap 的匿名内存“打孔”?
【发布时间】:2014-02-12 08:35:22
【问题描述】:

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

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

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

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

【问题讨论】:

  • 什么是“多”?几十万?
  • @unwind:老实说,我对理论案例比对实际案例更感兴趣,所以假设是数百万。 :)
  • 我不太确定你所说的理论是什么意思。您的问题是一个具体的工程问题,取决于很多因素。我什至不确定linux的具体版本对这类问题没有影响。您的问题基本上归结为,您是否应该在用户空间中模拟系统任务(大块的内存分配)。如果您被困在一个相关功能实施不佳的平台上,您应该只问自己这样的问题。我不认为linux是这种情况,VM管理相当复杂。
  • @JensGustedt:我的意思是能够在内存本身中打孔的问题,而不是在特定的前提下它是否有用。
  • @Dolda2000 但您的基本假设是,仅以最直接的方式使用 API 是不好的,即您可以在用户空间中做得更好。这是一个......不危险,但“奇怪”的概念。内核应该在这些方面擅长。如果它不能管理一百万个mmap()ed 区域,那么它就坏了,你不应该花时间尝试在用户空间中解决它。至少这是我的感觉,但我不是内核开发人员。

标签: c linux mmap


【解决方案1】:

在某个时候,我对这个主题(用于不同的用途)进行了大量研究。就我而言,我需要一个人口稀少的大型哈希图 + 时不时将其归零的能力。

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% 的映射出错,此时memsetting 整个映射变得更简单( mincore 是一个系统调用,需要更改一个上下文)。

incore 表解决方案:

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

【讨论】:

  • 您的mmap 解决方案实际上似乎正是我所寻找的。在我的特殊情况下,速度并不是那么重要,因为新的引用相对较少,但我想保存未在使用中的内存。实际上我并没有让我感到震惊,我可以简单地 mmap 覆盖现有的映射,并在检查 pmap 的同时进行测试,以验证这样做不会使映射碎片化。其他方面的好答案!
【解决方案2】:

我不明白为什么给mmap/munmap打很多电话应该是那么糟糕。在内核中查找映射的性能应该是 O(log n)。

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

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

正如下面 cmets 中的讨论,令人惊讶的是,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。

【讨论】:

  • mprotect(PROT_NONE) 似乎不对。这实际上不允许内核回收页面,是吗?是的,MADV_REMOVE 让我充满了希望,但文档确实声称它只是由少数文件系统作为专业实现的。
  • @Dolda2000 我不知道 PROT_NONE 是否允许 Linux 回收页面,我知道它在几种 BSD 中都可以。但是在更大的 mmap 中执行此操作只会将该 mmap 拆分为两个或三个单独的映射,因此它不会阻止内核内部的碎片。它几乎就像munmap,只是那些特定地址不会被标记为空闲,因此它们将无法分配。
  • 我刚想起一件事。 Linux 中的匿名映射实际上不是在 tmpfs 之上内部实现的,因此madvise(MADV_REMOVE) 应该在它们上工作吗?我需要在几分钟后测试一下。
  • 我刚才自己试过了,可惜好像不行。 madvise 在匿名映射上调用时返回 EINVAL
  • 哦,你知道什么。看来我没有正确阅读madvise 联机帮助页。我没想到MADV_DONTNEED 实际上允许内核清除页面。你知道的越多。
猜你喜欢
  • 2022-09-23
  • 2023-02-07
  • 2015-11-17
  • 1970-01-01
  • 1970-01-01
  • 2019-04-08
  • 2013-07-06
  • 2017-02-13
  • 2016-11-21
相关资源
最近更新 更多