阶段五 · 进程与网络

进程管理

一句话总结

系统里每个正在运行的程序都是一个进程(Process),有唯一的 PID.
进程管理的核心就三件事:看谁在跑(ps,top),让它停(kill + 信号),控制它在前台还是后台跑(jobs,bg,fg,nohup).

进程是什么

敲下 python app.py 回车,操作系统就把这段程序装进内存,分配资源,开始执行--这个"正在运行的程序实例"就是一个进程.同一个程序可以同时跑出多个进程(开三个终端各跑一个 python,就是三个进程).

程序(program)是菜谱,进程(process)是按菜谱正在炒的那盘菜.
菜谱只有一份,但可以同时开三个灶台照着它炒三盘--三个进程,互不干扰,各自占用自己的锅和火.

每个进程都带着几个关键身份信息,后面所有命令都围绕它们打转:

属性 含义
PID 进程 ID,系统内唯一的身份证号,kill 就是靠它定位
PPID 父进程 ID(Parent PID)--谁启动了它
UID 属主用户,决定这个进程有什么权限
STAT 状态:运行中,睡眠,僵尸等
%CPU / %MEM 占用的 CPU 和内存比例,排查"谁把机器拖卡了"靠它

进程都有父亲

进程是树状结构:Shell(如 zsh)由终端启动,在 Shell 里跑的命令又以 Shell 为父进程.所有进程往上追溯,最终都挂到 PID 为 1init / systemd 进程上--它是开机后内核启动的第一个进程,是整棵进程树的根.

ps:进程的快照

ps(process status)给出某个瞬间的进程列表--像拍一张照片,不会自动刷新.它的选项历史包袱很重(BSD 风格不带 -,System V 风格带 -),但日常只需要记住两个组合.

ps aux

ps aux

列出系统上所有用户所有进程,带 CPU / 内存占用.排查"谁在吃资源"的首选.

ps -ef

ps -ef

同样列出所有进程,但额外清晰展示 PPID(父进程),适合理清进程的父子关系.

$ ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.1 168920 11200 ? Ss 09:00 0:02 /sbin/init
alice 842 12.3 4.5 985400 92000 pts/0 Sl 10:15 1:30 python app.py
alice 901 0.0 0.0 12100 3200 pts/0 R+ 10:42 0:00 ps aux
# ↑PID ↑控制终端 ↑状态 ↑命令

各列含义已在上表覆盖,这里重点看 STAT 列的状态字母:

状态 含义
R Running--正在运行或就绪等待 CPU
S Sleeping--可中断睡眠,等待某个事件(绝大多数进程的常态)
D 不可中断睡眠,通常卡在磁盘 I/O--连 kill 都打不动它
T Stopped--被信号暂停(如按了 Ctrl-Z)
Z Zombie--僵尸进程,已结束但父进程还没回收它的退出状态

后面可能跟附加符号:s 表示会话首进程,+ 表示在前台进程组,l 表示多线程.

ps 的真正威力来自和管道,grep 组合--这正是前面阶段反复练习的套路:

# 找出所有 python 进程(注意用 [p] 技巧避免匹配 grep 自己,见第 11 篇)
$ ps aux | grep "[p]ython"

# 按内存占用排序,看 Top 5 内存大户
$ ps aux | sort -k4 -rn | head -5
# ↑ 第 4 列 %MEM,-r 倒序 -n 按数值

# 只看自己的进程
$ ps -u $USER

僵尸进程不可怕,进程泄漏才可怕

僵尸(Z)进程本身不占 CPU 和内存,只占一个 PID 槽位,等父进程调用 wait() 回收即可消失.无法 kill 一个僵尸(它已经死了),真正要处理的是它的父进程--父进程退出后,僵尸会被 init/systemd 接管并清理.如果僵尸越积越多,说明父进程写得有 bug,没回收子进程.

top / htop:实时监控

如果说 ps 是照片,top 就是实时录像--它每隔几秒刷新一次,动态展示当前最耗资源的进程.排查"机器突然变卡了"时,第一反应就是敲 top.

$ top
top - 10:45:30 up 2:30, 2 users, load average: 1.20, 0.95, 0.80
Tasks: 215 total, 1 running, 214 sleeping
%Cpu(s): 15.3 us, 4.2 sy, 0.0 ni, 80.0 id # us=用户态 sy=内核态 id=空闲
MiB Mem : 7950.2 total, 1200.5 free, 4800.0 used

PID USER %CPU %MEM TIME+ COMMAND
842 alice 98.5 4.5 1:30.22 python
1203 alice 12.0 8.1 0:45.10 node

进入 top 后是交互界面,记住几个高频按键就够用:

按键 作用
P 按 CPU 占用排序(默认)
M 按内存占用排序
k 输入 PID 直接 kill 一个进程
q 退出

优先装 htop

htoptop 的增强版:彩色,可上下滚动,可用鼠标点选,支持树状视图(F5).brew install htopapt install htop 即可.一旦用过 htop,基本就回不去原版 top 了.

信号:进程间的"敲门方式"

要让一个进程停下来,不是直接"删掉"它,而是给它发一个信号(Signal)--相当于敲门通知它"该收摊了".信号是 Unix 进程间通信的一种轻量机制,内核负责投递.

SIGTERM 是礼貌敲门"忙完请关门走人",进程可以先保存好工作再退出.
SIGKILL 是直接把门拆了把人拖走--立即生效,但进程来不及做任何清理.

常用信号只有几个,重点记住它们的编号能否被捕获:

信号 编号 含义 可被忽略/捕获?
SIGTERM 15 礼貌终止,kill 默认发的就是它--给进程清理机会 可以
SIGKILL 9 强制杀死,立即生效,无法清理 不能
SIGINT 2 中断,按 Ctrl-C 发的就是它 可以
SIGHUP 1 挂断--终端关闭时发给子进程,常被守护进程用作"重载配置" 可以
SIGSTOP 19 暂停进程,按 Ctrl-Z 发的是 SIGTSTP(20,可捕获版) 不能
SIGCONT 18 恢复一个被暂停的进程继续运行 -

为什么 SIGKILL(9) 和 SIGSTOP(19) 不能被捕获

这两个信号被内核保留为"最终手段"--如果允许进程拦截它们,一个失控或恶意的进程就能让自己永远杀不死,停不下来.所以内核强制规定:9 必杀,19 必停,进程没有任何反抗余地.这也是为什么 kill -9 是"最后的核武器".

kill / pkill / killall:发送信号

kill 的名字有误导性--它不是"杀死",而是"发信号".只是默认发的 SIGTERM 通常会让进程退出,才得名 kill.

# 默认发 SIGTERM(15),礼貌地请进程退出
$ kill 842

# 进程不响应 SIGTERM?升级为 SIGKILL(9)强制杀死
$ kill -9 842
$ kill -KILL 842 # 等价写法,用信号名

# 暂停 / 恢复一个进程
$ kill -STOP 842 # 冻结它
$ kill -CONT 842 # 让它继续

# 列出所有信号编号与名称
$ kill -l

不要张口就 kill -9

正确顺序是kill(SIGTERM)再 kill -9(SIGKILL).SIGTERM 给进程机会保存文件,关闭连接,删除临时文件;kill -9 直接拔电源,可能导致数据损坏,锁文件残留,子进程变孤儿.只有在进程"卡死,SIGTERM 无效"时才动用 -9.

如果不想先查 PID,可以用按名字操作的两个变体:

pkill

pkill [-信号] 进程名模式

按名称模式匹配发送信号,支持部分匹配.pkill -9 python 杀掉所有名字含 python 的进程.

killall

killall [-信号] 精确进程名

精确名称杀掉所有同名进程.killall nginx 终止全部 nginx 进程.

# 配合 pgrep 先看会杀掉谁,再动手--养成这个习惯能少踩坑
$ pgrep -l python
842 python
903 python

$ pkill python # 确认无误后再发信号

作业控制:前台与后台

一个终端同一时刻只能有一个前台进程占据键盘输入.但常常希望"让这个耗时任务在后台跑,腾出终端干别的"--这就是作业控制(Job Control).

三个关键操作串成一条工作流:

前台运行中  ──Ctrl-Z──▶  暂停(Stopped)  ──bg──▶  后台运行
▲ │
└────────────────── fg ◀─────────────────────┘

启动时直接加 & ──▶ 一开始就在后台运行
操作 作用
命令 & 启动时直接丢到后台运行,终端立即可用
Ctrl-Z 把当前前台进程暂停并丢到后台(发 SIGTSTP)
jobs 列出当前 Shell 管理的后台作业及其编号
bg %1 让 1 号作业在后台继续运行(恢复被暂停的)
fg %1 把 1 号作业调回前台
# 启动一个耗时任务,发现忘了加 &,想转后台
$ ./long_task.sh
^Z # 按 Ctrl-Z 暂停
[1]+ Stopped ./long_task.sh

$ bg %1 # 让它在后台继续跑
[1]+ ./long_task.sh &

$ jobs # 看看现在有哪些作业
[1]+ Running ./long_task.sh &

$ fg %1 # 需要交互时再调回前台
./long_task.sh

作业号(%1) vs 进程号(PID)

jobs 里的 [1]当前 Shell 的作业号,只在这个终端有效,用 %1 引用;PID 是全系统唯一的.fg %1 用作业号,kill 842 用 PID--别混淆.% 还能按名字引用,如 fg %python.

nohup 与 disown:让进程活过终端关闭

后台进程有个致命问题:关闭终端时,Shell 会向它的所有子进程发送 SIGHUP,把它们一起带走.所以单纯 & 放后台还不够--SSH 断线,任务就没了.

解决办法有两类:

# 方案一:nohup--启动时就忽略 HUP 信号,输出重定向到文件
$ nohup ./long_task.sh > task.log 2>&1 &
$ exit # 终端关了,任务照跑

# 方案二:disown--已经在后台跑的作业,事后"脱离" Shell 管理
$ ./long_task.sh &
$ disown %1 # 从作业表移除,不再受 SIGHUP 影响

长任务的现代方案:tmux

nohup / disown 能让进程活下来,但看不到它的实时输出,也无法再交互.更好的方案是用 tmux(第 20 篇)--把任务跑在一个 tmux 会话里,断开 SSH 后会话依然存活,重连后 tmux attach 就能回到现场,输出,交互一个不少.生产环境跑长任务,优先 tmux.

nice 与 renice:调整优先级

当 CPU 紧张时,可以通过 nice 值告诉内核"这个进程不急,先让别人用 CPU".nice 值范围 -20(最高优先级,最不"礼貌")到 19(最低优先级,最"谦让"),默认 0.普通用户只能调高(降低优先级),调低需要 root.

# 启动时就用低优先级跑一个备份任务,不抢占在线服务的 CPU
$ nice -n 19 ./backup.sh

# 对已运行的进程改优先级
$ renice -n 10 -p 842

nice 值就是进程的"谦让程度"--值越大越谦让(nice,与日常英文同义),主动给别人让 CPU;值越小越霸道,优先抢占 CPU.名字本身就是记忆点.

快速回顾

  • 进程 = 运行中的程序实例:由 PID 唯一标识,呈父子树状结构,根是 PID 1 的 init/systemd
  • ps aux 拍快照:看所有进程;top / htop 实时监控资源占用--排查卡顿的两大入口
  • 信号是进程的"敲门方式":SIGTERM(15) 礼貌退出 · SIGKILL(9) 强制杀死 · SIGINT(2) 即 Ctrl-C · SIGSTOP(19) 暂停
  • 9 和 19 无法被捕获:是内核保留的最终手段;杀进程先 kill 再 kill -9,别一上来就 -9
  • kill 按 PID · pkill 按模式 · killall 按精确名:动手前先 pgrep 确认目标
  • 作业控制:& 后台启动 · Ctrl-Z 暂停 · jobs 列表 · bg 后台继续 · fg 调回前台,作业号用 %1
  • nohup / disown 让进程活过终端关闭;长任务更推荐用 tmux
  • nice 值(-20~19)调整 CPU 优先级,值越大越"谦让"

动手练习

  1. CPU/内存大户:用 ps aux | sort -k3 -rn | head 找出当前 CPU 占用最高的几个进程,再换成 -k4 看内存大户
  2. 实时监控:打开 top(或安装 htop),分别按 PM 排序,观察哪些进程最耗资源,用 q 退出
  3. 作业控制全流程:跑一个 sleep 300,按 Ctrl-Z 暂停,用 jobs 查看,再用 bg 转后台,fg 调回前台
  4. 信号实验:启动 sleep 300 &,用 kill(SIGTERM)终止它;再启动一个,试试 kill -STOPkill -CONT 观察状态变化
  5. nohup 验证:用 nohup sleep 600 & 启动任务后 exit 关闭终端,重新登录用 pgrep -l sleep 确认它还活着
  6. 信号编号:对比 kill -l 列出的信号编号,记住 1,2,9,15,18,19 各对应哪个信号