
你在服务器上敲下一条命令回车屏幕上输出结果之后又回到提示符。很多人觉得这只是执行了一下命令但实际上这背后是操作系统安排了一个进程从诞生、运行、退出直至被回收的完整生命周期。LINUX系统里关于进程的内容特别多今天先写开篇这篇把最基础、也最容易被忽视的概念和实操梳理清楚进程和程序到底差在哪进程怎么组织成树状态栏里那些字母是什么意思以及日常排查最常用的几套命令怎么配合着用。这篇适合两类人看一类是刚接触Linux、看到ps输出一头雾水的新手另一类是平时会用top但从来没认真理解过 STAT 列的运维和开发。读完你可以自己去解释为什么这个进程是 Z 状态为什么系统卡住但是 CPU 占用不高也能随手找出某个端口对应的进程。我会尽量把原理和命令串起来讲因为光记住命令不看原理换个场景你照样不会用。1. 先解决最基础的疑惑进程是什么程序又是什么1.1 程序是一本书进程是正在读这本书的过程我见过不少同学把进程和程序混为一谈最典型的话是我把这个程序杀了。严格来说你杀掉的不是程序而是这个程序运行时产生的进程。程序是静态的东西它躺在磁盘上是一堆指令和数据的集合你不运行它它就在那儿躺着什么也不干。进程是动态的东西是操作系统把程序加载到内存里、分配了资源、调度到 CPU 上执行之后形成的运行现场。打个比方程序是菜谱进程是按照菜谱做菜的过程。菜谱可以同时被十个人读对应同一个程序可以被多个用户同时运行每口锅、每个灶台的火力、每个厨师的颠勺动作都不一样这就是不同的进程各自有独立的状态。这个区别在做运维排查时特别关键。比如你用ps看到一个服务进程占用了 30% 的 CPU那绝对不是程序文件在占用而是它运行起来之后某个线程在忙。你要做的是定位到这个进程、看它的内部状态而不是去磁盘上删程序文件——删了文件进程还在跑。1.2 一个进程里到底装着哪些东西一个进程不只是一段正在执行的代码它是一个完整的运行环境。内核为了管理它会在内存里维护一大堆数据结构。抛开内核内部不谈从使用者的视角看一个进程至少包含这么几块地址空间包括程序的代码段、数据段、堆和栈。堆是动态分配内存的地方栈是函数调用时压栈出栈的地方。文件描述符表进程打开了哪些文件、哪些网络连接、哪些管道都记录在一张表里。lsof -p pid可以看到这张表的全貌。执行上下文寄存器里的值、程序计数器、CPU 状态。这是进程被切换出去之后还能接着上次的地方继续跑的关键。进程标识PID、PPID、用户 ID、组 ID 等一堆元信息。信号处理设置对哪些信号做了自定义处理、哪些被屏蔽、哪些是默认行为。这些东西合在一起才构成一个独立的进程。所以你启动一个服务看到的进程实际上是一个容器把代码、资源、状态全都装在里面。内核的调度单位是线程但资源的分配单位是进程这句话值得记住。1.3 为什么 Linux 要用进程来管理一切早期的批处理系统根本没有进程的概念一次只能跑一道作业跑完才能跑下一道。后来人们发现这样太浪费因为程序等 I/O比如读磁盘的时候 CPU 是空闲的可以趁机跑别的程序。多道程序系统出现了就必须要有一个东西来记录每个程序跑到哪了、占用哪些资源这就是进程的雏形。到现代 Linux 系统进程已经成了资源的天然边界每个进程有独立的地址空间一个进程越界访问内存不会拖垮另一个每个进程有独立的文件描述符表一个进程关掉文件不会影响别的进程。这也是一个程序崩溃了服务器还能活着的根本原因。理解这一层之后你再看kill -9就没有那么可怕了它只是让一个运行现场被强制销毁程序文件还在磁盘上下次照样能启动。这也是为什么实战中常用重启进程而不是重装程序来解决问题——问题是出在运行状态上不是出在静态文件上。2. 进程不是孤岛PID、父子进程和那棵看不见的进程树2.1 每个进程都有身份证PID 和 PPID系统中每个进程都有一个唯一编号叫 PIDProcess ID。进程启动时由内核分配这个编号可以复用——某个进程退出之后它的 PID 可能被后续新进程占用。如果你在排查时发现上次看到这个 PID 的进程不对了不用太惊讶多半是已经被回收再分配了。除了 PID还有个 PPIDParent Process ID表示我是谁生出来的。几乎每个进程都有父进程也就是创建它的那个进程。你可以在终端敲一个命令然后用ps -o pid,ppid,comm看看自己和父进程的关系$ ps -o pid,ppid,comm PID PPID COMM 1234 1024 bash 5678 1234 ps这里ps的父进程是bashbash的父进程又可能是你登录时创建会话的那个进程。这种谁生谁的关系一层层往上追溯最后会聚到一个点。2.2 一切进程的源头0 号进程和 1 号进程Linux 启动过程中最先有两个特殊的进程0 号进程负责内核态的调度和初始化在早期的系统里它进一步创建出 1 号进程。1 号进程是所有用户态进程的祖先负责初始化整个用户空间、启动各种服务并且在系统里扮演最后收容所的角色——孤儿进程都会被它收养。你可以用pstree命令直观地看到这棵树。在大多数机器上输出长这样init--shell |-web |-db -croninit是树根下面挂着各种服务和子进程。看到某棵子树特别深、特别大说明这个服务的进程模型比较复杂比如它会 fork 一堆子进程来处理并发请求。排查问题时有这张树状关系图能帮你快速理解这个进程是谁拉起、退出之后会不会连坐。2.3 父进程退出之后孩子归谁管这是一个很容易踩坑的知识点。一个进程退出后它的父进程还在那父进程要负责调用wait()来回收子进程的退出状态但如果父进程先退出呢子进程变成了孤儿进程orphan process内核会把它过继给 1 号进程由 1 号进程负责回收。我在实际排查中遇到过一台机器上残留了大量 PID 为 1 的进程第一反应是有别的程序把这些进程收养了结果一看其实是启动脚本的方式不对服务进程的父进程没有做成守护进程导致每次会话退出进程就被过继给 1 号进程。这本身不算故障但如果你想对进程做精细管理孤儿化之后很多东西就不好控制了。所以很多守护进程在启动时会故意做一次 fork让自己的父进程退出把自己主动孤儿化这也是常见的后台进程手法之一。3. 进程的一生fork()、exec() 与僵而不死的真相3.1 fork()进程分裂的唯一入口Linux 里创建进程的系统调用是fork()。它干了件非常反直觉的事情调用一次返回两次。在父进程里返回的是子进程的 PID在子进程里返回的是 0出错则返回负值。我早期学的时候怎么都想不通一个函数怎么能返回两次后来明白了因为fork()复制出了一个新的运行现场两个现场各自从返回点继续执行。父进程走的是父进程的代码路径子进程走的是子进程的代码路径只不过它们俩在fork()之前共享了几乎相同的地址空间。写一段最简单的实验代码#include stdio.h #include unistd.h int main(void) { pid_t pid fork(); if (pid 0) { perror(fork); return 1; } if (pid 0) { printf(子进程运行中: PID%d, 父进程 PID%d\n, getpid(), getppid()); } else { printf(父进程运行中: PID%d, 子进程 PID%d\n, getpid(), pid); } return 0; }编译运行后你会看到两行输出顺序不一定因为父子进程谁先抢到 CPU 由调度器决定父进程运行中: PID1000, 子进程 PID1001 子进程运行中: PID1001, 父进程 PID1000这个实验强烈建议亲手做一次做完你对进程是系统资源分配单位这句话的感受会完全不一样。3.2 写时拷贝fork 没那么傻如果fork()每次都要把整个进程地址空间完整复制一份那启动一个几千兆内存的进程再去fork子进程代价就太大了。现代 Linux 的fork()用了写时拷贝Copy-on-Write, COW技术一开始父子进程共享同一块物理内存只有某一方要对内存做修改时才真正复制那部分页面。这个优化是 Linux 能轻松创建大量子进程的基础。很多网络服务就是先fork()一堆子进程再各自处理连接如果没有 COW内存会被迅速打满。3.3 exec()换一具身体继续活fork()是复制一个和自己几乎一样的进程但实际业务里你想要的往往是用子进程去跑一个完全不同的程序。这时fork()之后通常紧跟exec()调用。exec家族函数会当前进程地址空间替换成新程序的可执行文件内容重新初始化代码段、数据段、堆和栈然后从新程序的入口开始执行。注意exec成功之后进程的 PID 不变但进程所执行的程序已经换掉了。这就是 shell 运行外部命令的原理shell 先fork()出一个子进程在子进程里调用exec()加载你要执行的命令比如ls、grep然后子进程就变成了这个命令。所以你在ps里看到进程名通常是命令名而不是shell 壳。3.4 退出之后的僵尸期僵而不死是怎么回事进程正常结束时调用exit()这时进程的资源大部分会被内核释放但内核会保留一个最小化的记录——退出码、消耗的 CPU 时间等等着父进程来收尸。如果父进程一直没有调用wait()/waitpid()这个进程就变成僵尸进程zombie process在ps里状态显示为 Z。僵尸进程不占用 CPU、不占用内存但占着一个 PID 名额。如果父进程是个长期运行的服务又一直不回收子进程子进程又频繁创建和退出PID 会被僵尸占满导致无法创建新进程。这是生产环境里比较常见的问题。怎么避免父进程要调用wait()或者父进程对接 SIGCHLD 信号做异步回收更省事的做法是让子进程死掉之后立刻有中间层去领养。排查时看到 Z 状态不要急着 kill——你杀不掉一个已经死掉的进程。正确的做法是处理它的父进程让父进程回收它或者把父进程重启。我见过一个有意思的案例某个计划任务脚本频繁 fork 然后忘记回收跑了一晚上系统里积压了上千个 Z 状态进程管理员怎么 kill 都没用因为 Z 是已死亡待认领状态kill 对它无效。后来找到父进程重启一次性清干净了。4. 看懂进程状态栏R、S、D、Z、T 都意味着什么4.1 STAT 列其实是一套编码在ps输出里 STAT 列看着像几个字母其实每个字母有明确含义。我把最常见的整理成表格状态字母含义直观理解R运行中或等待运行在 CPU 就绪队列里排队S可中断睡眠正在等事件如等网络、等 I/O信号能唤醒D不可中断睡眠通常正在等磁盘 I/O信号打断不了Z僵尸状态已退出但等待父进程回收T已停止被暂停比如按了 CtrlZ或收到 SIGSTOPI空闲内核线程内核线程空闲状态新内核才常见后面还可能跟一些附加字符表示更多细节比如表示高优先级、N表示低优先级、表示位于前台进程组。但你先掌握这六个字母就够了实际看的时候优先关心 R、S、D、Z。4.2 为什么 D 状态比 Z 状态更让人头疼生产环境中 I/O 密集的机器上你会看到很多进程处于 D 状态。D 表示不可中断睡眠常见于进程发起了磁盘读写、网络 NFS 等待这类内核层面的 I/O 操作。这种状态下进程不响应普通信号连kill -9都拿它没办法因为它正趴在内核里等 I/O 返回根本没机会处理信号。遇到大量 D 状态第一反应应该是查存储磁盘是不是有坏道存储网关是不是卡了文件系统是不是出问题了我记得有一次排查数据库卡顿top里几乎全是 D 状态最后发现是底层磁盘控制器固件有问题I/O 请求全部卡住进程全部堵在内核 I/O 等待里。这种情况单纯 kill 进程没用必须恢复底层存储。S 状态则完全不同它是可中断睡眠进程在等某个事件但信号来了它可以醒来处理。大多数服务进程在闲置时都是 S 状态这是健康的表现不用看到 S 就紧张。4.3 区分线程和进程的观察方法进程状态和线程状态有一定关联。top默认显示的是进程视图但如果按H键会切换成线程视图能看到每个线程的 CPU 占用和状态。ps -eLf也能看到线程其中 LWP 是线程 ID。排查应用 CPU 飙高时第一步就是切到线程视图确认是哪个线程在忙。这里有个常见的误区top里看到某个进程 100% CPU就以为整个程序都卡了。实际上很多现代服务是多线程的往往只有一两个线程在饱和工作其他线程都在等锁或者闲着。你需要的是往下钻到线程维度结合性能分析工具去看具体是哪个代码路径在消耗 CPU。5. 实操手册ps、top 与 /proc 目录的组合用法5.1 ps 的两种经典姿势ps是最常用的进程查看命令。但很多新手被两种参数风格搞晕# BSD 风格一个横杠都没有 ps aux # UNIX 风格带横杠 ps -ef其实两个命令输出差不多区别主要是显示字段的细微差异。ps aux第一列是 USER第二列是 PID第三列是 CPU%第四列是 MEM%后面是 VSZ、RSS、TTY、STAT、START、TIME、COMMAND。ps -ef的字段顺序略不同有 UID、PID、PPID、C、STIME、TTY、TIME、CMD。实战中我一般用ps aux更多因为它默认带 CPU 和内存占用排查负载时很方便。但如果你想知道每个进程的父进程是谁用ps -ef更直接因为 PPID 默认就显示。组合常用的过滤方式# 按进程名查找 ps -ef | grep 进程名 # 按 PID 看指定进程详情 ps -p 1234 -o pid,ppid,stat,%cpu,%mem,cmd # 用 pgrep 更精确找 PID pgrep -f 完整命令行关键词5.2 top 的交互操作比你想象的强大top不只是一个看一眼就退出的命令。它启动后处于交互模式这时候按键盘可以切换很多视图1展开/收起每个 CPU 核心的使用率P按 CPU 占用从大到小排序这是最常用的M按内存占用排序T按累计运行时间排序c切换显示完整命令行还是只显示进程名u输入用户名只看这个用户的进程k输入 PID 和信号编号直接给进程发信号r调整进程优先级nice 值H切换线程视图我排查看 CPU 占用时固定套路是先按P排序找到可疑进程按c看完整命令行确认是什么东西按H看是不是某个线程打满再按1判断是不是单个核心打满、整体负载不高。top的短板在于刷新快、不好做历史记录。持续压测时我会用pidstat或top -b重定向到日志文件# 每 2 秒输出一次共输出 30 次存到文件 top -b -d 2 -n 30 top.log5.3 根据端口找进程根据进程找端口排查网络服务时最常遇到的问题这个端口被谁占了或者这个进程开了哪些端口。两个方向都有对应命令# 查端口对应的进程需要管理员权限 ss -ltnp | grep :8080 # 查进程打开的端口和文件 ls -l /proc/1234/fdss -ltnp会显示监听端口 进程 PID 进程名。权限不够时 PID 会显示不出来需要用管理员权限执行。反过来如果你知道 PID直接看/proc/pid/fd目录能列出这个进程打开的所有文件描述符包括 socket、管道、普通文件。这里能看到进程到底在跟谁通信——如果一堆连接套接字指向外部地址基本可以判断它在做网络请求。5.4 /proc内核给你开的实时档案室/proc是一个假文件系统它不占磁盘是内核对外暴露的实时信息接口。每个进程都有一个/proc/pid目录里面全是运行时信息。我最常用这几个文件/proc/pid/cmdline进程启动时的完整命令行可能包含参数/proc/pid/status进程状态、内存、线程数、上下文切换次数等/proc/pid/stat内核记录的进程状态统计ps的数据来源之一/proc/pid/fd/进程打开的文件描述符列表举个例子想确认某个进程是不是卡在磁盘 I/O 上看 status 里的 State 字段$ grep -E State|VmRSS|Threads /proc/1234/status State: S (sleeping) VmRSS: 158000 kB Threads: 12State 是D的话就要警惕。另外/proc/pid/status里的 voluntary_ctxt_switches 和 nonvoluntary_ctxt_switches 能侧面反映进程被调度的情况非自愿上下文切换非常多说明它在跟别的进程抢 CPU系统可能已经过载。5.5 排查进程问题时的一个完整思路最后分享一个我实际排查进程异常时常用的流程你可以当模板用先用top按 CPU 或内存排序看谁在制造问题。用ps -p pid -o pid,ppid,stat,etime,cmd看它的启动时间、状态和完整命令行。根据进程名用pgrep -f确认有没有多个同名进程以及它们的父进程关系。查看/proc/pid/status确认进程状态、内存占用、线程数。如果是服务端口问题用ss -ltnp双向验证。如果状态异常Z、D、T 等针对不同状态做不同处理Z 找父进程D 检查存储T 用kill -SIGCONT恢复。这套流程看着简单但覆盖了大多数日常场景。很多时候排查不下去不是命令不够而是没想清楚我要找的到底是什么。状态分析优先于命令执行这是我和新手合作时最常说的一句话。写这篇的时候我又把 fork 实验从头跑了一遍手动制造了一次僵尸进程再用 wait 回收整个过程依然觉得很有意思。进程是 Linux 系统里最核心但也最容易被一带而过的概念把它吃透了后面学信号、守护进程、进程间通信都会轻松很多。下一篇进程的内容我打算侧重讲守护进程的写法和容器场景下进程的特殊性那部分实战味更浓到时候我们再接着聊。