在 Linux 运维和数据库调优(如 PostgreSQL、Oracle 或使用大量共享内存的 IPC 应用)中,我们经常会遇到系统响应突然变慢的情况。这时候,排查 Swap(交换分区) 占用是常规操作。
但很快你会发现一个令人头疼的问题:用 free -m 只能看到系统整体的 Swap;用 top 或 /proc/[pid]/status 只能看到单个进程的总 Swap 占用。
如果我想知道这个进程申请的「共享内存(Shared Memory)」里,到底有多少被无情地挤到了 Swap 里面?
默认的工具链并没有直接提供这个比例。今天我们就来拆解这个硬核指标的计算原理,并提供一个一键排查脚本。
一、 核心原理:信息隐藏在哪里?
Linux 的每个进程在内核中都有一个虚拟内存区域(VMA)链表。关于这些内存区域的详细使用情况,都实时记录在 /proc/[PID]/smaps 文件中。
如果我们打开一个进程的 smaps 文件,会看到很多类似下面的数据块:
7fc900000000-7fc904000000 rw-s 00000000 00:14 12345678 /dev/shm/my_shm (deleted)
Size: 65536 kB
KernelPageSize: 4 kB
MMUPageSize: 4 kB
Rss: 0 kB
Pss: 0 kB
Shared_Clean: 0 kB
Shared_Dirty: 0 kB
Private_Clean: 0 kB
Private_Dirty: 0 kB
...
Swap: 65536 kB
SwapPss: 65536 kB
Locked: 0 kB
Shmem: 65536 kB
要精准找出“共享内存被 Swap 的比例”,我们需要抓住三个关键特征:
- 共享属性标记:在 VMA 定义行(第一行),第二列是权限。如果包含
s(例如rw-s或r--s),代表这是一段共享内存映射(Shared Mapping)。 - Shmem 属性:现代 Linux 内核在
smaps中提供了Shmem:这一行,用来标记属于 tmpfs、SysV 共享内存或 POSIX 共享内存的尺寸。 - Swap 占用:
Swap:这一行直接记录了该虚拟内存段目前有多少 KB 被换出了物理内存。
只要我们遍历 /proc/[PID]/smaps,筛选出符合“共享”特征的内存段,将它们的 Size(或 Shmem)累加,再将对应的 Swap 累加,就能算出最精确的 Swap 占用比例。
二、 一键排查脚本
下面是一个可以直接在生产环境运行的 Shell 脚本。它使用 awk 高效解析 smaps,避免了对大进程进行分析时产生过高的 CPU 开销。
将以下代码保存为 check_shm_swap.sh:
#!/bin/bash
# -------------------------------------------------------------------------------
# 脚本名称: check_shm_swap.sh
# 功能描述: 分析指定进程的共享内存大小、被 Swap 换出的量以及占比
# 使用方法: sudo ./check_shm_swap.sh <PID>
# -------------------------------------------------------------------------------
# 确保以 root 权限运行(读取其他用户的 /proc/[pid]/smaps 需要 root)
if [ "$EUID" -ne 0 ]; then
echo "Error: Please run as root (using sudo)."
exit 1
fi
PID=$1
if [ -z "$PID" ]; then
echo "Usage: $0 <PID>"
exit 1
fi
if [ ! -d "/proc/$PID" ]; then
echo "Error: Process with PID $PID does not exist."
exit 1
fi
# 获取进程名称
PROCNAME=$(cat /proc/"$PID"/comm 2>/dev/null)
echo "=========================================================================="
echo " [分析报告] 进程 PID: $PID | 进程名: $PROCNAME"
echo "=========================================================================="
awk '
BEGIN {
total_shm_size = 0
total_shm_swap = 0
count = 0
}
# 匹配 VMA 头部行(例如: 7fc900000000-7fc904000000 rw-s 00000000 ...)
/^[0-9a-f]+-[0-9a-f]+/ {
# 在进入新 VMA 块之前,先结算上一个 VMA 块
if (is_shared == 1) {
total_shm_size += current_size
total_shm_swap += current_swap
if (current_swap > 0) {
details[count++] = sprintf("%-24s | Size: %8d KB | Swap: %8d KB | Path: %s", addr_range, current_size, current_swap, path)
}
}
# 初始化新 VMA 块的属性
addr_range = $1
perms = $2
# 提取映射路径/文件名
path = ""
for (i = 6; i <= NF; i++) {
path = path " " $i
}
sub(/^ /, "", path)
if (path == "") path = "[Anonymous Shared]"
current_size = 0
current_swap = 0
is_shared = 0
# 核心判断:
# 1. 权限第四位为 "s" (Shared)
# 2. 或者路径是 /dev/shm, /SYSV 开头的删除标记等共享内存典型路径
if (substr(perms, 4, 1) == "s" || path ~ /^\/dev\/shm/ || path ~ /^SYSV/) {
is_shared = 1
}
}
# 累加指标
/^Size:/ { current_size = $2 }
/^Swap:/ { current_swap = $2 }
/^Shmem:/ {
if ($2 > 0) {
is_shared = 1
}
}
END {
# 结算最后一个 VMA 块
if (is_shared == 1) {
total_shm_size += current_size
total_shm_swap += current_swap
if (current_swap > 0) {
details[count++] = sprintf("%-24s | Size: %8d KB | Swap: %8d KB | Path: %s", addr_range, current_size, current_swap, path)
}
}
print "--- 汇总数据 ---"
if (total_shm_size == 0) {
print "未检测到该进程挂载任何共享内存。"
exit 0
}
printf "共享内存总映射量 (Size): %12.2f MB (%d KB)\n", total_shm_size / 1024, total_shm_size
printf "共享内存被 Swap 换出量: %12.2f MB (%d KB)\n", total_shm_swap / 1024, total_shm_swap
ratio = (total_shm_swap / total_shm_size) * 100
printf "Swap 换出占比: %12.2f%%\n", ratio
print ""
if (count > 0) {
print "--- 换出细节 (Top 异常内存段) ---"
printf "%-24s | %-14s | %-14s | %s\n", "地址区间 (Address)", "总大小 (Size)", "Swap 占用", "路径/类型"
print "--------------------------------------------------------------------------------------------------------"
for (i = 0; i < count; i++) {
print details[i]
}
} else {
print "恭喜!该进程的所有共享内存目前全部驻留在物理内存中,未发生 Swap 换出。"
}
}
' /proc/"$PID"/smaps
使用示例
赋予执行权限并带 PID 运行(以进程号 12345 为例):
chmod +x check_shm_swap.sh
sudo ./check_shm_swap.sh 12345
输出效果类似于:
==========================================================================
[分析报告] 进程 PID: 12345 | 进程名: postgres
==========================================================================
--- 汇总数据 ---
共享内存总映射量 (Size): 16384.00 MB (16777216 KB)
共享内存被 Swap 换出量: 4096.00 MB (4194304 KB)
Swap 换出占比: 25.00%
--- 换出细节 (Top 异常内存段) ---
地址区间 (Address) | 总大小 (Size) | Swap 占用 | 路径/类型
--------------------------------------------------------------------------------------------------------
7f8a00000000-7f8e00000000 | 16777216 KB | 4194304 KB | /SYSV0052e2c1 (deleted)
三、 深度思考:共享内存被 Swap 意味着什么?
在 Linux 系统中,共享内存被 Swap 换出,通常是性能灾难的开始。
- 高频延迟抖动:共享内存的设计初衷是为了提供多进程间极速的数据交换(通常是纳秒级)。一旦部分页面被置换到磁盘的 Swap 分区,当进程再次尝试访问这些数据时,就会触发缺页中断(Page Fault),迫使内核从磁盘读取数据。这会导致原本平稳的接口响应时间(RT)突现数十毫秒甚至更长的毛刺。
- 连锁反应:以数据库为例,如果 PostgreSQL 的
shared_buffers发生 Swap,不仅查询会变慢,还会导致锁持有时间变长,进而引发整个系统的并发雪崩。
四、 如何根治和预防?
如果你发现你的核心进程共享内存 Swap 占比偏高,建议采取以下优化手段:
1. 调整系统的 Swappiness 倾向
Linux 内核的 vm.swappiness(默认通常是 60)控制了内核回收匿名内存(包括共享内存)与 Page Cache(文件缓存)的倾向。
- 临时调整(降低对 Swap 的依赖,更倾向于释放文件缓存):
sysctl vm.swappiness=10 - 永久生效:在
/etc/sysctl.conf中修改或添加vm.swappiness = 10。
2. 使用大页(HugePages)
这是解决共享内存被 Swap 的终极武器。
在 Linux 中,标准页大小为 4KB,而大页(HugePages)通常为 2MB 或 1GB。最关键的是:Linux 内核绝不允许 HugePages 被 Swap 换出。
如果你的应用(如 PostgreSQL、MySQL、Oracle)支持 HugePages,请务必开启它。这不仅能防止 Swap,还能减少内核页表(Page Tables)的开销,带来 10% 左右的性能提升。
3. 使用 mlock() 锁定内存
如果在编写核心服务,可以在代码中对共享内存区域调用 mlock() 或 mlockall()。
这会强制内核将这部分虚拟地址空间锁定在物理内存(RAM)中,绝对不参与 Swap 的分配。在应用层面,很多高性能组件(如 Elasticsearch、Redis)都提供了 bootstrap.memory_lock: true 这样的配置项,其底层原理正是如此。