2.4 进程管理

预计阅读时间:22 分钟

📖 目录

学习目标

学完本章后,你将能够:

  • 使用 pspstree 查看进程信息
  • 理解 Linux 信号机制,正确使用 kill 终止进程
  • 使用 &jobsfgbg 管理前后台作业
  • 使用 nohup 让进程在终端关闭后继续运行
  • 使用 nicerenice 调整进程优先级
  • 理解僵尸进程、孤儿进程的概念

核心知识

  • 进程(Process)——正在运行的程序实例。每个进程有唯一的 PID(进程 ID)
  • ps——进程快照查看(process snapshot),显示 PID、状态、CPU/内存使用
  • 信号(Signal)——Linux 进程间通信的一种方式,用于通知进程执行某种操作(终止、暂停等)
  • kill——发送信号给进程。默认发送 SIGTERM(15),kill -9 发送 SIGKILL
  • 后台作业(& / jobs / fg / bg)——Shell 的作业控制机制
  • nohup——"no hang up",让进程忽略 SIGHUP 信号,终端关闭后继续运行
  • nice 值——进程优先级(-20 最高,19 最低),默认 0
  • 僵尸进程 / 孤儿进程——僵尸 = 已终止但父进程未回收;孤儿 = 父进程先于子进程结束

知识关联

原理讲解

📝 为什么 Linux 要区分用户态和内核态?

CPU 有两种运行模式:用户态(User Mode)内核态(Kernel Mode)。用户程序运行在用户态,只能访问自己的内存空间;需要访问硬件(磁盘、网卡)或执行特权操作(如修改进程优先级)时,必须通过系统调用(System Call)切换到内核态。这种设计的核心目的是隔离和保护:一个有 bug 的用户程序不会搞坏其他进程或内核;一个进程无法读取另一个进程的内存。这就是为什么 kill -9 别人的进程可能需要 root 权限——发送信号给其他用户的进程是"特权操作",必须通过内核中转。用户态/内核态的切换有性能开销(每次切换约 1-10 微秒),但这个开销换来的是整个系统的安全和稳定。

📝 进程调度器(CFS)如何影响进程优先级?

Linux 的 CFS 调度器通过 nice 值(-20 到 19)控制 CPU 时间分配。nice 值不是"绝对优先级",而是"权重":nice=0 的进程获得的 CPU 时间约为 nice=10 进程的 10 倍。CFS 为每个进程维护 vruntime(虚拟运行时间),选择 vruntime 最小的进程运行。nice 值通过权重表影响 vruntime 的增长速度:nice 值越高,权重越小,vruntime 增长得越快,被调度的机会就越少。但注意 nice 值只影响 CPU 时间——磁盘 I/O、网络带宽不受影响。对于需要严格实时响应的场景(如音频处理),需要使用 SCHED_FIFO 或 SCHED_RR 实时调度策略,这些策略绕过 CFS 直接使用优先级队列。

📝 Shell 管道的底层原理

Shell 执行 cmd1 | cmd2 时:① 内核创建 pipe 对象(两个文件描述符:读端和写端);② fork() 创建两个子进程;③ cmd1 的 stdout 通过 dup2() 重定向到管道写端,cmd2 的 stdin 重定向到管道读端;④ 两个进程同时运行——cmd1 写满管道缓冲区(默认 64KB)时被阻塞,cmd2 读空时也被阻塞。这就是天然的"背压"机制:如果 cmd1 产生数据太快,管道会自动减慢 cmd1;如果 cmd2 处理太慢,cmd1 会被等待。整个过程不需要磁盘参与,数据在内核缓冲区中流转,这是管道高效的原因。管道的单向性(半双工)是其设计约束——如果需要双向通信,需要使用两个管道或命名管道(FIFO)。

1. 进程的核心属性——PID、PPID、状态

每个进程在系统中都有唯一的标识:

# PID(Process ID,进程 ID)
# PPID(Parent Process ID,父进程 ID)
# 进程状态(STAT)
#   R (running)    = 正在运行或可运行
#   S (sleeping)   = 正在等待某事完成(可中断睡眠)
#   D (disk sleep) = 不可中断睡眠(通常在等待磁盘 I/O)
#   Z (zombie)     = 已终止,但父进程未回收其退出状态
#   T (stopped)    = 被暂停(如 Ctrl+Z)
#   <              = 高优先级(负 nice 值)
#   N              = 低优先级(正 nice 值)

# 查看进程的父子关系
pstree -p | head -20              # 进程树
# 输出: systemd(1)─┬─systemd-journal(256)
# 输出:              ├─systemd-udevd(302)
# 输出:              ├─sshd(789)───sshd(1234)───bash(1235)
# 输出:              ├─nginx(890)───nginx(891)
# 输出:              └─cron(678)
ps -ef --forest                   # 另一种进程树
# 输出: UID   PID  PPID  C STIME TTY      TIME CMD
# 输出: root    1     0  0 14:30 ?    00:00:01 /sbin/init
# 输出: root  256     1  0 14:30 ?    00:00:00  \_ /lib/systemd/systemd-journald

进程状态详解:top 的 S 列和 ps 的 STAT 列显示的是同一个状态码,只是 top 只显示主状态字母,ps 还会追加辅助标志。完整对应关系如下:

状态码含义top 中显示常见场景
RRunning,正在运行或可运行RCPU 密集型计算、正在响应调度的进程
SSleeping,可中断睡眠S绝大多数进程:等待网络数据、定时器、用户输入
DDisk sleep,不可中断睡眠D等待磁盘 I/O 完成。常见于数据库刷盘、文件系统 sync、NFS 卡住
ZZombie,僵尸Z子进程已退出但父进程未调用 wait() 回收
TStopped,被暂停TCtrl+Z 暂停、或收到了 SIGSTOP
⚠️ D 状态(不可中断睡眠)是排查重点 D 状态进程不能响应任何信号——包括 kill -9。它在等待内核层面的 I/O 操作返回。如果系统出现大量 D 状态进程且持续不消失:① 磁盘硬件故障(坏道重试)② NFS 服务器失联(挂载的远程文件系统不可达)③ 内核 I/O 子系统异常。用 ps -eo pid,stat,wchan:30,cmd | grep '^ *[0-9]* D' 查看 D 状态进程阻塞在哪个内核函数(wchan 列),再对照排查。
# 查看 D 状态进程及其阻塞点
ps -eo pid,stat,wchan:30,cmd | awk '$2 ~ /D/'
# 输出:  4567 D    rpc_wait_bit_killable  mount.nfs ...
# wchan 显示 rpc_wait_bit_killable → 进程卡在 NFS 请求上,检查 NFS 服务器

# ps 附加标志(STAT 列的第二三个字母)
# s  = 会话领导者(session leader,通常是登录 Shell)
# +  = 前台进程组(在前台运行)
# l  = 多线程进程
# <  = 高优先级(负 nice 值);N = 低优先级(正 nice 值)
ps aux | awk '$8 ~ /[D]/'     # 筛选任意 D 状态(包含 Ds、Dl 等)

2. ps——进程快照查看

最常用的 ps aux(BSD 风格):

# ps aux 输出详解
ps aux
# 输出: USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
# 输出: root         1  0.0  0.1 168960 11960 ?        Ss   14:30   0:01 /sbin/init
# 输出: root       256  0.0  0.0  41672  5940 ?        Ss   14:30   0:00 /lib/systemd/systemd-journald
# 输出: alice     1234  0.5  2.0 412456 52348 ?        Ss   14:30   0:15 gnome-shell

# USER   PID  %CPU %MEM    VSZ   RSS  TT  STAT STARTED     TIME COMMAND
# alice  1234  0.5  2.0 412456 52348  ?   Ss   10:00   0:15.23 gnome-shel
#  ↑      ↑     ↑    ↑      ↑     ↑          ↑      ↑       ↑      ↑
#  │      │     │    │      │     │          │      │       │      └── 命令
#  │      │     │    │      │     │          │      │       └── 累计 CPU 时间
#  │      │     │    │      │     │          │      └── 启动时间
#  │      │     │    │      │     │          └── 进程状态
#  │      │     │    │      │     └── 常驻内存大小 (RSS, KB)
#  │      │     │    │      └── 虚拟内存大小 (VSZ, KB)
#  │      │     │    └── 物理内存占比
#  │      │     └── CPU 占比
#  │      └── 进程 ID
#  └── 用户
# 常用 ps 命令
ps aux                             # 所有进程(最常用)
ps auxf                            # 树形显示进程父子关系
ps aux --sort=-%cpu | head -10     # CPU 占用 Top 10
# 输出: USER       PID %CPU %MEM     VSZ   RSS TTY STAT START   TIME COMMAND
# 输出: firefox   3456 45.0 15.0 3456789 456789 ?   Rl  14:30  45:23 /usr/lib/firefox
# 输出: ...
ps aux --sort=-%mem | head -10     # 内存占用 Top 10
ps -eLf                            # 显示线程(-L)
ps -p 1234 -o pid,ppid,cmd,%cpu    # 自定义输出列
# 输出:   PID  PPID CMD                         %CPU
# 输出:  1234  1233 /usr/bin/gnome-shell          0.5

# 经典进程查看组合
ps aux | grep nginx                # 查找 nginx 进程
# 输出: root      890  0.0  0.0   12345   6789 ?  Ss  14:30   0:00 nginx: master process
# 输出: www-data  891  0.0  0.1   12345   7890 ?  S   14:30   0:00 nginx: worker process
ps aux | grep -v grep | grep nginx # 排除 grep 自身(更干净)
pgrep nginx                        # 只显示 PID
# 输出: 890
# 输出: 891
pgrep -a nginx                     # PID + 命令
# 输出: 890 nginx: master process
# 输出: 891 nginx: worker process

3. 信号机制与 kill——如何温和/粗暴地终止进程

信号(Signal)是 Linux 进程间通信的一种方式。内核或另一个进程可以向目标进程发送信号,告诉它"该做某事了"。

信号编号含义默认行为
SIGHUP1终端挂起/断开终止进程
SIGINT2键盘中断 (Ctrl+C)终止进程
SIGTERM15终止信号(默认 kill)终止(可捕获,进程可清理后退出)
SIGKILL9强制杀死(最后手段)终止(不可捕获、不可阻塞)
SIGSTOP19暂停进程(不可阻塞)暂停执行
SIGCONT18继续已暂停的进程继续执行
# kill —— 发送信号
kill 1234                 # 发送 SIGTERM(15),请进程自行退出
# 输出: (无输出,如果成功发送信号)
kill -15 1234             # 同上,显式指定 SIGTERM
kill -9 1234              # SIGKILL——强制杀死(进程没有机会清理)
kill -SIGTERM 1234        # 用名称指定信号

# pkill —— 按名称发送信号
pkill nginx               # 向所有名为 nginx 的进程发送 SIGTERM
pkill -9 nginx            # 强制杀死所有 nginx 进程
pkill -u alice            # 杀死 alice 的所有进程

# killall —— 按名称杀死(比 pkill 更精确,默认 SIGTERM)
killall nginx

# 常用模式:先温和再暴力
kill 1234                  # 第一次:给进程清理的机会
sleep 3                    # 等 3 秒
kill -9 1234               # 还没死?强制结束
终止方式命令示例作用范围适用场景
kill PIDkill 1234指定单个进程精确终止已知 PID 的进程
kill -9 PIDkill -9 1234指定单个进程(强制)SIGTERM 无效时的最后手段
pkill 名称pkill nginx按进程名匹配所有批量终止同名进程
pkill -u 用户pkill -u alice按用户匹配所有进程清理某用户的全部进程
killall 名称killall nginx按进程名精确匹配比 pkill 更精确(不匹配部分名称)
⚠️ kill -9 是最后手段 SIGKILL(kill -9)不能被进程捕获或忽略——内核直接杀死进程。这意味着进程无法:① 保存未写入磁盘的数据 ② 关闭打开的文件 ③ 清理临时文件 ④ 通知子进程。优先用 SIGTERM(默认 kill)让进程优雅退出。只有 SIGTERM 无效时才用 SIGKILL。

4. 作业控制——前后台切换

Shell 的作业控制让你可以在一个终端中同时管理多个任务。

# 后台运行:命令末尾加 &
sleep 300 &
# [1] 12345     ← [作业号] PID
# 输出: [1] 12345

# 查看后台作业
jobs
# [1]-  Running                 sleep 300 &
# [2]+  Running                 find / -name "*.conf" 2>/dev/null &
# 输出: [1]-  Running                 sleep 300 &
# 输出: [2]+  Running                 find / -name "*.conf" 2>/dev/null &

# 把后台作业调到前台
fg %1                  # 将作业 1 调到前台
# 输出: sleep 300
fg                     # 将最近的后台作业调到前台

# 暂停前台进程
# 在前台运行的程序按 Ctrl+Z

# 恢复暂停的作业到后台
bg %1                  # 作业 1 在后台继续运行
# 输出: [1]+ sleep 300 &

# 完整的生命周期
# 1. 前台运行程序(没有 &)
# 2. 按 Ctrl+Z 暂停(变成 Stopped)
# 3. bg 让它在后台继续运行
# 4. fg 把它调回前台
# 5. Ctrl+C 终止

# 如果终端关闭后想让进程继续运行:
# 方案一:nohup
nohup long-running-script.sh &
# 输出: nohup: ignoring input and appending output to 'nohup.out'
# 忽略 SIGHUP 信号,输出写入 ~/nohup.out

# 方案二:disown
long-running-script.sh &
# 输出: [3] 12346
disown                  # 将作业从 Shell 的作业表中移除,关闭终端不会发 SIGHUP
# 输出: (无输出)

# 方案三:tmux 或 screen(最推荐)
tmux new -s mysession   # 创建会话
# 输出: (进入 tmux 会话)
# 在会话中运行程序
# Ctrl+B D 断开(detach),程序继续运行
tmux attach -t mysession # 重新连接

5. 进程优先级——nice 和 renice

Linux 的 CPU 调度器根据 nice 值决定为每个进程分配多少 CPU 时间。

# nice 值范围:-20(最高优先级)到 19(最低优先级),默认 0

# 启动时设置优先级(nice)
nice -n 10 ./backup.sh     # 以低优先级运行(不影响其他进程)
# 输出: (执行 backup.sh)
nice --10 ./urgent.sh      # 以高优先级运行(需要 root)

# 修改运行中进程的优先级(renice)
renice -n 5 -p 1234        # 将 PID 1234 的 nice 设为 5
# 输出: 1234 (process ID) old priority 0, new priority 5
sudo renice -n -5 -p 1234   # 设为高优先级(需要 root)
# 输出: 1234 (process ID) old priority 5, new priority -5

# 查看进程的 nice 值
ps -eo pid,ni,cmd | grep backup
# 输出:  12345  10 ./backup.sh
# top 中 NI 列显示 nice 值

CFS 调度器原理——nice 值到底怎么起作用:现代 Linux 使用 CFS(Completely Fair Scheduler,完全公平调度器)。它不是按优先级"排队执行",而是给每个进程计算一个"虚拟运行时间"(vruntime),每次调度永远选择 vruntime 最小的进程运行。nice 值通过权重改变 vruntime 的增长速度:

# nice 值与权重的换算关系(内核维护的查找表,部分数据):
# nice -20 → 权重 88761(vruntime 增长最慢,获得最多 CPU)
# nice   0 → 权重 1024(基准)
# nice  19 → 权重    15(vruntime 增长最快,几乎拿不到 CPU)

# 直观理解:
# 一个 nice=0 的进程和一个 nice=10 的进程同时就绪,
# nice=0 的进程大约获得 10 倍于 nice=10 进程的 CPU 时间。

# 实操:观察 nice 值对 CPU 分配的影响
# 启动两个相同的计算任务,一个正常、一个低优先级
sha1sum /dev/zero &                 # nice=0,占满一个核
# 输出: [7] 15000
nice -n 19 sha1sum /dev/zero &      # nice=19,几乎让出 CPU
# 输出: [8] 15001
top -b -n 1 | grep sha1sum
# 输出:  15000 alice  20   0   13100   456  120 R  99.7  0.0 0:30.12 sha1sum
# 输出:  15001 alice  39  19   13100   456  120 R   0.3  0.0 0:00.05 sha1sum
# 第一个进程 99.7% CPU,第二个只有 0.3%——这就是权重的效果

# 关于调度器的几个事实:
# 1. nice 值只影响 CPU 时间分配,不影响内存、I/O 等其他资源
# 2. 负 nice 值(高优先级)需要 root 权限
# 3. 实时调度(SCHED_FIFO/SCHED_RR)与 nice 无关,用于嵌入式/音频等场景

6. 僵尸进程和孤儿进程

僵尸进程(Zombie):子进程已终止,但父进程没有调用 wait() 读取其退出状态。僵尸进程不占用内存或 CPU,只占用内核进程表中的一项(有限资源)。如果僵尸进程大量堆积,系统可能无法创建新进程。

孤儿进程(Orphan):父进程先于子进程终止。孤儿子进程会被 PID 1(systemd)收养,成为"过继给 systemd 的孤儿"。

# 查找僵尸进程
ps aux | grep -w Z
# 输出: alice  9999  0.0  0.0      0     0 ?        Z   14:30   0:00 [sh] 
ps -eo pid,stat,cmd | grep "^.* Z"
# 输出:  9999 Z [sh] 

# 僵尸进程不能被 kill -9 杀死——因为它已经"死了"。
# 唯一的办法是:杀死它的父进程(让其父进程回收僵尸)

实战:制造一个僵尸进程并清理它。僵尸进程产生的根本原因是父进程没有调用 wait() 读取子进程退出状态。用一小段 C 代码可以完整演示:

# 1. 写一个不回收子进程的程序
cat > /tmp/zombie.c <<'EOF'
#include <unistd.h>
#include <stdio.h>
int main() {
    if (fork() == 0) {
        printf("child pid: %d\n", getpid());
        _exit(0);            // 子进程立即退出
    }
    sleep(60);               // 父进程睡 60 秒,期间不调用 wait()
    return 0;
}
EOF
gcc -o /tmp/zombie /tmp/zombie.c

# 2. 运行它,子进程 3 秒后退出变僵尸
/tmp/zombie &
# 输出: child pid: 15123
sleep 3
ps -eo pid,ppid,stat,cmd | grep -w Z
# 输出:  15123  15122 Z [zombie] <defunct>
# Z 状态确认,PPID 是父进程 15122

# 3. 清理方法一:杀死父进程,僵尸由 init(systemd)收养并回收
kill 15122
ps -eo pid,stat,cmd | grep -w Z || echo "僵尸已清除"
# 输出: 僵尸已清除

# 4. 如果父进程是系统服务(如 sshd),不能杀——需要排查服务 bug
# 或者用 systemd 的 KillMode 重启服务:
# sudo systemctl restart sshd   # 服务重启后其所有子进程被回收

# 5. 生产环境发现大量僵尸:先确认父进程是谁
ps -eo ppid,stat | awk '$2 ~ /Z/' | sort | uniq -c | sort -rn
# 输出:     87 789
# 87 个僵尸进程的父进程都是 789 → 排查 789 这个进程为什么不回收子进程
ps -p 789 -o pid,cmd
# 输出:  789 nginx: master process
📝 僵尸进程的记忆法 僵尸进程本身不消耗 CPU 和内存,只占用一个进程表条目(task_struct)。单个僵尸无害,但若父进程持续制造僵尸(程序 bug),进程表会耗尽,系统无法创建新进程。此时"杀僵尸"没有意义——必须修父进程,或重启父进程所在的服务。

7. 进程间通信(IPC)——进程之间怎么协作

每个进程有独立的内存空间,进程之间交换数据必须通过内核提供的 IPC 机制。本章的信号(signal)只是其中一种。

方式速度适用场景例子
管道(pipe)父子进程间单向字节流ls | grep conf(Shell 管道就是 IPC)
信号(signal)通知事件,不传数据kill -USR1 nginx 让 nginx 重开日志
共享内存(shm)最快大量数据高频读写(配合信号量同步)数据库缓冲池、消息队列中间件
消息队列(msg)进程间传递结构化消息传统微服务内部通信
Socket中/慢任意主机间通信(本机可用 Unix Socket 加速)Nginx 与 PHP-FPM 的 unix:// 通信
# 查看系统当前 IPC 资源
ipcs -m                      # 共享内存段
# 输出: key        shmid      owner      perms      bytes      nattch
# 输出: 0x00000000 12345      mysql      600        8388608    2
ipcs -q                      # 消息队列
ipcs -s                      # 信号量

# 查看哪个进程在使用共享内存(排查"内存去哪了")
lsof /dev/shm 2>/dev/null        # /dev/shm 是 POSIX 共享内存的挂载点
# 输出: COMMAND  PID  USER  FD   TYPE  DEVICE  SIZE/OFF  NODE  NAME
# 输出: mysql    890  mysql DEL  REG   0,32    8388608   12345 /dev/shm/xxx
# DEL 表示已删除但仍被进程持有的段(数据仍占用内存)
df -h /dev/shm
# 输出: Filesystem      Size  Used Avail Use% Mounted on
# 输出: tmpfs           7.8G  1.2G  6.6G  16% /dev/shm

8. systemd 与进程——服务化后的进程管理

现代 Linux 上,后台服务进程大多由 systemd 启动。用 systemd 的视角管理进程,比直接 kill 更优雅:

# 查看某服务管理的所有进程
systemctl status nginx
# 输出: ● nginx.service - A high performance web server...
# 输出:      Active: active (running)
# 输出:      CGroup: /system.slice/nginx.service
# 输出:              ├─890 "nginx: master process"
# 输出:              ├─891 "nginx: worker process"
# 输出:              └─892 "nginx: worker process"
# CGroup 段列出该服务进程组的所有成员

# 优雅重启(不中断连接,nginx 会 fork 新进程替换旧的)
sudo systemctl reload nginx

# systemd-cgls —— 以树形查看整个 cgroup 层级
systemd-cgls --no-pager | head -30
# 输出: Control group /:
# 输出: ├─user.slice
# 输出: │ └─user-1000.slice
# 输出: │   └─session-3.scope
# 输出: │     └─1235 /bin/bash
# 输出: └─system.slice
# 输出:   ├─nginx.service
# 输出:   ├─sshd.service
# 输出:   └─cron.service

# systemd 通过 cgroup 实现的资源限制(cgroup v2)
sudo systemctl set-property nginx.service CPUWeight=200
sudo systemctl set-property nginx.service MemoryMax=512M
systemctl show nginx.service -p MemoryMax -p CPUWeight
# 输出: MemoryMax=536870912
# 输出: CPUWeight=200

# 服务异常时的处理顺序(与直接 kill 对比)
sudo systemctl status mysqld     # 看日志与状态
sudo systemctl restart mysqld    # 正常重启
sudo systemctl kill -s KILL mysqld  # 服务不响应时的强制手段

9. 进程监控实战——pidstat/pstree/htop 组合排查

把本章工具组合起来,走一遍"系统突然变慢"的标准排查流程:

# 第 1 步:整体判断——uptime + top 看负载构成
uptime
# 输出: 10:30:00 up 3 days, 2:31, 3 users, load average: 8.50, 7.20, 5.10
# 8 核机器 load 8.5 → CPU 过载,找凶手

# 第 2 步:pidstat 锁定嫌疑进程(比 top 更适合脚本化)
pidstat 1 5
# 输出: 10:30:01  1000  1234   45.0    3.0    0.0   48.0   2  java
# 输出: 10:30:02  1000  1234   46.2    2.8    0.0   49.0   2  java
# java 进程稳定吃掉近半个 CPU?继续深挖

# 第 3 步:pstree 看进程关系(确认是哪个服务的子进程)
pstree -p | grep java
# 输出: |-java(1234)-+-{java}(15001)
# 输出:              |-{java}(15002)
# 输出:              `-{java}(15003)
# 是哪个服务的 java?用 systemctl status 或 lsof -p 1234 查

# 第 4 步:htop 树形视图 + 过滤(交互式深挖)
htop
# F5 树形视图 → F4 输入 java 过滤 → F6 按 CPU% 排序
# 观察线程:按 H 显示线程,找到具体热点线程 TID

# 第 5 步:处理
# 如果是业务高峰,记录基线即可;如果是异常,按本章「信号机制与 kill」的信号流程处理

示例代码

# 练习 1:查看进程
ps aux | head -10
# 输出: USER       PID %CPU %MEM    VSZ   RSS TTY STAT START   TIME COMMAND
# 输出: root         1  0.0  0.1 168960 11960 ?   Ss  14:30  0:01 /sbin/init
# 输出: ...
pstree -p | head -15
# 输出: systemd(1)─┬─systemd-journal(256)
# 输出:              ├─sshd(789)───sshd(1234)───bash(1235)

# 练习 2:前后台作业
sleep 100 &
# 输出: [1] 12345
jobs
# 输出: [1]+  Running                 sleep 100 &
fg %1                      # 调到前台
# 输出: sleep 100
# 按 Ctrl+Z 暂停
bg                          # 在后台继续
# 输出: [1]+ sleep 100 &
jobs
# 输出: [1]+  Running                 sleep 100 &

# 练习 3:信号和 kill
sleep 500 &
# 输出: [2] 12346
kill $!                    # $! = 上一个后台进程的 PID
# 输出: [2]+  Terminated             sleep 500
# 另一个方式
sleep 500 &
# 输出: [3] 12347
PID=$!
echo "PID is $PID"
# 输出: PID is 12347
kill -15 $PID

# 练习 4:nohup
nohup sleep 300 &
# 输出: nohup: ignoring input and appending output to 'nohup.out'
# 输出: [4] 12348
# 关闭终端重新打开,用 ps aux | grep sleep 查看是否还在运行

# 练习 5:nice 和 renice
nice -n 15 find / -name "*.conf" 2>/dev/null &
# 输出: [5] 12349
ps -eo pid,ni,cmd | grep find
# 输出:  12349  15 find / -name *.conf
renice -n 10 -p $(pgrep -f "find / -name") 2>/dev/null

# 练习 6:处理挂起的进程
# 运行 top → 不退出 top,按 Ctrl+Z 暂停
jobs
# 输出: [6]+  Stopped                 top
bg %6                      # top 在后台运行
fg %6                      # 把它调回前台
# 按 q 退出

常见错误

错误/误区解决方案
"kill -9 是杀死进程的唯一方式"优先用 kill(SIGTERM,信号 15)——它让进程有机会清理资源、保存数据。只有等了 5-10 秒后进程还没死,才用 kill -9
"ps aux | grep 进程名 结果包含了 grep 自身"| grep -v grep 排除。或用 pgrep 进程名ps -C 进程名
"终端关闭后后台进程就死了"Shell 后台进程(&)在终端关闭时会收到 SIGHUP 信号而终止。用 nohupdisown、或 tmux 让进程独立于终端运行。
"看到 Z 状态的进程以为是什么严重问题"少量僵尸进程通常无害。它们只是等待父进程回收的"已终止进程"。但如果大量堆积(100+),说明父进程有 bug——需要排查父进程为什么不调用 wait()
"以为 kill -9 0 是杀死 PID 0"kill -9 0 会杀死当前进程组的所有进程(包括当前 Shell!)。PID 0 是内核的 idle 进程,不能用 kill 杀死。

最佳实践

#建议说明
1先 SIGTERM,后 SIGKILL始终给进程优雅退出的机会。SIGKILL 是核武器——用了就没有挽回余地。
2用 pgrep 确认 PID 后再 killpgrep -a nginx 先列出 PID 和命令,确认目标正确后再 pkill nginx
3长时间运行的任务用 tmuxtmux 在后台保持会话,即使 SSH 断开也能重新连接。比 nohup 更灵活(可以随时 reconnect 查看输出)。
4后台任务放特定目录运行对于后台进程,先 cd /tmp 或专用工作目录再运行,避免"被占用目录无法卸载"的问题。
5用 ulimit 限制进程资源如果某程序失控(内存泄漏、fork 炸弹),ulimit -u 100 限制最大进程数,ulimit -v 1048576 限制虚拟内存。

练习题

  1. (概念)SIGTERM(15)和 SIGKILL(9)有什么区别?为什么优先用 SIGTERM?
  2. (概念)僵尸进程和孤儿进程有什么区别?各自如何产生?
  3. (实操)启动一个 sleep 300 后台进程,用 jobs 查看,用 fg 调到前台,按 Ctrl+Z 暂停,用 bg 恢复到后台,最后用 kill 终止它。
  4. (实操)使用 ps aux --sort=-%mem | head -10 找出当前系统内存占用最高的 10 个进程。
  5. (实操)nohup 启动一个长时间运行的任务(如 find / -type f 2>/dev/null &>),然后关闭终端重新打开,用 ps 确认进程还在运行。
  6. (探究 🔍)创建两个进程:sleep 300 &nohup sleep 300 &。用 ps -o pid,comm,sid 查看它们的 SID(会话 ID)。理解为什么 SIGHUP 会杀死作业而不杀死 nohup 进程。
  7. (探究 🔍)strace -p PID(需要 sudo)跟踪一个进程的系统调用。先找一个无害的进程(如 sleep 500),观察 strace 输出。用 Ctrl+C 停止跟踪。
点击查看答案
  1. (概念)SIGTERM(15)允许进程捕获信号并执行清理(保存数据、释放资源)后优雅退出。SIGKILL(9)强制内核立即终止进程,无法被捕获、忽略或处理。优先用 SIGTERM 给进程优雅终止的机会,避免数据丢失或资源泄漏。
  2. (概念)僵尸进程:子进程已终止但父进程未调用 wait() 获取退出状态,进程表条目残留。孤儿进程:父进程先于子进程退出,子进程被 init(systemd,PID 1)收养。僵尸消耗进程表条目但无内存负担;孤儿不会造成资源泄漏。
  3. (实操)`sleep 300 &` → `jobs` → `fg %1` → Ctrl+Z(暂停)→ `bg %1`(后台继续)→ `kill %1`。完整练习了作业控制四步曲。验证:`ps aux | grep sleep` 确认进程已终止。
  4. (实操)`ps aux --sort=-%mem | head -10` 输出内存占用前十进程。关注 %MEM 列和 RSS 列(物理内存占用)。若发现某进程异常占用过大内存,用 `top -p PID` 持续监控该进程。
  5. (实操)`nohup find / -type f 2>/dev/null &`。关闭终端重新打开后 `ps aux | grep find` 确认进程继续运行。nohup 的作用是忽略 SIGHUP 信号。输出默认写入当前目录的 nohup.out。
  6. (探究 🔍)`sleep 300 &` 是当前会话的作业,终端关闭时收到 SIGHUP。`nohup sleep 300 &` 设置了忽略 SIGHUP。`ps -o pid,comm,sid` 显示两者 SID(会话 ID)相同,但 nohup 的进程信号掩码不同。理解 SIGHUP → 会话 → 作业的关系。
  7. (探究 🔍)终端 A 启动 `sleep 500 &`,记下 PID。终端 B 执行 `sudo strace -p PID`。strace 跟踪系统调用(如 rt_sigprocmask、nanosleep)。strace 是诊断"程序卡在哪里"的利器。按 Ctrl+C 安全退出。

学习检查点

学完本章后,请检验自己是否掌握以下内容:

检查项自测问题验证方法
概念理解能用自己的话解释进程状态(R/S/D/Z/T)的含义尝试向他人讲解
命令操作能不查文档使用 ps、kill、jobs、nohup、nice 管理进程在终端实际执行
原理掌握能说出 SIGTERM 和 SIGKILL 的区别以及信号机制的工作原理画出信号处理流程图
故障排查能独立排查和处理僵尸进程(Z 状态)问题模拟故障并修复
最佳实践能说明为什么应该先用 kill 而不是 kill -9 终止进程对比不同终止方式

本章总结

进程管理是 Linux 系统管理的核心操作。你学会了用 ps 查看进程、用 kill 发送信号控制进程、用 jobs/fg/bg 管理前后台作业、用 nohup 让进程脱离终端运行、用 nice/renice 调整进程优先级。理解僵尸进程和孤儿进程能帮你在排查系统异常时"看到"这些特殊状态。

速查表

概念一句话定义
PID / PPID进程 ID 和父进程 ID
ps aux查看所有进程的快照(BSD 风格)
信号(Signal)进程间通信方式,通知进程执行某种操作
kill / kill -9发送 SIGTERM(优雅终止)/ SIGKILL(强制杀死)
jobs / fg / bgShell 作业控制:查看、调前台、调后台
nohup忽略 SIGHUP,终端关闭后继续运行
nice 值进程优先级(-20 最高,19 最低)
僵尸进程已终止但未回收的进程(Z 状态)

下一步:进入 2.5:时间管理与日志「时间管理与日志」,学习系统时间同步和日志管理。

延伸阅读

  • 命令strace——跟踪进程的系统调用和信号,深入理解进程行为
  • 命令lsof——列出进程打开的文件(List Open Files),排查"哪个进程在占用这个文件"
  • 命令tmux——终端复用器,比 nohup 更强大的会话管理工具
  • 文章:搜索 "Linux process states" 了解更详细的进程状态(包括 D、S、R、T、Z、X)

常见问题

find 和 grep 命令有什么区别?
find 按文件名、大小、时间等属性在文件系统中搜索文件。grep 在文件内容中搜索匹配的文本。组合使用:find . -name "*.log" -exec grep "error" {} ; 在日志文件中搜索错误。现代替代方案:fd(find 的更快替代)和 ripgrep/rg(grep 的更快替代)。
sed -i 原地替换有什么风险?
sed -i 直接修改原文件,出错就不可恢复。建议先不加 -i 看看输出:sed 's/old/new/g' file,确认无误后加 -i 执行。更安全的是加备份后缀:sed -i.bak 's/old/new/g' file,会生成原文件的 .bak 备份。
awk 比 cut 好在哪?
cut 只能按单个分隔符切割固定的列,遇到空字段会错位。awk 支持任意分隔符(-F)、条件过滤、数学计算、字符串处理。例如 awk '$3 > 100 {print $1, $5}' 只输出第三列大于 100 的行的第一和第五列。复杂文本处理优先用 awk。
↑ 回到顶部