蔚淦丞
发布于 2026-11-05 / 6 阅读
0
0

MP操作系统调试方法体系:从观测到定位的完整路径

MP操作系统调试方法体系:从观测到定位的完整路径


第一层:全局观测——知道"系统怎么了"

1.1 内存全景:/proc/meminfo 解读

/proc/meminfo 是内存问题的第一现场。但字段众多,日常只需关注以下核心指标:

字段含义运维要点
MemAvailable应用程序实际可用内存✅ 判断内存是否充足只看这个,MemFree 低是正常的
SwapFree剩余可用 Swap⚠️ 持续下降说明物理内存压力大,正在频繁换页
Dirty + Writeback脏页 + 正在刷盘的页面持续 > 数百 MB 说明磁盘 I/O 存在瓶颈
SUnreclaimSlab 中不可回收部分真正的内核固定开销,单调递增即内核泄漏
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

内存越界的难点在于:发生问题的模块不一定是出问题的模块(可能是别人往他的内存写数据了)。

分析思路

  1. 通过在内存块前后放置红区来判断
  2. 先区分是主动 free 被检测到,还是越界写了别人的内存
  3. 关键是先找到问题内存块地址,再通过 gdb 查看前后红区情况
  4. 如果是 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。两种情况:

  1. 自己的指针释放两次
  2. 释放了别人的指针

如果内存已被还给内核,不会打印明确信息。此时 /proc/<pid>/maps 可查看该进程是否还有报错的内存地址,如果没有则可能是重复释放。

检测机制:红区释放时取反,缓存过后毒化为 6b6b6b6b。

延长缓存时间增大检出率

dfm mem free-use-processname

将通用池内存块释放后仍缓存指定时间。如果看到 6b6b6b6b,也要考虑是否是释放后仍在使用的问题。

2.4 信号量死锁

多个进程/线程彼此等待对方释放信号量,导致所有相关进程都无法继续执行。

排查四步法

  1. 找出线程1拥有的信号量和等待的信号量1
  2. 找出信号量1的拥有者线程2
  3. 找出线程2拥有的信号量和等待的信号量2
  4. 重复直到查清完整等待环

调试命令

# 查看互斥锁使用情况,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 线程栈异常

判断流程

  1. 先看是否有明确提示 Detect stack overflow
  2. 即使栈溢出,栈使用占比也不一定显示 100%,最根本要看 /proc/xxx/maps 确认异常地址是否在正常栈范围
  3. 其他栈异常:gdb 中 info registers 找到 sp,然后 x/1280x $sp 尽量多收集附近信息
  4. 栈异常大概率栈信息显示 ??,无法解析函数符号

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 文件的顺序

  1. 同目录下 demo_stripped.debug
  2. .debug/ 子目录
  3. /usr/lib/debug/ 系统目录
  4. 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

应用程序函数地址固定,但动态库地址不固定。定位方法:

  1. 根据打印的动态库地址和挂机点函数地址算出偏移
  2. 用 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)

寄存器角色崩溃时检查要点
RDIarg1是否为 NULL / 野指针
RSIarg2同上;若为长度值,是否异常大
RDXarg3计数器/偏移量,是否溢出
RAX返回值前一调用返回 NULL/错误码后未检查
RBP帧指针若为 0 或非栈地址 → 栈已损坏

💡 核心原则:先定性(ERR+CR2)再定位(RIP+addr2line),用固定 ABI 约定弥补缺失的动态内存视图。


第四层:防御性编程——让 bug 不再发生

调试能力的终极目标是减少调试需求。以下是从上述所有案例中提炼的编码规范。

4.1 内存安全

  1. 谁分配,谁释放——从源头控制
  2. 推动分配接口可追溯——避免新增公共内存分配接口,如确需新增,需支持传入调用者信息
  3. 统一内存接口封装——提升可维护性与可观测性
  4. 严格边界检查——malloc 写入不超过分配大小;数组索引有效性校验;size 为变量时必须检测范围
  5. 交付前用 efence 检测——使用 sdt_efence_pub.h 提供的接口,正式提交时改回原接口

4.2 并发安全

  1. 避免信号量保护范围过大
  2. 获取和释放成对,不要遗漏
  3. 避免死锁:
    • 切断外部模块持锁再获取自己锁的可能
    • 避免持自己锁再同步调用外部模块接口
  4. 全新代码使用 gnl_sem_pub.h 接口

4.3 可调试性设计

  1. 生产构建保留符号关联:strip + .gnu_debuglink,确保崩溃时可回溯
  2. CI/CD 自动归档 debug 文件到符号服务器
  3. 开启 ASan 做 CI 门禁:-fsanitize=address 在测试阶段拦截内存错误
  4. 日志分级与结构化:关键操作留痕,便于事后关联分析

附录:调试工具速查表

层级工具/命令用途
全局观测/proc/meminfo内存全景与泄漏类型判断
全局观测top 交互模式(P/M/H/1)CPU/内存热点进程、线程级分析
全局观测free -h / slabtop -o内存摘要 / Slab 详情
进程定位show diagnose memory topMP 进程级内存占用
进程定位show slab alloc exclude ...MP 函数级内存分配统计
进程定位dfm mem detect/efenceMP 内存越界检测
进程定位dfm mem free-use-processnameMP 重复释放/UAF 检测
进程定位show semaphore mutex pended summaryMP 信号量死锁排查
进程定位spy cpu / show cpuMP CPU 占用追踪
代码定位addr2line -e <binary> <offset>地址→源码行号
代码定位gdb + symbol-file交互式调试与符号加载
代码定位objcopy --only-keep-debug分离 debug 信息
代码定位ERR+CR2+RIP+GPR无 core dump 时的现场分析
预防-fsanitize=addressCI 阶段内存错误拦截
预防sdt_efence_pub.h交付前越界检测

评论