Linux内核虚拟内存Overcommit异常:shm_open+mmap无法用全部分配内存
环境信息
- 操作系统: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

