第 50 章 虚拟内存操作 (Virtual Memory Operations)

      +

      核心结论

      • mprotect():改变虚拟内存区域保护位(PROT_NONE/READ/WRITE/EXEC)——页粒度;addr 必须页对齐;length 向上取整;违反保护 → SIGSEGV。

      • mlock / mlockall 锁页:把虚拟页锁在物理内存里,永不被换出——用于 (1) 实时性需求(避免 page fault 抖动);(2) 安全(敏感数据永不落盘)。

      • RLIMIT_MEMLOCK 资源限制:每进程可锁字节数;2.6.9+ 非特权可锁(≤ soft limit,默认 8 页 = 32 KB);特权进程不限;shmctl SHM_LOCK 按 user ID 累计。

      • munlock / 自动解除:munlock(addr, len);自动解除场景——进程终止、munmap、MAP_FIXED 覆盖;锁不跨 fork 继承、不跨 exec 保留。

      • mlockall(MCL_CURRENT|MCL_FUTURE):MCL_CURRENT 锁当前所有页;MCL_FUTURE 锁未来映射(含后续 mmap/sbrk/malloc/stack 增长)。

      • mincore():查询 [addr, addr+len) 范围内每页是否驻留在 RAM——返回字节数组,每字节最低位 = 1 表示驻留;addr 必须页对齐;SUSv3 未规定。

      • madvise():建议内核的访问模式(MADV_NORMAL/RANDOM/SEQUENTIAL/WILLNEED/DONTNEED);可移植版本是 posix_madvise()(POSIX_* 前缀)。

      • MADV_DONTNEED 语义:Linux 对 MAP_PRIVATE 是「销毁页内容」(下次访问 page fault 重置);SUSv3 posix_madvise 不影响语义;可移植代码不要依赖破坏性语义。

      本章主旨

      本章不直接关于 IPC——但常与 mmap/SysV shm/POSIX shm 配合使用。四大操作:(1) mprotect 改保护(典型场景:把 PROT_NONE 的 guard page 改成可读写);(2) mlock 锁页(实时性/安全);(3) mincore 查驻留(程序感知内存压力);(4) madvise 优化预读(数据库/科学计算)。锁页的关键约束是 RLIMIT_MEMLOCK;madvise 的关键是「Linux 的 MADV_DONTNEED 是破坏性的」——可移植程序应只调 POSIX 版本。

      一、核心概念

      本章围绕 6 个核心概念展开:mprotect、mlock/mlockall、RLIMIT_MEMLOCK、mincore、madvise 与锁的语义细节。

      概念 定义 + 重要性 实现提示

      mprotect 改保护位

      mprotect(addr, len, prot) 改变虚拟页保护位;prot ∈ {NONE, READ, WRITE, EXEC};addr 页对齐;违反 → SIGSEGV

      §50.1;典型场景:把 PROT_NONE 的匿名映射改成 PROT_READ

      PROT_WRITE;用于 JIT 代码生成(先 PROT_WRITE 写,再 mprotect PROT_EXEC)

      mlock/mlockall 锁页

      mlock(addr, len) / munlock(addr, len) 锁/解一段;mlockall(flags) 锁全部(MCL_CURRENT / MCL_FUTURE);锁住的页永不换出

      §50.2;用 munlockall() 解锁全部;mlock 调用前会先 fault 进内存,mlockall(MCL_FUTURE) 后续分配可能因 RLIMIT_MEMLOCK 失败

      RLIMIT_MEMLOCK 资源限制

      软限制(默认 8 页 = 32 KB)—非特权进程可锁字节上限;特权(CAP_IPC_LOCK)无限;shmctl SHM_LOCK 按 user ID 累计

      §50.2;用 setrlimit(RLIMIT_MEMLOCK, …​) 调整;/proc/PID/status 的 VmLck 查看当前锁定量(不含 SHM_LOCK)

      mincore 查驻留

      mincore(addr, len, vec) 返回每页是否驻留 RAM;vec 是字节数组,最低位置 1 = 驻留;addr 页对齐

      §50.3;SUSv3 未规定;2.6.21 前对 MAP_PRIVATE/nonlinear 不准;唯一保证驻留的页是 mlock 锁住的

      madvise 访问模式建议

      madvise(addr, len, advice) 告知内核访问模式;advice = NORMAL/RANDOM/SEQUENTIAL/WILLNEED/DONTNEED

      §50.4;可移植版本 posix_madvise() 用 POSIX_ 前缀;glibc ≥2.7 的 POSIX_MADV_DONTNEED 不破坏页

      锁的语义细节

      不跨 fork 继承;不跨 exec 保留;不嵌套(同一页多次 mlock 一次 unlock 就全解);共享页只要有一个进程锁住就保留;MAP_FIXED 覆盖自动解锁

      二、详细笔记

      50.1 mprotect 改变保护位

      Whatmprotect(addr, length, prot) 改变 [addr, addr+length) 范围的保护位。

      Why:典型场景——(1) 把 PROT_NONE guard page 改成 PROT_READ|PROT_WRITE;(2) JIT 代码生成(mmap PROT_READ|PROT_WRITE 写代码 → mprotect PROT_READ|PROT_EXEC 执行)。

      How

      // 摘自《The Linux Programming Interface》 第 50 章
      #include <sys/mman.h>
      int mprotect(void *addr, size_t length, int prot);
      
      /* 典型用法:创建 PROT_NONE 映射 → mprotect 改成可读写 */
      addr = mmap(NULL, LEN, PROT_NONE, MAP_SHARED|MAP_ANONYMOUS, -1, 0);
      if (addr == MAP_FAILED) errExit("mmap");
      /* 此时 /proc/self/maps 显示 ---s */
      mprotect(addr, LEN, PROT_READ|PROT_WRITE);
      /* 现在 /proc/self/maps 显示 rw-s */

      addr 必须页对齐;length 向上取整到页;prot 含义同 mmap(NONE/READ/WRITE/EXEC)。

      When:(1) 创建 PROT_NONE guard page 防止越界访问;(2) JIT 编译器生成代码段;(3) 用户态实现 stack probing;(4) mprotect 后用 msync(MS_INVALIDATE) 让其他 mmap 同一文件的进程看到新保护。

      Example:第 50 章 t_mprotect.c 创建 1 MB PROT_NONE 映射,cat /proc/PID/maps 看到 ---s,mprotect 后变 rw-s

      50.2 mlock/mlockall 内存锁

      Whatmlock(addr, len) 锁住一段虚拟页;mlockall(flags) 锁住全部;对应 munlock / munlockall

      Why:(1) 实时性——避免 page fault 抖动;(2) 安全——敏感数据永不落 swap(gpg 用 mlock 锁 passphrase)。

      How

      // 摘自《The Linux Programming Interface》 第 50 章
      #include <sys/mman.h>
      int mlock(const void *addr, size_t length);
      int munlock(const void *addr, size_t length);
      int mlockall(int flags);
      int munlockall(void);
      
      #define MCL_CURRENT  1   /* 锁当前所有映射 */
      #define MCL_FUTURE   2   /* 锁未来映射 */
      
      /* 锁一段;mlock 调用前会 fault 进物理内存 */
      mlock(addr, len);
      
      /* 锁全部(含未来) */
      mlockall(MCL_CURRENT | MCL_FUTURE);

      When:(1) 实时程序(音视频、工业控制);(2) 持有密码学密钥的进程;(3) 数据库关键缓冲。

      Example:第 50 章 memlock.cmlock 按命令行间隔锁部分页,调 displayMincore 显示驻留位图——锁住的页用 * 表示。

      50.3 RLIMIT_MEMLOCK 资源限制

      WhatRLIMIT_MEMLOCK 限制进程可锁字节数;软限制默认 8 页(x86-32 = 32 KB)。

      Why:防止普通用户锁大量内存挤垮系统;特权进程无限制。

      How

      内核版本 非特权 mlock 特权 mlock

      < 2.6.9

      ENOMEM(不允许)

      受 RLIMIT_MEMLOCK 软限制

      ≥ 2.6.9

      受 RLIMIT_MEMLOCK 软限制(默认 32 KB)

      无限制(CAP_IPC_LOCK)

      按字节向下取整到页;shmctl SHM_LOCK 另算——按 real user ID 累计所有锁住的 SysV shm 段。

      When:(1) setrlimit 提高 RLIMIT_MEMLOCK;(2) 程序启动检查当前 limit;(3) mlock 失败时优雅降级(用 mlockall(MCL_FUTURE) 主动放弃);(4) /proc/PID/status 的 VmLck 字段查看当前锁定量(注意:不包含 SHM_LOCK)。

      Exampleulimit -l 64 设为 64 KB;root 无限制;普通用户 mlock 超过 64 KB 报 ENOMEM。

      50.4 mincore 查驻留

      Whatmincore(addr, length, vec) 报告 [addr, addr+length) 范围内每页是否驻留 RAM。

      Why:程序感知内存压力——决定是否 prefetch、是否换更紧凑的数据结构。

      How

      // 摘自《The Linux Programming Interface》 第 50 章
      #define _BSD_SOURCE
      #include <sys/mman.h>
      int mincore(void *addr, size_t length, unsigned char *vec);
      /* vec 长度 = (length + PAGE_SIZE - 1) / PAGE_SIZE */
      /* 每字节最低位 = 1 表示该页驻留;高位未定义 */
      
      unsigned char vec[N];
      mincore(addr, len, vec);
      for (size_t i = 0; i < N; i++)
          if (vec[i] & 1) printf("page %zu resident\n", i);

      addr 必须页对齐;length 向上取整到页;2.6.21 前对 MAP_PRIVATE/nonlinear 不准。

      When:(1) 数据库判断 working set;(2) 垃圾收集器决定是否 swap;(3) NUMA-aware 程序感知节点内存压力;(4) 唯一保证驻留的页是 mlock 锁住的。

      Example:第 50 章 memlock.cdisplayMincore*. 字符画驻留位图。

      50.5 madvise 访问模式建议

      Whatmadvise(addr, length, advice) 告知内核访问模式——内核可优化 I/O 策略。

      Why:预读 (readahead)、drop-behind、丢弃冷页——影响磁盘 I/O 与内存占用。

      How

      // 摘自《The Linux Programming Interface》 第 50 章
      #define _BSD_SOURCE
      #include <sys/mman.h>
      int madvise(void *addr, size_t length, int advice);
      
      /* advice 取值 */
      MADV_NORMAL       /* 默认:cluster 传输 + 轻度预读 */
      MADV_RANDOM       /* 随机访问——关预读 */
      MADV_SEQUENTIAL   /* 顺序访问——激进预读 + 访问后丢页 */
      MADV_WILLNEED     /* 立即预读(类似 readahead) */
      MADV_DONTNEED     /* Linux 对 MAP_PRIVATE:销毁页内容;下次 page fault 重置 */
      
      /* 可移植版本(POSIX) */
      #define _POSIX_C_SOURCE 200809L
      int posix_madvise(void *addr, size_t length, int advice);
      /* POSIX_MADV_NORMAL/RANDOM/SEQUENTIAL/WILLNEED/DONTNEED */

      When:(1) 数据库扫描大量数据 → MADV_SEQUENTIAL;(2) B-tree 节点 → MADV_RANDOM;(3) 大文件即将访问 → MADV_WILLNEED;(4) 大临时缓冲释放内存 → MADV_DONTNEED(注意 Linux 的破坏性语义)。

      Example:mmap 一个 1 GB 文件后 madvise(addr, len, MADV_SEQUENTIAL)——内核预读 2× readahead window;之后改 MADV_RANDOM 关闭预读。

      50.6 内存锁的语义细节

      What:mlock/mlockall 创建的锁有特定的继承、生命周期与粒度语义。

      Why:避免误用——比如「同一个页内多个数据结构独立锁」是不可能的。

      How

      场景 行为

      fork

      子进程 不继承 内存锁——子进程需自己 mlock

      exec

      内存锁 不保留——exec 后所有锁消失

      锁嵌套

      同一范围多次 mlock = 单次锁;一次 munlock 全解

      共享页

      多个进程共享的页只要有 任一 进程锁住就保留

      覆盖

      MAP_FIXED 覆盖的页自动解锁

      自动解除

      进程终止 / munmap / MAP_FIXED 覆盖

      SHM_LOCK 语义不同——(1) 按段(不是按进程);(2) 段 fault-in 时才锁(mlock 调用前全部 fault-in);(3) VmLck 不包含 SHM_LOCK。

      When:(1) fork 后子进程需 mlock 自己;(2) exec 后需重新 mlock;(3) 多进程共享 shm 段只需一个进程 SHM_LOCK(且段 detach 后仍驻留)。

      Example:第 50 章 t_mprotect.c mmap 后调两次 mlock 同一范围,munlock 一次即全解——验证「锁不嵌套」。

      三、关键图表

      非可视化条目(系统调用 / 限制)
      系统调用 / 限制 描述 / 用途

      mprotect(addr, len, prot)

      改保护位;addr 页对齐;违反保护 → SIGSEGV

      mlock(addr, len) / munlock()

      锁/解一段页;mlock 调用前先 fault-in

      `mlockall(MCL_CURRENT

      MCL_FUTURE)`

      锁当前/未来页;MCL_FUTURE 后续分配可能因 RLIMIT_MEMLOCK 失败

      munlockall()

      解锁全部;同时撤销 MCL_FUTURE

      mincore(addr, len, vec)

      查驻留;vec 字节数组,最低位置 1 = 驻留

      madvise(addr, len, advice)

      访问模式建议;Linux MADV_DONTNEED 对 MAP_PRIVATE 破坏性

      posix_madvise()

      POSIX 版;用 POSIX_* 前缀;glibc ≥2.7 的 POSIX_MADV_DONTNEED 不破坏页

      RLIMIT_MEMLOCK

      软限制(默认 32 KB);特权无限;shmctl SHM_LOCK 按 user ID 累计

      /proc/PID/statusVmLck

      当前 mlock 锁定字节数(不含 SHM_LOCK)

      MADV_NORMAL/RANDOM/SEQUENTIAL/WILLNEED/DONTNEED

      五种 advice

      锁不嵌套

      同一范围多次 mlock 一次 unlock 全解

      锁不跨 fork/exec

      子进程需重新 mlock;exec 后全部消失

      共享页

      四、思维导图

      mindmap
        root((第 50 章 虚拟内存操作))
          mprotect 改保护
            PROT NONE READ WRITE EXEC
            addr 页对齐
            guard page
            JIT 代码生成
            SIGSEGV 违反
          mlock mlockall
            单段锁 mlock
            全部锁 mlockall
            MCL CURRENT FUTURE
            实时性 安全
            munlock 自动解除
          RLIMIT_MEMLOCK
            软限制 32KB 默认
            特权无限
            2.6.9 非特权可用
            shmctl SHMLOCK
            setrlimit 调整
          mincore 查驻留
            vec 字节数组
            最低位 1 驻留
            SUSv3 未规定
            唯一保证 mlock
          madvise posix_madvise
            NORMAL RANDOM
            SEQUENTIAL 激进预读
            WILLNEED 预读
            DONTNEED Linux 破坏
            POSIX_ 前缀可移植
          锁的语义
            不继承 fork
            不保留 exec
            不嵌套
            共享页任一锁住
            MAP_FIXED 覆盖解锁

      五、重点与易错点

      1. mprotect 与 mmap prot 含义一致——PROT_NONE/READ/WRITE/EXEC 标志同 mmap;违反 → SIGSEGV;addr 页对齐。

      2. mlock 调用前 fault-in——mlock(addr, len) 会把 [addr, addr+len) 全部页 fault 进物理内存;mlockall(MCL_CURRENT) 同理;mlockall(MCL_FUTURE) 只对未来生效。

      3. RLIMIT_MEMLOCK 默认 32 KB——非特权进程默认只能锁 8 页(x86-32);root(CAP_IPC_LOCK)无限;用 setrlimit 或 ulimit -l 调整。

      4. Linux 2.6.9 前后语义不同——之前非特权不能 mlock;之后非特权可锁但受 RLIMIT_MEMLOCK 软限制。

      5. 锁不嵌套——同一范围多次 mlock 等价一次;一次 munlock 全解;同一虚拟页内不同数据结构不能独立锁。

      6. 锁不跨 fork 继承——子进程需自己 mlock;锁不跨 exec 保留。

      7. 共享页任一锁住就保留——MAP_SHARED 多进程映射同一文件,只要一个进程 mlock,所有进程都享受驻留。

      8. mlock vs SHM_LOCK 语义——mlock 按进程 fault-in 全页;SHM_LOCK 按段 lazy fault;SHM_LOCK 不计入 VmLck;SHM_LOCK 段 detach 后仍驻留。

      9. mincore 返回是快照——返回后页可能被换出;唯一保证驻留的页是 mlock 锁住的;addr 页对齐;2.6.21 前对 MAP_PRIVATE 不准。

      10. mincore vec 是 unsigned char——Linux 是 unsigned char*;某些 UNIX 是 char*;每字节最低位置 1 = 驻留;高位未定义。

      11. madvise 与 posix_madvise 的 DONTNEED——Linux madvise MADV_DONTNEED 对 MAP_PRIVATE 破坏性(下次访问 page fault 重置);POSIX_MADV_DONTNEED(glibc ≥2.7)什么也不做;可移植代码用 POSIX 版本。

      12. MADV_SEQUENTIAL = 激进预读 + 快速丢页——适合一次顺序扫描(grep-like);不适合随机访问。

      13. madvise 是 hint,不是指令——内核可忽略;性能影响视 workload 而异。

      14. JIT 代码生成三步走——mmap(PROT_READ|PROT_WRITE) 写代码 → mprotect(PROT_READ|PROT_EXEC) 执行;现代 Linux 还需 arch_prctl(ARCH_PROTECT_PAX) 或 seccomp 配合。

      15. 跨章衔接:第 48 章「SysV 共享内存」用 shmctl SHM_LOCK 锁段;第 49 章「内存映射」用 mmap MAP_LOCKED 创建时锁;本章 mlcok 是后续锁;第 54 章「POSIX 共享内存」同样可 mlock。