You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Linux内核虚拟内存Overcommit异常:shm_open+mmap无法用全部分配内存

Linux虚拟内存过度提交异常问题

环境信息

  • 操作系统:Ubuntu 22.04.3、Ubuntu 20.04、Red Hat Enterprise Linux 8.8、Fedora release 38
  • 编译器:gcc-13.2.1、gcc 11.4.0、gcc 9.4.0、gcc 8.5.0、clang 17.0.0
  • 处理器:AMD EPYC 7571(AWS EC2,124.68 GiB内存)、Intel Core i7-2620M(8GB内存)、Intel Core i3-5005U(8GB内存)、AMD Ryzen 5 5600G(16GB内存)

问题描述

Linux内核的虚拟内存过度提交(Overcommit)功能未按官方文档预期工作。

使用shm_open()和mmap()为无关进程分配共享内存时,mmap()可正常分配占物理内存比例较大的内存,但当访问的内存页数达到物理内存的50%时,Linux按需内存分配机制会触发总线错误(Bus error)。

默认状态下,执行grep -i commit /proc/meminfo显示CommitLimit: XXXX kB(XXXX为物理内存的50%),cat /proc/sys/vm/overcommit_memory显示默认过度提交模式为0(启发式过度提交处理)。

若通过sudo sysctl -w vm.overcommit_memory=2将模式设为"不允许过度提交",且过度提交比例(overcommit_ratio)设为小于50%时,功能符合预期;但当比例设为51%-100%时无效果,仍会在访问页数达到物理内存50%时触发总线错误。

然而使用匿名mmap()时,即使在启发式过度提交模式下也不会触发总线错误。曾怀疑共享内存对象因文件指针产生缓存,占用等量内存导致限制为物理内存的1/2,但该推测合理性存疑。

附加系统信息

  • /proc/sys/kernel/shmmax = 18446744073692774399

  • /proc/sys/kernel/shmall = 18446744073692774399

  • /proc/sys/kernel/shmmni = 4096

  • /proc/<PID>/maps中文件描述符的address start – address end与程序中分配的内存位置及大小一致。

  • dmesg中无总线错误相关日志。

  • 将/proc/<PID>/oom_score_adj设为-1000无效果。

  • 创建与物理内存等量的交换文件无效果。

测试系统限制设置

real-time non-blocking time  (microseconds, -R) ->unlimited
core file size              (blocks, -c) ->unlimited
data seg size               (kbytes, -d) ->unlimited
scheduling priority                 (-e) ->0
file size                   (blocks, -f) ->unlimited
pending signals                     (-i) ->510627
max locked memory           (kbytes, -l) ->unlimited
max memory size             (kbytes, -m) ->unlimited
open files                          (-n) ->1024
pipe size                (512 bytes, -p) ->8
POSIX message queues         (bytes, -q) ->819200
real-time priority                  (-r) ->0
stack size                  (kbytes, -s) ->8192
cpu time                   (seconds, -t) ->unlimited
max user processes                  (-u) ->510627
virtual memory              (kbytes, -v) ->unlimited
file locks                          (-x) ->unlimited

故障后核心追踪

(gdb) bt full
#0  main (argc=2, argv=0x7fffecbfb148) at overcommit.cpp:101
page_size = 4096
total_system_ram = 133873930240
pool_size = 80324358144
print_interval = 4016214016
fd = 3
shmpath = "/foo"
pool = 0x7eef714b9000 ""
temp = 0 '\000'
count = 66936954880

复现问题的测试程序

// 2023年9月 - Gene Weber
//
// 编译命令:g++ -std=c++11 overcommit.cpp -W -Wall -Wextra -pedantic -pthread -o overcommit -lrt

#include <iostream>
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/types.h>
#include <math.h>
#include <cerrno>


int main(int argc, char** argv) {
    int page_size = 4096;
    
    if (argc != 3) {
        std::cerr << "\n使用方法: ./overcommit <总物理内存字节数> <a/f> \n\n";
        std::cerr << "       <总物理内存字节数> = 执行\"free -b\"得到的total值.\n";
        std::cerr << "       <a/f> = a表示使用匿名mmap,f表示使用基于文件的共享内存.\n";
        exit(EXIT_FAILURE);
    }

    uint_fast64_t total_system_ram = strtoull(argv[1], NULL, 10);

    // 将pool_size设置为系统内存的~60%,且为页大小的整数倍
    uint_fast64_t pool_size = total_system_ram * 0.6;
    pool_size = pool_size - (pool_size % page_size);
    std::cout << "内存池大小 = " << pool_size << "\n";

    // 设置状态打印间隔,必须为页大小的整数倍
    // 任意选择每遍历1/20的范围打印一次
    uint_fast64_t print_interval = pool_size/20;
    print_interval = print_interval - (print_interval % page_size);

    char* pool = nullptr;

    // 如果选择"a"选项,使用MAP_ANON | MAP_SHARED;否则使用shm_open()
    // 匿名mmap可以成功执行
    if (*argv[2] == 'a') {
        pool = (char*)mmap(NULL, pool_size, PROT_READ | PROT_WRITE, MAP_ANON | MAP_SHARED, -1, 0);
        if (pool == MAP_FAILED) {
            std::cerr << "\nmmap内存池失败,程序退出.\n\n";
            exit(EXIT_FAILURE);
        }
    }
    else {  // 使用shm_open(),此路径会触发故障
        int fd;
        std::string shmpath = "/foo";
    
        // 删除已存在的共享内存对象
        shm_unlink(shmpath.c_str());
        // 创建具有读写权限的共享内存对象
        fd = shm_open(shmpath.c_str(), O_CREAT | O_EXCL | O_RDWR, S_IRUSR | S_IWUSR);
        
        if (fd == -1) {
            std::cerr << "\nshm_open共享内存失败,程序退出.\n\n";
            exit(EXIT_FAILURE);
        }
        
        // 设置共享内存对象大小
        if (ftruncate64(fd, pool_size) == -1) {
            std::cerr << "\nftruncate共享内存失败,程序退出.\n\n";
            exit(EXIT_FAILURE);
        }
        
        // 映射共享内存对象
        // 使用MAP_SHARED和MAP_SHARED_VALIDATE | MAP_POPULATE的结果无差异
          // char* pool = (char*)mmap(NULL, pool_size, PROT_READ | PROT_WRITE, MAP_SHARED_VALIDATE | MAP_POPULATE, fd, 0);
        pool = (char*)mmap(NULL, pool_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
        if (pool == MAP_FAILED) {
            std::cerr << "\nmmap内存池失败,程序退出.\n\n";
            exit(EXIT_FAILURE);
        }
    }

    std::cout << "内存池基地址 = " << (uint_fast64_t)pool << "\n";
    std::cout << "内存池顶部地址 = " << (uint_fast64_t)pool + pool_size << "\n";

    // 使用madvise预加载所有内存页,匿名mmap成功,基于文件的共享内存失败
    // std::cout << "预加载内存池所有页.\n" << std::flush;
    // if (madvise(pool, pool_size, MADV_POPULATE_READ) == -1) {
    //     std::cout << "错误码 = " << errno << ".  " << std::flush;
    //     std::perror("madvise失败:");
    //     exit(EXIT_FAILURE);
    // }

    char temp = 'a';
    uint_fast64_t count=0;

    // 从内存池基地址到顶部遍历,每页访问一个位置。当访问的内存达到物理内存的~50%时,循环会失败。
    // 内存访问顺序不影响结果,已测试多种场景。
    while (count < pool_size) {

        // 按间隔打印状态
        if (count % print_interval == 0) {
            std::cout << "已访问字节数 = " << count << "  当前地址 = " << (uint_fast64_t)&pool[count] << "\n";
        }

        // 读或写地址都会触发页错误。核心转储的回溯显示count值约为物理内存的50%。
        // pool[count] = temp;
        temp = pool[count];

    count += page_size;
    }

    std::cout << "执行成功!  temp值 = " << temp << "\n";
}

内容的提问来源于stack exchange,提问作者geenweb

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.11 08:59:51