Linux 性能优化
系统变慢时,“慢”往往只是表象,真正的瓶颈可能在 CPU、内存或磁盘 I/O 中的任意一处,而这三者又互相牵连:内存不足会引发 Swap 换页,进而变成磁盘 I/O 压力;磁盘慢又会让进程卡在不可中断状态,反过来推高 CPU 的平均负载。
所以性能分析的思路是固定的:先看指标定位到大方向,再用对应的工具逐层下钻到进程。本文按 CPU → 内存 → I/O 的顺序,梳理每一层的关键指标、背后的原理和常用工具。
CPU
CPU 这一层最常用的入口指标是平均负载,它便宜、直观,但也最容易被误读,所以先从它讲起,再深入到背后真正消耗 CPU 的上下文切换。
平均负载
平均负载可以简单理解为平均活跃进程数,准确地说,是处于可运行状态和不可中断状态的平均进程数:
- 可运行状态:正在使用 CPU 或者正在等待 CPU 的进程,也就是常用
ps命令看到的处于 R 状态(Running 或 Runnable)的进程 - 不可中断状态:正处于内核态关键流程中的进程,并且这些流程是不可打断的,最常见的是等待硬件设备的 I/O 响应,也就是
ps命令中看到的 D 状态(Uninterruptible Sleep,也称为 Disk Sleep)的进程
判断负载高不高,必须先知道有几个核。查看 CPU 个数:
1 | root@imwl01:~# grep 'model name' /proc/cpuinfo |wc -l |
4 核机器满负载对应的平均负载是 4,一般认为超过 70%(也就是 2.8)就可以开始排查 CPU 使用情况了。
用 uptime 查看:
1 | root@imwl01:~# uptime |
各字段含义:
15:45:41:当前时间up 44 min:开机时间2 user:登录用户数load average后的三个数:过去 1 分钟、5 分钟、15 分钟的平均负载
三个值放在一起看才有意义:上面这台机器三个值都很接近,说明负载平稳;如果 1 分钟的值明显高于 15 分钟,说明负载正在快速上升;反之则说明高峰已经过去。
平均负载升高有两种可能的原因:
- CPU 密集型程序导致
- I/O 更加繁忙(对应的是上面提到的不可中断状态进程变多)
两者在平均负载上看不出区别,需要用工具进一步区分:
mpstat:查看 CPU 整体及每个核的使用情况,如mpstat -P ALL 5 1pidstat:查看具体进程的使用情况,如pidstat -u 5 1
这两个命令由 sysstat 包提供,yum install -y sysstat 即可安装。
为什么三个数字要一起看
上面说”1 分钟明显高于 15 分钟就是负载在上升”,背后的依据是:这三个数不是简单的算术平均,而是指数移动平均(EWMA),各自带一个衰减常数。内核每 5 秒采样一次活跃进程数,然后按
1 | load = load * exp(-5/T) + n * (1 - exp(-5/T)) # T 取 60 / 300 / 900 秒 |
的方式往前滚。所以 1 分钟那个值衰减快、对突发敏感;15 分钟那个衰减慢、滞后明显。一次持续 1 分钟的尖峰过去之后,1 分钟值很快掉回去,15 分钟值还要拖十几分钟才慢慢回落——这才是”比较三个数就能看出趋势方向”的数学来源,而不是某种经验法则。
也正因为是 EWMA,它对刚刚开始的问题反应是钝的:一台机器负载真出问题的头十几秒,三个数可能都还很好看。
“load 很高但 CPU 很闲”
这是这个指标最常见的困惑,原因就在上面那句”包括不可中断状态”。D 状态的进程根本没在用 CPU,它在等 I/O,但一样计进 load。所以 NFS 卡住、磁盘故障、iSCSI 断链这类场景下,load 能飙到几十而 CPU 使用率接近 0。
mpstat/pidstat 能区分是 CPU 还是 I/O,但它们给的是”用了多少”,回答不了”被拖慢了多少”。更直接的答案是 PSI(Pressure Stall Information,4.20 引入):
1 | cat /proc/pressure/cpu |
它给出的是因为等某类资源而被拖慢的时间占比,some 是”有任务在等”,full 是”全都在等”。这个语义比 load 直接得多:load 是 12 说明不了什么,而 io full avg10=30 明确告诉你有 30% 的时间整机都卡在 I/O 上。cgroup v2 里每个 cgroup 也有自己的 *.pressure 文件,能一直定位到具体是哪个容器在制造压力。
CPU 上下文切换
Linux 是一个多任务操作系统,支持远大于 CPU 数量的任务同时进行(并发或者并行)。系统需要事先帮 CPU 设置好寄存器和程序计数器,从而使 CPU 知道任务从哪儿加载、从哪儿开始运行。
- CPU 寄存器:CPU 内置的容量小、速度极快的内存
- 程序计数器:用来存储 CPU 正在执行的指令位置,或者即将执行的下一条指令位置
这两者合起来就是 CPU 上下文。所谓 CPU 上下文切换,就是先把前一个任务的 CPU 上下文(也就是 CPU 寄存器和程序计数器)保存起来,然后加载新任务的上下文到这些寄存器和程序计数器,最后再跳转到程序计数器所指的新位置,运行新任务。
而这些保存下来的上下文,会存储在系统内核中,并在任务重新调度执行时再次加载进来。这样就能保证任务原来的状态不受影响,让任务看起来还是连续运行。
按切换对象的不同,可分为进程上下文切换、线程上下文切换和中断上下文切换三类。
进程上下文切换
进程的运行空间按特权等级分为内核空间和用户空间:
- 内核空间(Ring 0):可以访问所有资源
- 用户空间(Ring 3):只能访问受限资源,不能直接访问内存等硬件设备,需要通过系统调用陷入到内核中,才能访问这些特权资源
进程在用户空间运行时被称为进程的用户态,而陷入内核空间的时候被称为进程的内核态。从用户态到内核态的转变,需要通过系统调用来完成。
系统调用时,CPU 寄存器里原来用户态的指令位置需要先保存起来;接着,为了执行内核态代码,CPU 寄存器需要更新为内核态指令的新位置;最后才是跳转到内核态运行内核任务。系统调用结束后,CPU 寄存器需要恢复原来保存的用户态,然后再切换到用户空间继续运行进程。所以一次系统调用其实发生了两次 CPU 上下文切换:用户态 → 内核态 → 用户态。
不过要注意,系统调用过程中一直是同一个进程在运行,因此它通常被称为特权模式切换,而不是上下文切换。
真正的进程上下文切换,是指从一个进程切换到另一个进程:
1 | 进程 1 → 保存进程 1 的上下文 → 加载进程 2 的上下文 → 进程 2 |
线程上下文切换
线程是调度的基本单位,而进程则是资源拥有的基本单位,进程只是给线程提供虚拟内存、全局变量等资源。
- 当进程只有一个线程时,可以认为进程就等于线程
- 当进程拥有多个线程时,这些线程会共享相同的虚拟内存和全局变量等资源,这些资源在上下文切换时是不需要修改的
- 线程也有自己的私有数据,比如栈和寄存器等,这些在上下文切换时是需要保存的
因此线程上下文切换分两种情况:
- 两个线程属于不同进程,切换过程就等同于进程上下文切换
- 两个线程属于同一进程,因为虚拟内存是共享的,切换时这些资源保持不动,只需要切换线程的私有数据、寄存器等不共享的数据
所以同一进程内线程之间切换消耗的资源,比进程之间切换要少。
中断上下文切换
中断处理会打断进程的正常调度和执行。对于同一个 CPU 来说,中断处理比进程拥有更高的优先级。
中断上下文切换并不涉及到进程的用户态,所以即便中断过程打断了一个正处在用户态的进程,也不需要保存和恢复这个进程的虚拟内存、全局变量等用户态资源。中断上下文其实只包括内核态中断服务程序执行所必需的状态,包括 CPU 寄存器、内核堆栈、硬件中断参数等。
自愿与非自愿上下文切换
- 自愿上下文切换:是指进程无法获取所需资源导致的上下文切换。比如 I/O、内存等系统资源不足时,就会发生自愿上下文切换
- 非自愿上下文切换:是指进程由于时间片已到等原因,被系统强制调度而发生的上下文切换。比如大量进程都在争抢 CPU 时,就容易发生非自愿上下文切换
vmstat 可以分析系统内存使用情况以及 CPU 上下文切换和中断的次数。看到异常时,可以按下面的对应关系判断:
- 自愿上下文切换变多了,说明进程都在等待资源,有可能发生了 I/O 等其他问题
- 非自愿上下文切换变多了,说明进程都在被强制调度,也就是都在争抢 CPU,说明 CPU 的确成了瓶颈
- 中断次数变多了,说明 CPU 被中断处理程序占用,还需要通过查看
/proc/interrupts文件来分析具体的中断类型
DMA
Direct Memory Access(DMA)模块允许硬件设备直接通过 DMA 写内存,而不需要通过 CPU(占用 CPU 资源)。设备与内存之间的大批量数据搬运交给 DMA 之后,CPU 只需要在传输完成时处理一次中断,这也是减少中断和上下文切换开销的重要手段。
常用工具的输出解读
前面反复提到 mpstat、pidstat 和 vmstat,这里把它们的输出字段集中解释一遍。
mpstat
1 | imwl@imwl:~$ mpstat |
括号里为 top 等工具中的常用缩写:
- user:用户态 CPU 时间,包含 guest(us)
- nice:低优先级(1-19)用户态 CPU 时间。优先级取值 -20 ~ 19,数值越高优先级越低(ni)
- system:内核态 CPU 时间(sy)
- idle:空闲时间,不包括等待 I/O 的时间(id)
- iowait:等待 I/O 的 CPU 时间(wa)
- irq:处理硬中断的 CPU 时间(hi)
- softirq:处理软中断的 CPU 时间(si)
- steal:当系统运行在虚拟机中的时候,被其他虚拟机占用的 CPU 时间(st)
- guest:代表通过虚拟化运行其他操作系统的时间,也就是运行虚拟机的 CPU 时间(guest)
- guest_nice:以低优先级运行虚拟机的时间(gnice)
其中 CPU 使用率 = 1 - 空闲时间 / 总 CPU 时间。iowait 偏高就是把排查方向指向磁盘 I/O 的信号。
pidstat
1 | imwl@imwl:~$ pidstat |
- UID:用户 ID
- PID:进程识别号
- PPID:当前进程的父进程 ID
vmstat
1 | [root@test-1 ~]# vmstat |
vmstat 一条输出就横跨了本文的三个主题:procs 段的 r、b 对应可运行和不可中断进程数,memory、swap 段对应内存与换页,io 段对应块设备读写,system 段的 in、cs 就是中断和上下文切换次数。
内存
如果 CPU 指标正常但系统依然卡顿,下一个要看的就是内存。内存问题的麻烦之处在于它很少单独出现:不够用会触发回收和 Swap,最终表现为磁盘 I/O 变慢和进程被 OOM 杀死。
查看系统内存使用情况
使用 free,默认单位为 KB:
1 | root@imwl01:~# free |
- total:总内存大小
- used:已使用内存的大小,包含了共享内存
- free:未使用内存的大小
- shared:共享内存的大小,
tmpfs就是一种特殊的缓存 - buff/cache:缓存和缓冲区的大小。Buffer 是对磁盘数据的缓存,而 Cache 是文件数据的缓存,它们既会用在读请求中,也会用在写请求中
- available:新进程可用内存的大小,包含未使用 + 可回收
要按进程看,使用 top 后按 M 以内存排序:
1 | root@imwl01:~# top |
- VIRT:进程虚拟内存的大小,只要是进程申请过的内存,即便还没有真正分配物理内存,也会计算在内
- RES:常驻内存的大小,也就是进程实际占用的物理内存大小,不包括已经被换出到 Swap 的部分,但包括共享内存,也就是说 SHR 是 RES 的一部分(上面
node进程的 RES 是 84952、SHR 是 34176,%MEM的 2.1% 也是按 RES 除以总内存 3907.9MiB 算出来的) - SHR:共享内存的大小,比如与其他进程共同使用的共享内存、加载的动态链接库以及程序的代码段等
- %MEM:进程使用物理内存占系统总内存的百分比
由于 RES 会重复统计共享部分,直接累加各进程的 RES 会偏大。要统计所有进程使用的内存总量,应该用 Pss,即把共享内存平分到各个进程后,再加上进程本身的非共享内存大小的和:
1 | # 使用 grep 查找 Pss 指标后,再用 awk 计算累加值 |
不想自己拼命令的话,直接 smem -t 也能拿到按进程列出的 Pss 汇总。
虚拟内存
内存是稀缺的,随着应用膨胀,进程对内存的需求会越来越大,虚拟化技术就是为了解决内存不够用的问题。
虚拟化技术中,操作系统设计了虚拟内存(理论上可以无限大的空间),实际受限于 CPU 的处理能力,通常 64bit CPU 就是 2**64 个地址。应用使用的是虚拟内存,操作系统管理虚拟内存和真实内存之间的映射。
操作系统会将虚拟内存分成整齐的小块,每个小块称为一个页(Page)。之所以这样做,原因主要有以下两个方面:
- 一方面应用使用内存是以页为单位,整齐的页能够避免内存碎片问题
- 另一方面,每个应用都有高频使用的数据和低频使用的数据。这样做,操作系统就不必从应用角度去思考哪个进程是高频的,仅需思考哪些页被高频使用、哪些页被低频使用。如果是低频使用,就将它们保存到硬盘上;如果是高频使用,就让它们保留在真实内存中
因此如果一个应用需要非常大的内存,它申请的是虚拟内存中的很多个页,真实内存不一定需要够用。
大页则是指比普通页(4KB)更大的内存块,常见的大小有 2MB 和 1GB,通常用在使用大量内存的进程上,比如 Oracle、DPDK 等。

内存映射
- 物理内存:只有内核才可以直接访问
- 虚拟内存:Linux 内核给每个进程都提供了一个独立的虚拟地址空间,并且这个地址空间是连续的
内存映射其实就是将虚拟内存地址映射到物理内存地址。

进程的虚拟内存空间按用途划分为若干段:

内存分配与回收
分配
对小块内存(小于 128K),C 标准库使用 brk() 来分配,也就是通过移动堆顶的位置来分配内存。这些内存释放后并不会立刻归还系统,而是被缓存起来,这样就可以重复使用。
而大块内存(大于 128K),则直接使用内存映射 mmap() 来分配,也就是在文件映射段找一块空闲内存分配出去。
回收
内存紧张时,系统会按代价从低到高依次采取三种手段:
- 回收缓存,比如使用 LRU(Least Recently Used)算法,回收最近使用最少的内存页面
- 回收不常访问的内存,把不常用的内存通过交换分区直接写到磁盘中
- 杀死进程,内存紧张时系统还会通过 OOM(Out of Memory)直接杀掉占用大量内存的进程
内存泄漏
并不是所有内存区域都会泄漏:
- 只读段,包括程序的代码和常量,由于是只读的,不会再去分配新的内存,所以也不会产生内存泄漏
- 数据段,包括全局变量和静态变量,这些变量在定义时就已经确定了大小,所以也不会产生内存泄漏
- 栈,虽然大小一直在变,但栈帧会随着函数返回、局部变量出作用域自动弹出,回收由系统负责,所以也不会泄漏(栈这边的典型问题是溢出,不是泄漏)
真正会泄漏的是堆和内存映射段:堆上的内存要由程序显式 free,内存映射段里由程序自己动态分配和管理的那部分(比如共享内存)也一样。所以,如果程序在分配后忘了回收,就会导致泄漏问题。
Swap
Swap 技术允许把暂时用不到的内存数据先放到磁盘上,把物理内存腾出来给需要的进程。需要注意的是,把「完整的进程数据(正文段、数据段、堆栈段等)」整体换出去,是古早的整进程交换(swapping);Linux 的换出是以页为粒度的,而且只有匿名页(堆、栈这类没有文件后备的内存)才会被写入 swap。文件页则是干净的直接回收、脏的回写到原来的文件里,程序的正文段属于文件页,回收时直接丢弃,下次需要再从可执行文件里重新读进来,并不占用 swap。
它包含两个方向的动作:
- 换出:把进程暂时不用的内存数据存储到磁盘中,并释放这些数据占用的内存
- 换入:进程再次访问这些内存的时候,把它们从磁盘读到内存中来
按载体分为 swap 分区和 swap 文件两种类型。
Swap 技术在性能上存在着碎片、频繁切换等明显劣势。使用 Swap 技术,程序员需要清楚地知道自己的应用用多少内存,并且小心翼翼地使用内存,避免需要重新申请,或者研发不断扩容的算法,因此一般不推荐使用 Swap。
相关的两个内核参数:
/proc/sys/vm/min_free_kbytes:一旦剩余内存小于页低阈值,就会触发内存的回收/proc/sys/vm/swappiness:调整文件页和匿名页的回收倾向,取值 0-100,越低越不倾向使用Swap。但即便设为 0,当剩余内存 + 文件页小于页高阈值时,仍然会发生Swap
缓存命中率
缓存命中率指通过缓存获取数据的请求次数,占所有数据请求次数的百分比。因为内存的数据读写比磁盘读写速度快得多,所以可以依赖缓存提升速度,命中率也就成了衡量缓存是否有效的直接指标。
内存优化常见思路
- 禁止 Swap。如果必须开启 Swap,降低
swappiness的值,减少内存回收时 Swap 的使用倾向 - 减少内存的动态分配。比如可以使用内存池、大页(HugePage)等
- 尽量使用缓存和缓冲区来访问数据。比如可以使用堆栈明确声明内存空间,来存储需要缓存的数据;或者用 Redis 这类的外部缓存组件,优化数据的访问
- 使用 cgroups 等方式限制进程的内存使用情况。这样可以确保系统内存不会被异常进程耗尽
- 通过
/proc/pid/oom_adj调整核心应用的oom_score,oom_score越小,进程就越不容易被系统杀死
定位流程与工具对照



磁盘 I/O
磁盘为系统提供了最基本的持久化存储,而文件系统则在磁盘的基础上,提供了一个用来管理文件的树状结构。绝大多数应用并不直接和磁盘打交道,而是通过文件系统间接访问。
这条路径上有两种走法,也正好解释了前面 free 输出里 Buffer 与 Cache 的区别:
- 读写普通文件时,I/O 请求会首先经过文件系统,然后由文件系统负责来与磁盘进行交互,对应的缓存是 Cache
- 而在读写块设备文件时,会跳过文件系统,直接与磁盘交互,也就是所谓的“裸 I/O”,对应的缓冲是 Buffer
这个区别光看结论容易记反,跑两组对照实验就再也不会混了。开一个终端挂 vmstat 1 盯住 buff 和 cache 两列,另一个终端分别做:
1 | # 读块设备 → buff 列涨(注意选一块空闲盘,别对着挂载中的分区乱写) |
写方向同理:dd of=/dev/sdb1 涨 buff,dd of=/tmp/bigfile 涨 cache。每组之间记得 echo 3 > /proc/sys/vm/drop_caches 清一下,否则第二次读全命中缓存,什么都看不出来。
有一层历史背景值得补上,否则很容易把这两个词理解成两套独立的内存池:Linux 2.4 以后 buffer cache 和 page cache 已经合并了。今天的 “Buffer” 其实就是 page cache 中归属块设备的那一部分,底层是同一套页面管理。这解释了两件事:为什么 free 干脆把它们合成 buff/cache 一列显示;以及为什么 drop_caches 会把两者一起清掉——它们本来就是一个子系统。
正因为磁盘比内存慢几个数量级,I/O 问题很少以“磁盘慢”的形式直接暴露,而是通过前两节的指标间接体现出来:进程卡在 D 状态推高平均负载、mpstat 里的 iowait 升高、vmstat 的 b 列和 bi/bo 列变大、内存不足触发 Swap 换页又制造出额外的磁盘读写。因此排查 CPU 或内存问题时,始终要把 I/O 作为可能的根因一起考虑。