干这行久了都会遇到同一个场景:新产品上线后跑了半天,线上进程突然状态不对,要么 CPU 占用飘到几百,要么程序直接崩掉连日志都来不及打。这时候我一般不会急着去翻业务代码,而是先看进程状态,再确认手里的二进制有没有带调试信息,最后用 GDB 一层层扒开问题。Linux 进程管理、GCC 编译、GDB 调试,这三样东西在很多人看来是独立技能,但从我实际解决问题的经验看,它们就是一条完整的排查链路。
这篇文章我会把这三块放到一起讲,从最常用的 ps、top、信号操作开始,再讲 GCC 编译时如何把调试信息做进二进制,最后通过 GDB 把崩溃问题定位到代码行。同时会穿插我实际踩过的坑,比如 gcc 升级后版本不生效、gdb 怎么判断某个地址的内存是否已经被释放、VSCode 配 GCC 工具链到底配什么。对正在做 Linux 开发、嵌入式项目或者运维排查的同学,这套东西读完后基本可以直接套用。
1. Linux 进程管理:动手之前先把“状态”看懂
1.1 查看进程的常用命令:ps、top、htop 怎么选
排查任何程序问题,第一步都是回答“这个进程现在到底在干嘛”。Linux 下最常用的两个进程查看命令是 ps 和 top,但很多人其实没搞清楚它们的分工。
ps 是快照式命令,适合做瞬态查看和脚本处理。排查崩溃、确认 PID 是否存在、查看进程父子关系时,我基本都用 ps aux 或者 ps -ef。这两个写法看起来很像,实际输出字段有差异:ps aux 是 BSD 风格,会显示 CPU 占用、内存占用、VSZ、RSS,适合快速看资源消耗;ps -ef 是 System V 风格,会显示 UID、PID、PPID,适合梳理进程父子关系。日常我一般直接 ps aux,要看父进程时再补 ps -ef。
top 是动态刷新模式,适合持续观察。比如进程在跑,但想确认它是不是真的在消耗 CPU,或者想盯着内存是不是持续上涨,top 比 ps 直观得多。按 P 按 CPU 排序,按 M 按内存排序,按 t 看 CPU 总量的条形统计,这几个快捷键我几乎每次都用。
htop 属于增强版 top,交互体验更好,可以鼠标点击、树状显示进程,还能直接在界面里发信号给进程。看起来方便,但很多服务器默认没装,还得先 apt install htop 或者 yum install htop,所以我反而习惯了直接 top,毕竟哪个机器都有。
pidstat 是 sysstat 包里的命令,适合做单进程维度的定时采样,例如打算观察进程 1234 的 CPU 和内存变化:
bash复制pidstat -u -r -p 1234 1 10
这条命令每秒采样一次,连续 10 次,能清晰看到该进程的 CPU 使用率、内存变化趋势,比 top 的持续盯屏更适合记录和对比。
1.2 进程状态 STAT 里藏着的关键信息
进程状态字段是排查问题的重灾区。一个进程卡住了,用 ps aux 看 STAT 列,就能迅速判断是“没在干活”还是“卡在内核里出不来了”。
Linux 进程状态常见的有这几种:
| 状态码 | 含义 | 常见场景 | 是否能被杀掉 |
|---|---|---|---|
| R | Running / Runnable | 正在运行或处于就绪队列 | 能 |
| S | Sleeping(可中断) | 等待 IO、等待锁、正常休眠 | 能 |
| D | Uninterruptible Sleep | 不可中断睡眠,通常卡在内核 IO | 很难杀掉,甚至 kill -9 也没用 |
| Z | Zombie | 子进程已退出但父进程没回收 | 杀不掉,只能让父进程处理或退出 |
| T | Stopped / Traced | 被 Ctrl+Z 挂起,或被 gdb 中断 | 能,kill -SIGCONT 恢复 |
| X | Dead | 完全退出,极少见 | 无需处理 |
我印象最深的是 D 状态进程。曾经有个服务读某个 NFS 挂载目录,网络存储出现故障后,进程一直处于 D 状态,kill -9 都杀不掉,因为它在内核态等待 IO,信号根本来不及处理。最后只能重启机器或者等超时才恢复。所以看到 D 状态,不要浪费时间发信号,先排查底层 IO 是否异常。
Z 状态也是面试和日常经常碰到的。僵尸进程的产生原理很简单:子进程 exit 之后,会向父进程发送 SIGCHLD,如果父进程没有调用 wait/waitpid 去回收,这个子进程就会残留为僵尸。它不占 CPU,也不占内存,但会占一个 PID。你没法直接 kill 僵尸,因为进程已经死了,唯一的办法是让它的父进程退出,交给 init 进程回收,或者让父进程程序里正确写 wait 逻辑。
顺带说一下,ps 里看线程要用 -L 参数。进程和线程的关系,可以用一句话概括:进程是资源分配单位,线程是调度单位。一个进程崩溃不可怕,但多线程里一个线程越界写内存,可能把别的线程的数据一起写坏,这时候查错尤其费劲。排查线程状态时我常用:
bash复制ps -eLf | grep 进程名
或者:
bash复制top -H -p PID
后者会展示指定进程内的所有线程 CPU 使用情况,哪个线程把 CPU 吃光了,一眼就能看到。
1.3 进程生命周期、信号与前后台调度
进程从创建到结束,绕不开 fork 和 exec 两个系统调用。开发时不用手写这些,但理解生命周期对排查程序是否被正常拉起、退出码代表的含义有帮助。
Linux 的信号机制必须要熟,因为运维和调试过程里,信号是我们和进程交互最主要的方式。
| 信号 | 编号 | 默认行为 | 我常用的场景 |
|---|---|---|---|
| SIGHUP | 1 | 终止进程 | 终止挂断的会话进程,很多守护进程重载配置用它 |
| SIGINT | 2 | 终止进程 | Ctrl+C 触发,相当于前台进程被中断 |
| SIGKILL | 9 | 无法捕获、无法忽略 | 最后手段,强制杀掉进程 |
| SIGTERM | 15 | 终止进程 | kill 默认发这个,程序可以捕获做清理 |
| SIGSTOP | 19 | 停止进程 | 和 Ctrl+Z 类似,进程进入 T 状态 |
| SIGCONT | 18 | 让停止的进程继续 | 恢复被 SIGSTOP 的进程 |
发信号时,kill 后面可以接 PID,pkill 可以接进程名,killall 也可以按名字批量发。但要记住,kill -9 是最后手段,因为它不留给进程任何清理机会,锁文件、临时文件、共享内存都可能处于脏状态。遇到需要优雅重启的程序,优先 kill -SIGTERM PID,让它自己处理完收尾工作。
前台和后台切换也是日常高频操作。在终端里启动一个程序,按 Ctrl+Z 会把它挂起,进程进入 T 状态。接着用:
bash复制jobs
bg %1
fg %1
可以把作业放到后台继续执行,或者拉回前台。注意,bg 后的进程虽然放到后台,但它的 stdout 还是连着当前终端,如果终端断开,进程可能收到 SIGHUP 被终止。所以真正要长期跑,应该用:
