HOOOS

Linux 共享内存的深水区:shm_open 与 shmget 会被 Swap 交换吗?

0 23 Linux内功修炼 Linux共享内存操作系统
Apple

在 Linux 系统底层开发和高性能服务优化中,共享内存(Shared Memory)是实现进程间零拷贝通信的王牌。但许多开发者在设计高并发、低延迟系统时,常常会忽略一个致命的隐患:当宿主机物理内存不足时,通过 shm_openshmget 申请的内存,到底会不会被操作系统交换到 Swap 分区?

简短的答案是:在默认情况下,完全会。

一旦共享内存被交换到 Swap,原本纳秒/微秒级别的内存访问就会瞬间退化为毫秒级的磁盘 I/O(即便是 NVMe SSD 也无法承受这种延迟抖动),导致整个服务出现严重的卡顿或超时。

本文将从 Linux 内核的虚拟内存管理机制(VM)出发,深度解析为什么共享内存会被 Swap,以及在生产环境中我们该如何彻底杜绝这一现象。


一、为什么共享内存会被 Swap?

要理解这个问题,我们需要看清 shm_openshmget 在内核底层的真面目。

1. shm_open(POSIX 共享内存)的本质

POSIX 共享内存是通过虚拟文件系统 tmpfs 实现的。
当调用 shm_open("/my_shm", ...) 时,内核实际上是在 /dev/shm/(默认挂载为 tmpfs)下创建了一个虚拟文件。随后,你通过 mmap 将这个文件映射到进程的虚拟地址空间。

  • tmpfs 的特性tmpfs 是一个完全基于内存的文件系统,它没有真实的磁盘介质支撑。
  • 内存类型:在 Linux 内核眼里,tmpfs 中的页(Pages)属于 shmem(Shared Memory) 类型的页。
  • 交换逻辑:由于没有后端物理磁盘文件可以同步(不像普通的 mmap 映射磁盘文件可以直接 page out 丢弃),当系统面临内存压力(Memory Pressure)时,内核的页面回收机制(Page Reclaim)必须将这些修改过的脏页写入到 Swap 空间,才能释放出物理物理页框(Page Frame)。

2. shmget(System V 共享内存)的本质

System V 共享内存在内核中通过特殊的匿名共享内存段(anonymous shared memory segments)实现。

  • 虽然它不显式依赖 /dev/shm 目录,但在内核底层的内存分配路径上,它与 POSIX shm_open 走的是同一套内核接口(即 shmem.c 中的匿名页面管理)。
  • 因此,当系统物理内存耗尽,内核触发 kswapd 或直接内存回收(Direct Reclaim)时,System V 共享内存段的页面同样会被放入 Inactive Anon List(非活动匿名页链表) 中,并最终被交换到 Swap。

二、内核层面的数据流向

Linux 内核将内存页主要分为两类:

  1. File-backed pages(文件页):有磁盘文件支撑,回收时可直接丢弃(如果未修改)或写回原文件。
  2. Anonymous pages(匿名页):无磁盘文件支撑(如进程的堆、栈、数据段)。共享内存(shmem)在回收行为上被归类为匿名页。

当系统物理内存不足,触发内存回收时,内核的工作流如下:

                    【 宿主机物理内存不足 】
                              │
                              ▼
                    【 触发内核页面回收 】
                              │
            ┌─────────────────┴─────────────────┐
            ▼                                   ▼
    [ File-backed Pages ]               [ Anonymous Pages / shmem ]
    (如普通文件 mmap)                 (如 shm_open, shmget, heap)
            │                                   │
      有未落盘的数据?                             是否有活跃的 Swap 分区?
        ┌───┴───┐                               ┌───┴───┐
       YES      NO                             YES      NO
        │       │                               │       │
        ▼       ▼                               ▼       ▼
    写回磁盘   直接释放页框                 写入 Swap 分区  触发 OOM Killer

从上图可以看出:

  • 如果系统开启了 Swap,共享内存页会被写入 Swap,进程访问该内存时触发 Major Page Fault(主缺页中断),从磁盘换入内存,导致延迟暴增。
  • 如果系统关闭了 Swap,共享内存页无法被交换。但如果此时物理内存依然耗尽,内核为了保全系统,会触发 OOM Killer,直接杀掉占用内存较多的进程。

三、如何在生产环境中防止共享内存被 Swap?

在低延迟场景(如量化交易、高并发缓存、实时通信)中,我们必须确保共享内存常驻物理内存。以下是四种主流的规避方案:

方案 1:使用 mlock / mlockall 锁定内存(推荐用于 POSIX shm_open

对于通过 shm_open 并在 mmap 映射后的虚拟内存地址,可以使用 mlock 系统调用锁住该段地址空间,防止其被内核交换。

int fd = shm_open("/my_shm", O_RDWR | O_CREAT, 0666);
ftruncate(fd, SHM_SIZE);
void* addr = mmap(NULL, SHM_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);

// 锁定整个共享内存段
if (mlock(addr, SHM_SIZE) != 0) {
    perror("mlock failed");
    // 注意:非 root 用户或未配置 rlimit 的用户可能会因为权限不足失败
}
  • 优点:控制粒度细,可针对特定共享内存段进行锁定。
  • 限制:需要 CAP_IPC_LOCK 权限,或者在 /etc/security/limits.conf 中为用户配置足够的 memlock 额度(例如 * hard memlock unlimited)。

方案 2:使用 SHM_LOCKED 标志(针对 System V shmget

如果是 System V 共享内存,可以在创建后通过 shmctl 施加 SHM_LOCKED 锁,明确告知内核不允许将其交换。

int shmid = shmget(key, SHM_SIZE, IPC_CREAT | 0666);
void* addr = shmat(shmid, NULL, 0);

// 锁定共享内存段
if (shmctl(shmid, SHM_LOCKED, NULL) < 0) {
    perror("shmctl SHM_LOCKED failed");
}
  • 优点:原生支持,直接作用于整个共享内存段,无需关心具体的虚拟地址映射。
  • 限制:同样需要 CAP_IPC_LOCK 权限。

方案 3:使用大页内存(Huge Pages / Direct mapped HugeTLB)

这是高性能场景下的终极解决方案。
在 Linux 中,大页(Huge Pages,如 2MB 或 1GB 页面)具有一个天然的特性:大页永远不会被交换到 Swap。

  • 对于 System V:在 shmget 时传入 SHM_HUGETLB 标志:

    int shmid = shmget(key, SHM_SIZE, IPC_CREAT | 0666 | SHM_HUGETLB);
    
  • 对于 POSIX mmap:在 mmap 时使用 MAP_HUGETLB 标志:

    void *addr = mmap(NULL, SHM_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS | MAP_HUGETLB, -1, 0);
    
  • 核心优势

    1. 绝不参与 Swap:内核设计上就不支持大页交换。
    2. 提升 TLB 命中率:减少页表层级,大幅降低 TLB miss,对内存密集型应用有显著的性能提升。
  • 前置条件:系统必须提前预留充足的大页内存(如通过 sysctl -w vm.nr_hugepages=1024)。

方案 4:调整系统级参数 swappiness

可以通过调整系统的 swappiness 参数,降低内核使用 Swap 的倾向。

# 查看当前 swappiness (默认通常是 60)
sysctl vm.swappiness

# 临时将其设为 10(只有在物理内存非常紧张时才使用 Swap)
sysctl -w vm.swappiness=10
  • 警告:这是一种全局粗粒度的调整。即使将 swappiness 设为 1(甚至 0,在某些老内核版本),当物理内存彻底耗尽且无法回收文件页时,内核依然会为了避免崩溃而将共享内存交换到 Swap。因此,该方案不能作为完全避免 Swap 的唯一保障。

四、生产环境排查指南

如果怀疑线上的共享内存遭遇了 Swap,可以通过以下手段进行验证:

1. 查看进程的 smaps

通过 /proc/[pid]/smaps 可以精准查看某个进程的共享内存段有多少被交换出去了:

# 找到你的进程 PID,过滤出共享内存(如 /dev/shm)的内存分布
cat /proc/[PID]/smaps | grep -A 15 "/dev/shm"

输出中如果出现以下字段且数值大于 0,说明已经发生了 Swap:

Swap:               1024 kB  <-- 说明有 1MB 共享内存被交换到了磁盘
SwapPss:            1024 kB

2. 监控系统级 shmem 的 Swap 情况

通过 /proc/meminfo 监控系统整体的共享内存和 Swap 使用趋势:

cat /proc/meminfo | grep -E "(Shmem|SwapTotal|SwapFree)"

总结

Linux 的共享内存在提升性能的同时,也隐藏了被 Swap 导致的“性能塌方”风险。

  • shm_openshmget 申请的内存,在系统面临内存压力且开启 Swap 时,默认均会被交换到磁盘
  • 若要保证绝对不被 Swap,最稳妥的方案是:
    • 常规内存:使用 mlock (POSIX) 或 SHM_LOCKED (System V) 进行物理内存锁定。
    • 超高性能场景:直接采用 Huge Pages(大页) 承载共享内存,兼顾免 Swap 安全性与高访问性能。

点评评价

captcha
健康