草庐IT

linux - 具有私有(private)匿名映射的 ENOMEM 的 munmap() 失败

coder 2023-06-16 原文

我最近发现 Linux 不保证用 mmap 分配的内存可以用 munmap 释放,如果这导致 VMA(虚拟内存区域)数量结构超过 vm.max_map_count。联机帮助页(几乎)清楚地说明了这一点:

 ENOMEM The process's maximum number of mappings would have been exceeded.
 This error can also occur for munmap(), when unmapping a region
 in the middle of an existing mapping, since this results in two
 smaller mappings on either side of the region being unmapped.

问题是 Linux 内核总是尽可能尝试合并 VMA 结构,这使得 munmap 即使对于单独创建的映射也会失败。我能够编写一个小程序来确认此行为:

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>

#include <sys/mman.h>

// value of vm.max_map_count
#define VM_MAX_MAP_COUNT        (65530)

// number of vma for the empty process linked against libc - /proc/<id>/maps
#define VMA_PREMAPPED           (15)

#define VMA_SIZE                (4096)
#define VMA_COUNT               ((VM_MAX_MAP_COUNT - VMA_PREMAPPED) * 2)

int main(void)
{
    static void *vma[VMA_COUNT];

    for (int i = 0; i < VMA_COUNT; i++) {
        vma[i] = mmap(0, VMA_SIZE, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);

        if (vma[i] == MAP_FAILED) {
            printf("mmap() failed at %d\n", i);
            return 1;
        }
    }

    for (int i = 0; i < VMA_COUNT; i += 2) {
        if (munmap(vma[i], VMA_SIZE) != 0) {
            printf("munmap() failed at %d (%p): %m\n", i, vma[i]);
        }
    }
}

它使用 mmap 分配大量页面(默认允许的最大值的两倍),然后每第二页 munmap 为每个剩余页面创建单独的 VMA 结构。在我的机器上,最后一次 munmap 调用总是失败并返回 ENOMEM

最初我认为 munmap 如果与用于创建映射的地址和大小的值相同,则永远不会失败。显然,在 Linux 上情况并非如此,我无法找到有关其他系统上类似行为的信息。

与此同时,在我看来,在任何操作系统上,对于每个合理的实现,应用于映射区域中间的部分取消映射预计会失败,但我还没有找到任何文档表明这种失败是可能的。

我通常认为这是内核中的错误,但了解 Linux 如何处理内存过度使用和 OOM 我几乎可以肯定这是一个旨在提高性能和减少内存消耗的“功能”。

我能找到的其他信息:

  • 由于其设计原因,Windows 上的类似 API 没有此“功能”(参见 MapViewOfFileUnmapViewOfFileVirtualAlloc VirtualFree) - 它们根本不支持部分取消映射。
  • glibc malloc 实现不会创建超过 65535 的映射,当达到此限制时退回到 sbrk:https://code.woboq.org/userspace/glibc/malloc/malloc.c.html .这看起来像是解决此问题的方法,但仍然有可能使 free 悄悄泄漏内存。
  • jemalloc 遇到了这个问题,并试图避免使用 mmap/munmap 因为这个问题(我不知道他们是如何结束的)。

其他操作系统真的保证内存映射的释放吗?我知道 Windows 会这样做,但是其他类 Unix 操作系统呢?自由系统? QNX?


编辑:我正在添加示例,说明当内部 munmap 调用因 ENOMEM 而失败时,glibc 的 free 会如何泄漏内存。使用 strace 查看 munmap 失败:

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>

#include <sys/mman.h>

// value of vm.max_map_count
#define VM_MAX_MAP_COUNT        (65530)

#define VMA_MMAP_SIZE           (4096)
#define VMA_MMAP_COUNT          (VM_MAX_MAP_COUNT)

// glibc's malloc default mmap_threshold is 128 KiB
#define VMA_MALLOC_SIZE         (128 * 1024)
#define VMA_MALLOC_COUNT        (VM_MAX_MAP_COUNT)

int main(void)
{
    static void *mmap_vma[VMA_MMAP_COUNT];

    for (int i = 0; i < VMA_MMAP_COUNT; i++) {
        mmap_vma[i] = mmap(0, VMA_MMAP_SIZE, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);

        if (mmap_vma[i] == MAP_FAILED) {
            printf("mmap() failed at %d\n", i);
            return 1;
        }
    }

    for (int i = 0; i < VMA_MMAP_COUNT; i += 2) {
        if (munmap(mmap_vma[i], VMA_MMAP_SIZE) != 0) {
            printf("munmap() failed at %d (%p): %m\n", i, mmap_vma[i]);
            return 1;
        }
    }

    static void *malloc_vma[VMA_MALLOC_COUNT];

    for (int i = 0; i < VMA_MALLOC_COUNT; i++) {
        malloc_vma[i] = malloc(VMA_MALLOC_SIZE);

        if (malloc_vma[i] == NULL) {
            printf("malloc() failed at %d\n", i);
            return 1;
        }
    }

    for (int i = 0; i < VMA_MALLOC_COUNT; i += 2) {
        free(malloc_vma[i]);
    }
}

最佳答案

在 Linux 上解决此问题的一种方法是一次 mmap 多个页面(例如,一次 1 MB),并在其后映射一个分隔页。所以,你实际上是在257页内存上调用了mmap,然后用PROT_NONE重新映射了最后一页,这样就无法访问了。这应该会破坏内核中的 VMA 合并优化。由于您一次分配多个页面,因此不应遇到最大映射限制。缺点是您必须手动管理如何对大型 mmap 进行切片。

关于您的问题:

  1. 由于各种原因,系统调用在任何系统上都可能失败。文档并不总是完整的。

  2. 您可以munmap mmapd 区域的一部分,只要传入的地址位于页面边界上,并且长度参数是向上舍入到页面大小的下一个倍数。

关于linux - 具有私有(private)匿名映射的 ENOMEM 的 munmap() 失败,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/43743555/

有关linux - 具有私有(private)匿名映射的 ENOMEM 的 munmap() 失败的更多相关文章

  1. ruby - 为什么我可以在 Ruby 中使用 Object#send 访问私有(private)/ protected 方法? - 2

    类classAprivatedeffooputs:fooendpublicdefbarputs:barendprivatedefzimputs:zimendprotecteddefdibputs:dibendendA的实例a=A.new测试a.foorescueputs:faila.barrescueputs:faila.zimrescueputs:faila.dibrescueputs:faila.gazrescueputs:fail测试输出failbarfailfailfail.发送测试[:foo,:bar,:zim,:dib,:gaz].each{|m|a.send(m)resc

  2. ruby - 在 Ruby 中使用匿名模块 - 2

    假设我做了一个模块如下:m=Module.newdoclassCendend三个问题:除了对m的引用之外,还有什么方法可以访问C和m中的其他内容?我可以在创建匿名模块后为其命名吗(就像我输入“module...”一样)?如何在使用完匿名模块后将其删除,使其定义的常量不再存在? 最佳答案 三个答案:是的,使用ObjectSpace.此代码使c引用你的类(class)C不引用m:c=nilObjectSpace.each_object{|obj|c=objif(Class===objandobj.name=~/::C$/)}当然这取决于

  3. ruby - 具有身份验证的私有(private) Ruby Gem 服务器 - 2

    我想安装一个带有一些身份验证的私有(private)Rubygem服务器。我希望能够使用公共(public)Ubuntu服务器托管内部gem。我读到了http://docs.rubygems.org/read/chapter/18.但是那个没有身份验证-如我所见。然后我读到了https://github.com/cwninja/geminabox.但是当我使用基本身份验证(他们在他们的Wiki中有)时,它会提示从我的服务器获取源。所以。如何制作带有身份验证的私有(private)Rubygem服务器?这是不可能的吗?谢谢。编辑:Geminabox问题。我尝试“捆绑”以安装新的gem..

  4. ruby - 即使失败也继续进行多主机测试 - 2

    我已经构建了一些serverspec代码来在多个主机上运行一组测试。问题是当任何测试失败时,测试会在当前主机停止。即使测试失败,我也希望它继续在所有主机上运行。Rakefile:namespace:specdotask:all=>hosts.map{|h|'spec:'+h.split('.')[0]}hosts.eachdo|host|begindesc"Runserverspecto#{host}"RSpec::Core::RakeTask.new(host)do|t|ENV['TARGET_HOST']=hostt.pattern="spec/cfengine3/*_spec.r

  5. ruby - 在匿名 block 中产生 - 2

    我没有理解以下行为(另请参阅inthisSOthread):defdef_testputs'def_test.in'yieldifblock_given?puts'def_test.out'enddef_testdoputs'def_testok'endblock_test=procdo|&block|puts'block_test.in'block.callifblockputs'block_test.out'endblock_test.calldoputs'block_test'endproc_test=procdoputs'proc_test.in'yieldifblock_gi

  6. ruby-on-rails - Rails 3.1 中具有相同形式的多个模型? - 2

    我正在使用Rails3.1并在一个论坛上工作。我有一个名为Topic的模型,每个模型都有许多Post。当用户创建新主题时,他们也应该创建第一个Post。但是,我不确定如何以相同的形式执行此操作。这是我的代码:classTopic:destroyaccepts_nested_attributes_for:postsvalidates_presence_of:titleendclassPost...但这似乎不起作用。有什么想法吗?谢谢! 最佳答案 @Pablo的回答似乎有你需要的一切。但更具体地说...首先改变你View中的这一行对此#

  7. ruby-on-rails - 创建 ruby​​ 数据库时惰性符号绑定(bind)失败 - 2

    我正在尝试在Rails上安装ruby​​,到目前为止一切都已安装,但是当我尝试使用rakedb:create创建数据库时,我收到一个奇怪的错误:dyld:lazysymbolbindingfailed:Symbolnotfound:_mysql_get_client_infoReferencedfrom:/Library/Ruby/Gems/1.8/gems/mysql2-0.3.11/lib/mysql2/mysql2.bundleExpectedin:flatnamespacedyld:Symbolnotfound:_mysql_get_client_infoReferencedf

  8. ruby - 正则表达式在哪个位置失败? - 2

    我需要一个非常简单的字符串验证器来显示第一个符号与所需格式不对应的位置。我想使用正则表达式,但在这种情况下,我必须找到与表达式相对应的字符串停止的位置,但我找不到可以做到这一点的方法。(这一定是一种相当简单的方法……也许没有?)例如,如果我有正则表达式:/^Q+E+R+$/带字符串:"QQQQEEE2ER"期望的结果应该是7 最佳答案 一个想法:你可以做的是标记你的模式并用可选的嵌套捕获组编写它:^(Q+(E+(R+($)?)?)?)?然后你只需要计算你获得的捕获组的数量就可以知道正则表达式引擎在模式中停止的位置,你可以确定匹配结束

  9. ruby - 具有两个参数的 block - 2

    我从用户Hirolau那里找到了这段代码:defsum_to_n?(a,n)a.combination(2).find{|x,y|x+y==n}enda=[1,2,3,4,5]sum_to_n?(a,9)#=>[4,5]sum_to_n?(a,11)#=>nil我如何知道何时可以将两个参数发送到预定义方法(如find)?我不清楚,因为有时它不起作用。这是重新定义的东西吗? 最佳答案 如果您查看Enumerable#find的文档,您会发现它只接受一个block参数。您可以将它发送两次的原因是因为Ruby可以方便地让您根据它的“并行赋

  10. ruby - 使用 rbenv 和 ruby​​-build 构建 ruby​​ 失败,出现 undefined symbol : SSLv2_method - 2

    我正在尝试在配备ARMv7处理器的SynologyDS215j上安装ruby​​2.2.4或2.3.0。我用了optware-ng安装gcc、make、openssl、openssl-dev和zlib。我根据README中的说明安装了rbenv(版本1.0.0-19-g29b4da7)和ruby​​-build插件。.这些是随optware-ng安装的软件包及其版本binutils-2.25.1-1gcc-5.3.0-6gconv-modules-2.21-3glibc-opt-2.21-4libc-dev-2.21-1libgmp-6.0.0a-1libmpc-1.0.2-1libm

随机推荐