Linux实时应用:如何预触发缺页并锁定MAP_PRIVATE的mmap内存页
Linux实时应用页错误排查与解决方案
问题背景
开发Linux实时应用时,需要完全避免启动后的页错误。初始调用mlockall(MCL_CURRENT | MCL_FUTURE);无报错,但仍存在大量Locked: 0 kB的页面(这类页面会触发页错误)。将mlockall移至dlopen前可锁定后续加载的共享库,但编译时预加载的共享库内存未被锁定;同时用madvise实现预缺页时,对---p权限的MAP_PRIVATE页面返回-1或EPERM错误。
疑问1:为何MCL_CURRENT无法锁定已加载的预共享库?
mlockall(MCL_CURRENT)仅会锁定当前已分配物理内存的页面,而非虚拟地址空间中所有映射的页面。预加载的共享库在进程启动时,动态链接器仅建立了虚拟地址映射,其中大量页面(如只读代码段的未触发缺页部分、写时复制的未修改页面)尚未分配对应的物理内存,这类虚拟页面不会被MCL_CURRENT锁定,表现为Locked: 0 kB。
此外,部分共享库的段(如未初始化的BSS段、动态链接器延迟处理的段)在mlockall调用时还未完成映射,也会导致无法被锁定。
疑问2:如何预触发MAP_PRIVATE映射页面的缺页?
原方案问题分析
madvise(start, len, MADV_POPULATE_WRITE)要求目标页面具备写权限,但---p权限的页面无读、写、执行权限,调用时自然返回EPERM;即使是r--p的只读MAP_PRIVATE页面,MADV_POPULATE_WRITE也会因权限不足报错。
可行解决方案
针对不同权限的页面,采用不同的预缺页策略:
- 只读/可执行页面(r--p / r-xp):
- 内核版本4.4及以上可使用
MADV_POPULATE_READ,无需写权限即可预分配物理页; - 低版本内核可通过逐字节读取页面内容(用
volatile变量防止编译器优化)触发缺页。
- 内核版本4.4及以上可使用
- 可写页面(rw-p / rw-s):
- 使用
MADV_POPULATE_WRITE预分配并锁定写时复制页面; - 或临时写入原值后恢复,触发物理页分配。
- 使用
- 无权限页面(---p):
- 这类页面通常是虚拟地址空间的间隙、未使用的映射段,进程运行时不会访问,可直接跳过。
修改后的prefault实现
#define _GNU_SOURCE #include <stdio.h> #include <stdlib.h> #include <stdint.h> #include <string.h> #include <unistd.h> #include <sys/mman.h> #include <inttypes.h> #include <limits.h> void prefault() { const pid_t pid = getpid(); FILE *fp; char path[PATH_MAX]; char buf[4096]; (void)snprintf(path, sizeof(path), "/proc/%" PRIdMAX "/maps", (intmax_t)pid); fp = fopen(path, "r"); if (!fp) { perror("fopen"); return; } volatile uint8_t val; while (fgets(buf, sizeof(buf), fp)) { void *start, *end, *offset; int major, minor, n, ret; uint64_t inode; char prot[5]; char seg_path[PATH_MAX]; n = sscanf(buf, "%p-%p %4s %p %x:%x %" PRIu64 " %s\n", &start, &end, prot, &offset, &major, &minor, &inode, seg_path); if (n < 7) continue; if (start >= end) continue; size_t len = (size_t)((uint8_t *)end - (uint8_t *)start); // 跳过无权限的---p页面 if (prot[0] == '-' && prot[1] == '-' && prot[2] == '-') { continue; } // 处理只读/可执行页面 if (prot[0] == 'r') { #ifdef MADV_POPULATE_READ ret = madvise(start, len, MADV_POPULATE_READ); if (ret == -1) { // 降级为逐页读取 uint8_t *addr = (uint8_t *)start; for (size_t i = 0; i < len; i += sysconf(_SC_PAGESIZE)) { val = addr[i]; } } #else // 内核不支持MADV_POPULATE_READ时,直接逐页读取 uint8_t *addr = (uint8_t *)start; for (size_t i = 0; i < len; i += sysconf(_SC_PAGESIZE)) { val = addr[i]; } #endif } // 处理可写页面 if (prot[1] == 'w') { ret = madvise(start, len, MADV_POPULATE_WRITE); if (ret == -1) { // 降级为逐页写入恢复 uint8_t *addr = (uint8_t *)start; for (size_t i = 0; i < len; i += sysconf(_SC_PAGESIZE)) { uint8_t orig = addr[i]; addr[i] = orig; // 写入原值,触发写时复制 } } } } (void)fclose(fp); }
附加信息
相关/proc//smaps输出
$ cat /proc/<pid>/smaps | grep -B 21 -A 2 "Locked: 0 kB" | grep -B 1 "^Size:" | grep -v "Size" | grep -v "^--" 7effd0021000-7effd4000000 ---p 00000000 00:00 0 7effd4021000-7effd8000000 ---p 00000000 00:00 0 7effd80c6000-7effdc000000 ---p 00000000 00:00 0 7effddf02000-7effddfa0000 rw-s 00000000 00:05 368 /dev/some_char_device 7effddfa0000-7effde1a0000 rw-s f0000000 00:05 368 /dev/some_char_device 7effde1c1000-7effde1c2000 ---p 00000000 00:00 0 7effde1c6000-7effde1ca000 rw-s f7c00000 00:05 368 /dev/some_char_device 7effde1ca000-7effde1cb000 ---p 00000000 00:00 0 7effe221b000-7effe221c000 ---p 00000000 00:00 0 7effe2220000-7effe2223000 rw-s 00000000 00:05 90 /dev/another_char_device 7effe22df000-7effe22e0000 ---p 00013000 08:02 2234654 /<path_to>/shared_library1.so 7effe22fd000-7effe22fe000 ---p 0000c000 08:02 2231701 /<path_to>/shared_library2.so 7effe23fc000-7effe23fd000 ---p 0001c000 08:02 2234652 /<path_to>/shared_library3.so 7effe2e15000-7effe2e16000 ---p 00215000 08:02 1957 /usr/lib/x86_64-linux-gnu/libc.so.6 7effe2e40000-7effe2e41000 ---p 00011000 08:02 2234649 /<path_to>/shared_library4.so 7effe2f14000-7effe2f15000 ---p 00046000 08:02 2232115 /<path_to>/shared_library5.so 7effe321a000-7effe321b000 ---p 0021a000 08:02 855 /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.30 7effe3258000-7effe3259000 ---p 0001f000 08:02 2234643 /<path_to>/shared_library6.so 7effe327d000-7effe327e000 ---p 00021000 08:02 2234641 /<path_to>/shared_library7.so 7effe328a000-7effe328b000 ---p 00009000 08:02 2232116 /<path_to>/shared_library8.so 7effe348e000-7effe348f000 ---p 00102000 08:02 91759 /<path_to>/shared_library9.so 7effe34c6000-7effe34c8000 r--p 00000000 08:02 175 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 7effe34f2000-7effe34fd000 r--p 0002c000 08:02 175 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 7ffc1d1b0000-7ffc1d1b4000 r--p 00000000 00:00 0 [vvar] 7ffc1d1b4000-7ffc1d1b6000 r-xp 00000000 00:00 0 [vdso] ffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0 [vsyscall]
内容的提问来源于stack exchange,提问作者Jay S.
相关产品推荐
相关产品推荐

