在 Linux 系统底层开发和高性能服务优化中,共享内存(Shared Memory)是实现进程间零拷贝通信的王牌。但许多开发者在设计高并发、低延迟系统时,常常会忽略一个致命的隐患:当宿主机物理内存不足时,通过 shm_open 或 shmget 申请的内存,到底会不会被操作系统交换到 Swap 分区?
简短的答案是:在默认情况下,完全会。
一旦共享内存被交换到 Swap,原本纳秒/微秒级别的内存访问就会瞬间退化为毫秒级的磁盘 I/O(即便是 NVMe SSD 也无法承受这种延迟抖动),导致整个服务出现严重的卡顿或超时。
本文将从 Linux 内核的虚拟内存管理机制(VM)出发,深度解析为什么共享内存会被 Swap,以及在生产环境中我们该如何彻底杜绝这一现象。
一、为什么共享内存会被 Swap?
要理解这个问题,我们需要看清 shm_open 和 shmget 在内核底层的真面目。
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目录,但在内核底层的内存分配路径上,它与 POSIXshm_open走的是同一套内核接口(即shmem.c中的匿名页面管理)。 - 因此,当系统物理内存耗尽,内核触发
kswapd或直接内存回收(Direct Reclaim)时,System V 共享内存段的页面同样会被放入 Inactive Anon List(非活动匿名页链表) 中,并最终被交换到 Swap。
二、内核层面的数据流向
Linux 内核将内存页主要分为两类:
- File-backed pages(文件页):有磁盘文件支撑,回收时可直接丢弃(如果未修改)或写回原文件。
- 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);核心优势:
- 绝不参与 Swap:内核设计上就不支持大页交换。
- 提升 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_open和shmget申请的内存,在系统面临内存压力且开启 Swap 时,默认均会被交换到磁盘。- 若要保证绝对不被 Swap,最稳妥的方案是:
- 常规内存:使用
mlock(POSIX) 或SHM_LOCKED(System V) 进行物理内存锁定。 - 超高性能场景:直接采用 Huge Pages(大页) 承载共享内存,兼顾免 Swap 安全性与高访问性能。
- 常规内存:使用