MP操作系统调试方法体系:从观测到定位的完整路径
第一层:全局观测——知道"系统怎么了"
1.1 内存全景:/proc/meminfo 解读
/proc/meminfo 是内存问题的第一现场。但字段众多,日常只需关注以下核心指标:
| 字段 | 含义 | 运维要点 |
|---|---|---|
| MemAvailable | 应用程序实际可用内存 | ✅ 判断内存是否充足只看这个,MemFree 低是正常的 |
| SwapFree | 剩余可用 Swap | ⚠️ 持续下降说明物理内存压力大,正在频繁换页 |
| Dirty + Writeback | 脏页 + 正在刷盘的页面 | 持续 > 数百 MB 说明磁盘 I/O 存在瓶颈 |
| SUnreclaim | Slab 中不可回收部分 | 真正的内核固定开销,单调递增即内核泄漏 |
| Committed_AS vs CommitLimit | 已承诺分配 vs 允许上限 | 接近时新进程可能 OOM |
| HardwareCorrupted | 硬件检测到的内存损坏页数 | 🚨 非零需立即排查硬件故障 |
速查口诀:
内存够不够 → 看 MemAvailable
有没有换页压力 → 看 SwapFree 变化趋势
磁盘 IO 卡不卡 → 看 Dirty + Writeback
内核吃多少 → 看 SUnreclaim + KernelStack + PageTables
会不会 OOM → 看 Committed_AS vs CommitLimit
硬件坏没坏 → 看 HardwareCorrupted
内存泄漏类型判断(时序分析)
单次快照无法区分泄漏与正常波动,必须采集时序数据(至少 30 分钟、每 60s 采样)。决策流程:
MemAvailable 持续下降?
├─ 否 → 无泄漏
└─ 是 → drop_caches 后是否恢复?
├─ 是 → 缓存行为,非泄漏
└─ 否 → AnonPages 是否同步增长?
├─ 是 → ✅ 用户态泄漏(查进程 RSS)
└─ 否 → SUnreclaim 是否同步增长?
├─ 是 → ✅ 内核 Slab 泄漏(查 /proc/slabinfo)
└─ 否 → 计算残差是否持续增长?
├─ 是 → ✅ 内核 Buddy 泄漏
└─ 否 → 检查 HardwareCorrupted 等罕见字段
💡 关键前提:判断前先执行
echo 2 > /proc/sys/vm/drop_caches排除缓存干扰,若 drop 后指标回落则非泄漏。
1.2 CPU 与进程概览:top 命令
当内存问题初步定性后,需要找到"谁在消耗资源"。top 是最直接的入口,日常排查优先使用交互模式,批处理模式仅用于脚本采集。
交互模式核心操作
启动 top 后,以下快捷键覆盖了绝大多数实时排查场景:
| 按键 | 功能 | 典型用途 |
|---|---|---|
P | 按 %CPU 排序 | 快速定位 CPU 热点进程 |
M | 按 %MEM 排序 | 快速定位内存消耗大户 |
T | 按 TIME+ 排序 | 找累计运行时间最长的进程 |
N | 按 PID 排序 | 恢复默认排序 |
H | 切换进程/线程视图 | 某进程 CPU 飙高时看具体哪个线程 |
1 | 展开每个 CPU 核心 | 判断负载是否集中在单核 |
c | 显示完整命令路径 | 区分同名进程 |
f | 选择显示的列 | 添加/移除关注字段 |
k | 向选中进程发送信号 | 紧急终止异常进程(默认 SIGTERM) |
r | 调整进程优先级 | renice 降低干扰进程优先级 |
E | 切换内存单位 | KB/MB/GB 切换便于阅读 |
q | 退出 | — |
聚焦指定进程
top -p <PID> # 只看单个进程
top -p 1234,5678 # 看多个进程(逗号分隔)
top -H -p $(pgrep -x java) # 查看 java 所有线程
💡 进入交互界面后仍可叠加使用上述快捷键,例如
-p <PID>进入后再按H即可聚焦单进程的线程级 CPU 分布。
头部信息关键字段
load average:1/5/15 分钟平均负载,超过 CPU 核数需关注us/sy/id/wa:用户态/内核态/空闲/IO 等待 CPU 占比avail Mem:应用程序实际可用内存(比 free 更准确)
批处理模式(脚本/自动化采集)
仅在需要程序化采集或写入日志时使用,人工排查不推荐:
top -b -n 1 -o %CPU | head -10 # 按 CPU 降序取 Top10
top -b -n 1 -o RES | head -10 # 按物理内存降序取 Top10
💡 top 交互模式是实时动态的,能观察趋势和瞬时变化;批处理只是某一时刻的静态快照。日常排查应养成在交互界面中操作的习惯,而非依赖
-b -n 1。
第二层:进程级定位——知道"哪个模块出了问题"
全局观测确定了资源瓶颈方向后,下一步是缩小范围到具体进程和模块。这一层的核心工具是 MP 操作系统提供的专用诊断命令。
2.1 内存泄露:定位到进程和函数
哪个进程在泄露
show diagnose memory top
一般来说 mpos 占大头;如果 aaa、ipv6 之类的进程占大头,很明显就是这个异常的进程在泄露。如果在 mpos 泄露,就需要进一步定位到是哪个模块的问题。
哪个函数在泄露
操作系统通过 pub.so 提供了内存申请和释放接口,替代 glibc 的 malloc/free,并提供统计工具。
# 申请了 1M 以上的函数地址
show slab alloc exclude mega-bytes-se 1
# 申请了 10000 次以上的函数地址
show slab alloc exclude blocks-se 10000
# 根据函数地址查函数名
display symbol addr xxxxxxxx
# 根据函数名查地址
display symbol sym xxxxxxxx
增量观测(精确定位增长源)
slab alloc statistics save # 记录当前状态
# ... 等待一段时间 ...
show slab alloc increment summay # 查看增量
C库缓存确认
show diagnose memory anon-mem-info
system bytes是申请的内存,in use bytes是使用中的内存,差值即为缓存。若缓存异常大,可能是 C 库内部碎片或缓存策略问题。
2.2 内存越界:detect 与 efence
内存越界的难点在于:发生问题的模块不一定是出问题的模块(可能是别人往他的内存写数据了)。
分析思路
- 通过在内存块前后放置红区来判断
- 先区分是主动 free 被检测到,还是越界写了别人的内存
- 关键是先找到问题内存块地址,再通过 gdb 查看前后红区情况
- 如果是 free 被检测到,区分通用池和专用池(布局有差异)
两个核心工具
# detect:动态监测,可检测越界但还没释放的内存(开销较小)
configure terminal
dfm mem detect-sshd on
# efence:精准抛出越界第一现场(开销大,底层用 mprotect)
configure terminal
dfm mem efence-sshd on
定位技巧
- 前管理区不对 → 大概率是前一块内存写越界了,向前找
- 后管理区不对 → 大概率是当前 caller 有问题
- 专用池:问题内存块前后块挨着;通用池:前后块可能隔很远
- 异常内存值有特征时,可通过 gdb 的
find指令搜索该模式
2.3 重复释放与释放后使用
重复释放
通用池重复释放会直接报出 double free 的 caller。两种情况:
- 自己的指针释放两次
- 释放了别人的指针
如果内存已被还给内核,不会打印明确信息。此时 /proc/<pid>/maps 可查看该进程是否还有报错的内存地址,如果没有则可能是重复释放。
检测机制:红区释放时取反,缓存过后毒化为 6b6b6b6b。
延长缓存时间增大检出率
dfm mem free-use-processname
将通用池内存块释放后仍缓存指定时间。如果看到 6b6b6b6b,也要考虑是否是释放后仍在使用的问题。
2.4 信号量死锁
多个进程/线程彼此等待对方释放信号量,导致所有相关进程都无法继续执行。
排查四步法
- 找出线程1拥有的信号量和等待的信号量1
- 找出信号量1的拥有者线程2
- 找出线程2拥有的信号量和等待的信号量2
- 重复直到查清完整等待环
调试命令
# 查看互斥锁使用情况,pended 代表有挂起的线程
show semaphore mutex pended summary
# trace 上一步得到的 owner
trace <owner>
# gnl_sem_pub.h 接口
show diagnose gnl semaphore mutex pend
show diagnose gnl semaphore
2.5 CPU 异常:死循环与调度问题
关键思路
- CPU 占用高 → 系统会自动检测输出日志,也可手动追踪
- 线程得不到调度 → 确认是否在等待资源;若不是,查调度统计信息
- 总线程调度少 → 梳理业务框架,是否锁过多或使用不合理
调试命令
# MPOS
spy cpu
show cpu # 各线程 CPU 占用
show cpu monitor # CPU 随时间变化趋势
router#trace pthreadname # 问题线程堆栈
# Linux
top # 配合 -H 看线程级
# 兜底手段(shell 不可用时)
sysrq TASK-CPU-INFO(a)
💡 遇到此类问题尽量多输出几次线程堆栈信息,因为堆栈输出过程漫长有滞后性,多次采样才好定位。
2.6 线程栈异常
判断流程
- 先看是否有明确提示
Detect stack overflow - 即使栈溢出,栈使用占比也不一定显示 100%,最根本要看
/proc/xxx/maps确认异常地址是否在正常栈范围 - 其他栈异常:gdb 中
info registers找到 sp,然后x/1280x $sp尽量多收集附近信息 - 栈异常大概率栈信息显示
??,无法解析函数符号
2.7 其他诊断工具
| 用途 | 命令 |
|---|---|
| 工作线程 job 信息 | show diagnose util workthread xxx |
| 文件 fd 占用信息 | show diagnose util kernelfd xxx |
| 日志收集 | collect diagnose sysinfo xxx |
第三层:代码级根因——知道"哪一行代码写的bug"
前两层将问题缩小到了具体进程、函数甚至内存地址。但要真正修复 bug,还需要回到源码层面。这一层的核心能力是:将运行时地址映射回源码行号。
3.1 GDB Debug Info:符号与源码的桥梁
编译选项选择
| 阶段 | 推荐选项 | 说明 |
|---|---|---|
| 开发 | -g3 -O0 | 全量调试信息,源码与指令一一对应 |
| 测试/预发 | -g2 -O1 | 平衡性能与调试能力 |
| 生产发布 | 分离 debug + strip | 保留 .gnu_debuglink,按需加载 |
为什么生产环境要分离 Debug 信息
- 减小发布体积:debug 信息通常占二进制 50%~80%
- 保护源码:避免源码路径、变量名泄露
- 按需加载:GDB 仅在需要时加载,减少内存占用
- 独立更新:代码小改无需重新分发完整 debug 包
分离操作
# 1. 提取 debug 信息
objcopy --only-keep-debug demo demo.debug
# 2. 剥离原文件 debug 段
objcopy --strip-debug demo demo_stripped
# 3. 添加关联链接(让 GDB 自动找到)
objcopy --add-gnu-debuglink=demo.debug demo_stripped
GDB 查找 debug 文件的顺序
- 同目录下
demo_stripped.debug .debug/子目录/usr/lib/debug/系统目录- build-id 匹配(
.build-id/xx/yyyy.debug)
手动指定:
(gdb) symbol-file demo.debug
(gdb) set debug-file-directory /path/to/debug/files
⚠️ 关键提醒:debug 文件必须与对应二进制同一次编译产物,否则 CRC 校验失败或符号错位。
3.2 动态库崩溃定位:addr2line
应用程序函数地址固定,但动态库地址不固定。定位方法:
- 根据打印的动态库地址和挂机点函数地址算出偏移
- 用 addr2line 定位具体行号
# x86 直接用,非 x86 在 /config/product_config 找工具路径
addr2line -e out/image/libso/cli/libmem-cli.so <偏移>
3.3 无 core dump 时的寄存器现场分析
当只有 oops 日志中的寄存器和栈信息时,按以下优先级分析:
第一步:ERR + CR2 定性错误类型
| ERR 值 | CR2 特征 | 结论 |
|---|---|---|
0x04 | ≈ 0x0 | 空指针读 |
0x05 | ≈ 0x0 | 空指针写 |
0x07 | 合法地址 | 写只读/COW 失败 |
0x15 | 数据段地址 | NX 违规(执行了非代码内存) |
0x06 | 合法地址 | 读权限违规(ASan redzone / mprotect) |
第二步:RIP 定位源码
addr2line -e /path/to/binary -f -C <RIP地址>
第三步:GPR 还原参数(x86_64 ABI)
| 寄存器 | 角色 | 崩溃时检查要点 |
|---|---|---|
| RDI | arg1 | 是否为 NULL / 野指针 |
| RSI | arg2 | 同上;若为长度值,是否异常大 |
| RDX | arg3 | 计数器/偏移量,是否溢出 |
| RAX | 返回值 | 前一调用返回 NULL/错误码后未检查 |
| RBP | 帧指针 | 若为 0 或非栈地址 → 栈已损坏 |
💡 核心原则:先定性(ERR+CR2)再定位(RIP+addr2line),用固定 ABI 约定弥补缺失的动态内存视图。
第四层:防御性编程——让 bug 不再发生
调试能力的终极目标是减少调试需求。以下是从上述所有案例中提炼的编码规范。
4.1 内存安全
- 谁分配,谁释放——从源头控制
- 推动分配接口可追溯——避免新增公共内存分配接口,如确需新增,需支持传入调用者信息
- 统一内存接口封装——提升可维护性与可观测性
- 严格边界检查——malloc 写入不超过分配大小;数组索引有效性校验;size 为变量时必须检测范围
- 交付前用 efence 检测——使用
sdt_efence_pub.h提供的接口,正式提交时改回原接口
4.2 并发安全
- 避免信号量保护范围过大
- 获取和释放成对,不要遗漏
- 避免死锁:
- 切断外部模块持锁再获取自己锁的可能
- 避免持自己锁再同步调用外部模块接口
- 全新代码使用
gnl_sem_pub.h接口
4.3 可调试性设计
- 生产构建保留符号关联:strip +
.gnu_debuglink,确保崩溃时可回溯 - CI/CD 自动归档 debug 文件到符号服务器
- 开启 ASan 做 CI 门禁:
-fsanitize=address在测试阶段拦截内存错误 - 日志分级与结构化:关键操作留痕,便于事后关联分析
附录:调试工具速查表
| 层级 | 工具/命令 | 用途 |
|---|---|---|
| 全局观测 | /proc/meminfo | 内存全景与泄漏类型判断 |
| 全局观测 | top 交互模式(P/M/H/1) | CPU/内存热点进程、线程级分析 |
| 全局观测 | free -h / slabtop -o | 内存摘要 / Slab 详情 |
| 进程定位 | show diagnose memory top | MP 进程级内存占用 |
| 进程定位 | show slab alloc exclude ... | MP 函数级内存分配统计 |
| 进程定位 | dfm mem detect/efence | MP 内存越界检测 |
| 进程定位 | dfm mem free-use-processname | MP 重复释放/UAF 检测 |
| 进程定位 | show semaphore mutex pended summary | MP 信号量死锁排查 |
| 进程定位 | spy cpu / show cpu | MP CPU 占用追踪 |
| 代码定位 | addr2line -e <binary> <offset> | 地址→源码行号 |
| 代码定位 | gdb + symbol-file | 交互式调试与符号加载 |
| 代码定位 | objcopy --only-keep-debug | 分离 debug 信息 |
| 代码定位 | ERR+CR2+RIP+GPR | 无 core dump 时的现场分析 |
| 预防 | -fsanitize=address | CI 阶段内存错误拦截 |
| 预防 | sdt_efence_pub.h | 交付前越界检测 |