HOOOS

Linux 性能调优:如何精准查看特定进程的共享内存被 Swap 占用的比例?

0 4 架构运维手记 LinuxSwap性能调优
Apple

在 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 的比例”,我们需要抓住三个关键特征:

  1. 共享属性标记:在 VMA 定义行(第一行),第二列是权限。如果包含 s(例如 rw-sr--s),代表这是一段共享内存映射(Shared Mapping)。
  2. Shmem 属性:现代 Linux 内核在 smaps 中提供了 Shmem: 这一行,用来标记属于 tmpfs、SysV 共享内存或 POSIX 共享内存的尺寸。
  3. 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 换出,通常是性能灾难的开始。

  1. 高频延迟抖动:共享内存的设计初衷是为了提供多进程间极速的数据交换(通常是纳秒级)。一旦部分页面被置换到磁盘的 Swap 分区,当进程再次尝试访问这些数据时,就会触发缺页中断(Page Fault),迫使内核从磁盘读取数据。这会导致原本平稳的接口响应时间(RT)突现数十毫秒甚至更长的毛刺。
  2. 连锁反应:以数据库为例,如果 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 这样的配置项,其底层原理正是如此。

点评评价

captcha
健康