
咱们搞 Linux 的人迟早得跟“进程”这个概念正面交锋。不管是排查服务器负载过高还是写个脚本管理后台任务又或者是面试的时候被问到“程序和进程什么区别”归根结底都是在跟进程打交道。不少人刚接触 Linux 的时候命令敲得飞起但一旦问到进程底层是怎么回事、状态怎么切换、为什么会有僵尸进程就开始犯迷糊了。这篇东西就是把这些概念掰开揉碎了讲清楚结合我实际运维和开发中踩过的坑配合常用命令和排查思路希望看完你能对进程有个立体的认识。这篇文章更适合这几类人看刚入门 Linux 想系统打基础的同学、准备 Linux 运维或后端开发面试的求职者以及平时写脚本但总感觉对系统底层把握不准的开发者。文中不会堆砌大段源码而是用类比加实操的方式把进程的来龙去脉讲透。1. 进程到底是什么从程序和进程的区别说起很多教程一上来就甩出“进程是正在运行的程序”这种定义字面上没毛病但对理解背后的机制帮助不大。咱们换个角度从操作系统管理资源的角度去看这个问题。1.1 程序是静态的进程是动态的程序是什么就是你磁盘上那个可执行文件比如/usr/bin/nginx它躺在硬盘里有固定的文件大小占用的磁盘空间是确定的不会自己跑起来也不会消耗 CPU 内存。你可以把它理解成一本菜谱放在书架上不管放多久内容都不会变。进程是什么是把这个菜谱拿进厨房按照步骤真正开始切菜、炒菜、装盘的过程。菜谱还是那本菜谱但“炒菜”这个活动占用着灶台CPU、占用着碗碟内存、有自己的操作进度程序计数器这套运行起来的活动就是进程。同一个菜谱你可以同时开好几个灶台炒出好几份同样的菜对应到 Linux 上就是同一个程序可以启动多个进程每个进程独立运行互不干扰。我第一次真正理解这个区别是排查一个 Nginx 启动后出现多个 worker 进程的情况。明明我执行了一次启动命令ps -ef里却看到好几个 nginx 进程当时还以为是重复启动了。后来才明白master 进程 fork 出来的 worker 进程走的还是同一份可执行文件但它们是完全不同的进程实例各自处理各自的连接请求。1.2 进程在系统里的“身份证”PCB 与 PID操作系统要管理这么多进程光靠进程本身是不行的得给每个进程建一个档案。这个档案在内核里是一个叫task_struct的结构体不同版本内核可能有差异但功能类似我们一般把它叫做进程控制块也就是 PCB。PCB 里记着的东西非常多但我习惯把它想成三块核心信息。第一块是身份信息包括进程 IDPID、父进程 IDPPID、用户 IDUID这些就像身份证号、户口本上的父母信息。第二块是状态信息比如进程当前是运行中、睡眠中还是停止状态以及寄存器里的值、程序计数器指向哪里这些是进程被切换出去以后下次切换回来要接着用的现场数据。第三块是资源信息比如打开的文件描述符列表、内存映射情况、CPU 占用统计等。当你在 shell 里敲ps命令的时候看到的每一行本质上就是系统从这一堆 PCB 里提取出来的关键字段展示给你看。PID就是进程的身份证号系统里每个进程都有唯一的 PID。PPID则是你爸爸的 PID比如你在 bash 里启动了一个sleep 100命令那么 sleep 进程的 PPID 就是 bash 进程的 PID。这里有一个我在面试中经常问候选人的问题PID 是唯一的但 PID 会不会被复用会的。当某个进程退出后它的 PID 可能会被系统分配给新创建的进程。所以如果你写脚本要长时间记录某个进程的信息光存 PID 是不靠谱的最好连同进程启动时间一起记录下来防止 PID 被复用导致误判。1.3 第一个进程和进程树Linux 系统启动后内核会创建第一个进程它的 PID 永远是 1叫做systemd在较新的 CentOS、Ubuntu 系统上都是它老一点的 SysV 时代是init。这个进程是整个用户态进程的祖先所有其他用户进程要么是它直接拉起来的要么是它的子孙后代。可以用pstree命令很直观地看到这棵进程树。我在排查诡异问题的时候经常先用pstree -p看一整个进程体系长什么样能帮助快速定位某个进程是谁拉起来的。比如你发现服务器上有个奇怪的进程占着高 CPU通过 pstree 找到它的父进程顺藤摸瓜就能找到启动它的源头这个思路在处理挖矿木马和异常进程时非常有效。2. 进程状态与状态切换running、sleep、zombie 实战解读有了进程进程在生命周期里会经历各种状态。top命令里S列那一堆字母ps命令里STAT列的字符都是进程状态的缩写。这块必须得弄清楚因为排查系统卡顿、进程异常时状态就是你诊断的第一手证据。2.1 运行态R和睡眠态S/DRRunning 或 Runnable不代表进程此刻真的在 CPU 上跑而是说它处于“只要 CPU 有空位就能立刻上去跑”的就绪状态。你看到一堆 R 状态的进程说明系统里有大量任务在争抢 CPU这时候结合负载均值可以判断是不是 CPU 瓶颈。SSleeping是可中断睡眠这是进程最常见的状态。比如你在终端里执行sleep 300这个 sleep 进程就是 S 状态它在等待时间到或者等待某个事件发生。这个状态可以被信号打断比如你用kill命令发一个 TERM 信号进程就会收到并处理。DUninterruptible Sleep是不可中断睡眠这个状态新手见了容易慌。D 状态通常是进程在等待 I/O 完成比如磁盘读写、网络响应而这个过程不允许被信号打断。如果系统里 D 状态进程特别多基本可以判断是 I/O 有问题可能磁盘坏了、NFS 挂了、或者存储系统响应极慢。D 状态的进程用kill -9都杀不掉因为它压根没收信号这时候只能等 I/O 恢复或者重启系统。我早年有一台机器挂载了一个不稳定的 NFS 共享一断连就出现一堆 D 状态进程机器负载飙到几十最后排查定位到是网络存储的问题。2.2 僵尸进程Z回收不了的“遗骸”僵尸进程是很多新手理解的难点。当一个进程结束运行后它并不会立刻从系统里消失。它需要向父进程报告“我退出了”并且把自己退出时的状态码交给父进程。如果父进程没有及时调用wait()系统调用来读取子进程的退出状态那么这个子进程就变成了僵尸进程进程描述符还留在内核里但已经停止了任何执行。用ps查看僵尸进程的状态是 ZZombie而且你发现 COMMAND 列经常是[python] defunct或[sleep] defunct这样带defunct标记的。僵尸进程不占 CPU也不占内存但它占着一个 PID如果大量堆积系统的 PID 数量会被耗尽后面想创建新进程都创建不了。处理僵尸进程的正解是先处理它的父进程。如果父进程还活着可以尝试给父进程发送 SIGCHLD 信号或者直接重启父进程父进程退出后僵尸进程会被 PID 1 的 systemd 收养并清理。如果父进程本身就是 PID 1那就比较麻烦了通常只能重启系统。我在排查 CI 构建机的时候遇到过一次Python 脚本 fork 出一堆子进程但父进程逻辑写得不好没有好好回收子进程结果跑了几天以后系统无法创建新进程最后定位到代码里没有正确调用 waitpid属于典型的程序 bug。2.3 停止态T和进程的暂停与后台运行T 状态是进程被暂停了。你可以在终端里按CtrlZ暂停一个前台进程此时进程就是 T 状态。kill -STOP信号也能达到同样效果。想要让暂停的进程继续运行用kill -CONT信号。这个机制配合 shell 的作业控制非常实用。比如你跑一个大任务想临时腾出终端干别的可以CtrlZ挂起然后bg让它到后台继续跑。也可以用jobs查看当前终端的作业列表。这里有个常见误区CtrlZ暂停的进程和启动的后台进程不是一回事。启动的进程是直接放在后台运行状态可以是 R 或 SCtrlZ是让进程先暂停你手动bg之后才会真正在后台跑起来。3. 进程的一生fork、exec、exit 和 wait进程不是凭空冒出来的除了 PID 1 是内核创建的其他进程都是通过一套固定的机制诞生的。理解这套机制对理解 Linux 的进程模型至关重要。3.1 fork()复制一份自己在 Linux 里一个进程创建另一个进程最核心的调用是fork()。fork()做的事情简单说就是“复制当前进程”内核会创建一个新的 PCB新进程几乎拥有和父进程一模一样的内存内容、文件描述符、环境变量等唯一区别是 PID 不同且fork()的返回值不同。我特别喜欢用“细胞分裂”来类比 fork。一个细胞父进程分裂成两个细胞两个细胞继承了相同的细胞质和细胞器内存内容和文件描述符但他们是两个独立的生命体。在 C 语言里fork()调用一次却返回两次在父进程里返回子进程的 PID在子进程里返回 0。所以程序员通常用返回值判断当前代码是在父进程还是子进程里执行据此走不同的分支。一个经典问题是fork 之后父进程和子进程是共享内存还是各自独立的内存答案是共享物理内存但标记为写时复制Copy-On-Write。也就是说在 fork 出来的那一瞬间父子进程指向同一块物理内存谁都不改数据那就相安无事共享着。一旦某一方要写入数据内核就另外分配一块物理内存把数据拷贝过去再修改。这种机制大大降低了 fork 的开销避免了无谓的数据复制。3.2 exec()换一套程序来跑fork 复制了父进程的躯壳但很多时候我们不想跑和父进程相同的代码而是想跑一个全新的程序。比如你在 shell 里敲lsshell 先 fork 出一个子进程这个子进程立刻调用exec系列函数把自己当前运行的程序替换成/bin/ls这个可执行文件。exec 会加载新的程序到当前进程的内存空间替换掉原来的代码段、数据段、堆栈但 PID 不变进程还是那个进程但干的活完全变了。所以创建新进程的标准组合拳是fork() exec()。先 fork 复制一个和自己一样的进程然后在子进程里 exec 加载新程序。shell 执行命令、Nginx 启动 worker、大多数服务进程拉起子进程底层都是这套组合拳。3.3 exit() 与 wait()好死不如赖活着走了也要留个交代进程退出时会调用exit()系统调用。内核会释放进程占用的内存、关闭打开的文件描述符等资源但进程的 PCB 不会立刻删除里面还保留着退出状态码等着父进程来收。这个状态码就是echo $?能看到的值0 表示正常退出非 0 表示有异常。父进程必须调用wait()或waitpid()来读取子进程的退出状态。读取完成之后内核才会真正把子进程的 PCB 删除子进程才算彻底消失。如果父进程一直不调用 wait子进程就一直是僵尸状态。手工写 C 程序的人应该深有体会如果不小心忘了写 wait 调用跑完 fork 之后一查一堆僵尸进程。写 Shell 脚本的人可能不太关心这些因为 shell 作为父进程会自动回收子进程。但如果你自己写服务端程序必须深刻理解 wait 的作用否则生产环境很容易出现僵尸进程堆积。4. 进程优先级与调度为什么你的程序“卡”了Linux 是多任务操作系统CPU 资源要在众多进程之间分配怎么分配、谁先跑、谁后跑这就是调度器做的事。进程优先级是调度器做决策的关键依据。4.1 nice 值与优先级的关系Linux 里每个进程有两个关键优先级参数一个是nice值范围是 -20 到 19默认是 0。nice 值越小优先级越高越容易被调度器选中运行。另一个是实时优先级范围 0 到 99实时进程的优先级永远高于普通进程。nice这个名字挺有意思直译“友好”你 nice 值越高就是越“友好”地把 CPU 让给别人自己的优先级就越低。用top看进程时NI列就是 nice 值PR列是内核实际使用的优先级数值。普通进程的PR一般等于20 NI所以 nice 为 0 的进程 PR 是 20。如果你想启动一个对 CPU 占用不高、慢慢跑的任务可以用nice -n 10 ./slow_task让它别跟核心服务抢 CPU。反之如果某个任务特别重要想要它优先跑可以用sudo nice -n -5 ./important_task把它 nice 值调成负数。不过设置负 nice 值需要 root 权限。4.2 进程调度器的工作原理不深入代码也够用现代 Linux 默认的调度器是 CFS完全公平调度器。CFS 的思路不是给每个进程分配固定的时间片而是维护一个虚拟运行时间。每次调度时CFS 选择虚拟运行时间最小的进程来运行这样所有进程都能公平地推进虚拟运行时间。用生活经验来类比CFS 就像几个人排队打饭每个人按一定速率积累“等待时间”等待时间最长的优先打饭。nice 值影响的是“积累速度”nice 为 -5 的进程积累虚拟运行时间的速率更慢所以它总能保持较小的虚拟运行时间于是更频繁地被选中运行。实际观察top时会发现同一时刻 CPU 核数就那么多R 状态进程再多能真正同时运行的也就等于 CPU 核数。如果 R 状态进程数超过 CPU 核数很多系统负载就高了你会感觉到明显卡顿。这时候优先要看的不是进程数量而是每个进程的%CPU使用率和整体的load average。4.3 调整进程优先级renice 实战一个进程跑起来了发现它太占 CPU影响了线上服务不用杀掉重启可以用renice动态调整优先级。比如把 PID 为 12345 的进程 nice 值调整为 10renice 10 -p 12345执行后用top查看NI 列应该显示 10 了。要特别注意普通用户只能调高自己的进程的 nice 值变“友好”不能调低要调低必须用 root。生产环境对数据库、Web 服务这类核心进程我一般不建议调 nice 值保持默认就好调来调去反而容易把系统搞乱。5. 守护进程与作业控制nohup、setsid 和 systemd说完成生命历程和调度得讲讲进程怎么在后台长期运行。开发者和运维打交道最多的场景之一就是怎么把一个程序放到后台跑而且关了终端也不能死。5.1 为什么关了终端程序就死了很多人写过python app.py启动服务然后一关终端服务就没了。原因是这个程序成了终端的会话成员终端关闭时会向会话中的所有进程发送 SIGHUP 信号默认处理是终止进程。这跟进程本身写得怎么样无关是终端会话的机制决定的。解决思路有两个方向一是忽略 SIGHUP 信号二是让进程脱离会话成为一个新的会话首领。前者对应nohup命令后者对应setsid命令或者用 systemd 来托管。5.2 nohup 和 setsid 的区别nohup command 是最常用的后台运行方式。nohup 会让进程忽略 SIGHUP 信号所以终端关了它也不受影响。但要注意它仍然属于当前会话虽然 SIGHUP 被忽略了但关闭终端时进程的 stdin/stdout 可能已经指向了关闭的终端设备所以一般要配合重定向nohup python app.py app.log 21 21是把标准错误重定向到标准输出和日志一起进 app.log。这个是丢到后台的意思两兄弟经常配合使用。不重定向的话nohup 默认会把输出写到当前目录的nohup.out也算一个可用选项但我不喜欢让日志写到默认文件里太不显眼。setsid的思路更彻底它让新进程完全脱离当前会话成为一个新会话的领头进程。用setsid启动的进程不仅不怕 SIGHUP而且不再关联当前终端。不过现在 systemd 大行其道的时代真正正经跑服务的场景我优先推荐用 systemd service 来管理配好 Restart、StandardOutput 这些参数用systemctl start/stop/status管理比 nohup 规范得多。5.3 孤儿进程会被谁收养如果一个进程的父进程退出了这个进程就成了孤儿进程。孤儿进程不会被晾着内核会把它们收养给 PID 为 1 的 systemd。所以孤儿进程的 PPID 最终会变成 1。这里要注意僵尸进程和孤儿进程的区别孤儿进程是父进程死了但自己还活着它还在正常运行僵尸进程是自己死了但父进程没来收尸它的 PCB 残留。孤儿进程如果一直活着不会有问题反而是僵尸进程会占着资源。不过要是孤儿进程本身不正常跑到最后变成僵尸它的新父进程是 systemdsystemd 通常处理得很好不会让僵尸堆积。6. 排查进程问题的常用命令与实战技巧概念讲了这么多最终还是要落在排查问题上。这里整理我工作中最高频用到的一些查看进程的命令和排查思路不打算把所有参数都列一遍只挑真实场景最有用的。6.1 ps、top、htop 的姿势ps -ef是最常用的进程快照查询方式每一行显示进程的 UID、PID、PPID、CPU 占用、启动时间、执行命令。ps aux也是类似功能但输出格式略有不同。当你想找某个特定进程时配合 grep 用ps -ef | grep nginx但 grep 会匹配到 grep 本身那条所以常用grep -v grep或者直接pgrep -af nginx。pgrep的好处是不会匹配自己-a显示完整命令行-f是匹配完整命令参数。top适合实时监控进程资源占用。但说实话我更多用htop因为交互体验更好可以 F5 看进程树F6 排序鼠标操作也挺流畅。在排查 CPU 爆高时top里按P键按 CPU 排序按M键按内存排序这个操作非常高频。6.2 查看进程打开的文件lsoflsof是个神器它列出进程打开的所有文件。前面提到 PCB 里有文件描述符表lsof就是把这个表展示出来。排查很多问题都会用到它。比如你想看某个进程打开了哪些文件lsof -p 12345或者你想知道某个文件被哪个进程占用lsof /var/log/nginx/access.log或者某个端口被哪个进程监听lsof -i :8080端口被占用的报错绝对是新手遇到频率最高的问题之一。lsof -i :端口号一秒定位省得去翻一堆日志。6.3 查看进程资源占用pidstat 和 /proc如果你要连续观察某个进程的 CPU 和内存变化pidstat比 top 更适合脚本化采集pidstat -p 12345 1 5这条命令每秒采样一次共采样 5 次输出 PID 12345 的 CPU、内存、线程状态等数据。/proc目录是一个透明的内核视图目录树每个运行中的进程都有对应的/proc/PID目录。比如/proc/12345/status里能看到进程的状态、内存、线程数/proc/12345/cmdline里是完整的命令行参数原本以 null 分隔需要处理一下。排查性能问题时/proc/PID/io能看进程的 I/O 读写统计/proc/PID/limits能看进程的资源限制。6.4 进程起不来的常见排查路径遇到“进程起不来”的问题我有一套固定的排查顺序。第一步dmesg | tail看系统日志很多时候是 OOM内存不足杀进程或者段错误dmesg 都会有记录。第二步看应用自己的日志比如 Nginx 的 error.logJava 应用的 catalina.out。第三步用ulimit -a看当前 shell 的资源限制特别是 open files 数量很多高并发服务启动时报 “too many open files” 就是这个限制太低了。第四步看磁盘空间df -h因为进程启动时如果没法写日志也可能静默失败。顺便提一句线上服务器排查进程问题时我强烈建议先用uptime看 load average再top看整体负载分布然后再深入具体进程。很多人一上来就盯着某个进程的 CPU 猛查结果发现是系统整体负载过高导致的连锁反应白白浪费时间。7. 高频面试题里的进程概念因为 Linux 进程这块是面试常客我把这些年面试别人或者被别人问过的高频问题整理一下当作知识检验清单。7.1 进程和线程的区别是什么这是最经典的问题。进程是资源分配的基本单位线程是 CPU 调度的基本单位。同一个进程下的多个线程共享该进程的地址空间、打开的文件、全局变量等而不同进程之间拥有独立的地址空间互不干扰。所以线程之间通信简单直接共享内存但同步复杂进程之间隔离性强但通信要走 IPC 机制。用生活化比喻进程像一家公司线程像公司里的员工。每个公司有独立的办公场地和资产进程有独立的地址空间员工在同一个办公场地里共享打印机、会议室线程共享进程资源但员工之间抢会议室就需要协调线程同步。7.2 fork 之后子进程会复制父进程的哪些内容子进程会复制父进程的地址空间代码、数据、堆栈、文件描述符表、环境变量、信号处理方式等。注意区分哪些是复制哪些是共享内存是写时复制文件描述符是共享同一个文件偏移量也就是说父子进程写同一个文件时偏移量是共享的可能导致交错写入。这也是一种常见的 fork 之后踩坑的地方尤其是在日志文件写入场景。7.3 僵尸进程和孤儿进程的区别如何处理一句话区别僵尸死了没人收尸孤儿活着没人管教。处理僵尸进程的关键是处理父进程父进程退出了僵尸进程就会被收养清理。写代码时要记住父进程要调用 wait/waitpid或者给 SIGCHLD 信号注册处理函数避免子进程退出后成为僵尸。7.4 nohup 和 有什么区别只是把进程放到后台运行进程仍然是当前终端的子进程终端关闭后仍可能收到 SIGHUP 被杀掉。nohup是让进程忽略 SIGHUP 信号但进程仍然属于当前会话如果想要完全脱离建议同时使用nohup command log 21 或者用setsid。实际生产环境中用 systemd 更规范。7.5 如何知道一个进程是否发生内存泄漏这个不是简单一条命令能回答的。通常的手段包括长期观察进程内存占用趋势用top -p PID定期采样 RSS 大小或者用/usr/bin/time -v查看内存峰值也可以通过/proc/PID/status里的VmRSS字段做监控报警。当然最靠谱的还是用专门的工具比如 valgrind、gdb 查看堆内存分配记录或者用 Python 的话 tracemalloc 模块。但在静态看内存是否涨这个问题上ps和/proc足够作为第一道筛查手段了。8. 从进程看 Linux 设计哲学一切皆文件进程是核心聊了这么多我最后想跟你分享一点对 Linux 设计的理解。Linux 最核心的设计哲学是“一切皆文件”但真正让系统活起来的是进程。文件是静态的设备是静态的网络连接是静态的描述真正去读写文件、收发数据、执行计算的是进程。可以说进程把整个系统的资源串联了起来。理解进程不光是懂几个命令、几个状态的问题而是理解了操作系统怎么协调资源、怎么隔离任务、怎么保证一个程序挂了不影响整个系统。很多人觉得这些概念离日常开发很远其实不然。写多线程程序要理解进程和线程的关系排查线上 CPU 飙高要看进程状态设计后台任务要考虑进程的生命周期和退出机制部署服务要明白守护进程是怎么实现的。进程概念是 Linux 知识体系里承上启下的一环向上连接着用户命令和 shell向下连接着系统调用和内核调度横向延伸出线程、IPC、信号、资源限制等一大片内容。这一环打通了后面学网络编程、学容器、学性能调优都会顺畅很多。最后分享一个我自己的小习惯每学到一个新的系统概念我就强迫自己用“如果我要用文字给别人讲明白该怎么讲”来复盘一遍。写这篇东西也算是我自己重新梳理了一遍进程知识体系的过程。如果看完你也能用自己的话把 fork、僵尸进程、优先级这些讲给旁边的人听那说明你是真的懂了。