进程与作业控制
第5章:进程与作业控制
进程是 Linux 的基本执行单元。理解进程的生命周期——从 fork 复制父进程,到 exec 加载新程序,再到 exit 退出被父进程 wait 回收——是读懂整个操作系统内核的钥匙。本章从进程 ID 体系讲起,深入 fork/exec/wait 的系统调用原理,剖析进程状态机,掌握 ps/top/htop 监控工具,信号机制,作业控制,优先级调度,以及 /proc 虚拟文件系统。
1. 进程基础概念:PID / PPID / PGID / SID
每个进程的身份证
Linux 中每个进程拥有四个核心 ID:
- PID(Process ID):内核分配的唯一进程标识,全局不重复。
- PPID(Parent PID):创建本进程的父进程 ID。孤儿进程的 PPID 会被改写为 1(init/systemd)。
- PGID(Process Group ID):进程组 ID,Shell 用来将一条管道命令中的所有进程归为一组,方便整体发送信号。
- SID(Session ID):会话 ID,通常等于会话首进程(Shell 本身)的 PID。终端挂起时整个 Session 会收到 SIGHUP。
进程树与 pstree
所有进程形成一棵树,根节点是 PID 1。现代 Linux 发行版上 PID 1 是 systemd,它负责启动用户空间的一切服务。使用 pstree 可以直观地查看进程父子关系:
pstree -p # 显示 PID
pstree -u # 显示用户名
pstree -p $$ # 只展示当前 Shell 的进程树
pstree -p 1 | head # 从 systemd 向下展开
为什么 init/systemd 是 1 号? 内核在启动完成后,第一个用户态进程就是从内核线程
kernel_initexec 出来的 init,它的 PID 硬编码为 1。所有孤儿进程都会被 1 号进程收养,确保僵尸进程可以被回收。
2. fork / exec / wait 原理
fork:复制一个自己
当 Shell 执行一条命令时,它调用 fork() 系统调用,内核将父进程的地址空间完整复制给子进程——但现代内核采用**写时复制(Copy-On-Write, COW)**优化:内存页面实际上在写入时才真正复制,读取时父子进程共享同一物理页。这使 fork 的开销极小。
fork 返回两次:在父进程中返回子进程 PID(>0),在子进程中返回 0,出错返回 -1。
exec:替换进程镜像
fork 之后,子进程调用 execve()(exec 系列函数的系统调用入口),内核将当前进程的代码段、数据段、堆栈全部替换为新程序,PID 不变。exec 成功后不会返回(原代码已不存在)。
wait:收割僵尸
子进程退出后,内核将其状态保存在进程表中,等待父进程调用 wait() / waitpid() 读取。在父进程读取之前,子进程处于 Z(Zombie) 状态——占用进程表槽位,但不消耗 CPU 和内存。父进程 wait 之后,进程表项才被彻底释放。
fork / exec / wait 调用流程
───────────────────────────────────────────────────────
Shell (父进程) 子进程
────────────── ──────────────
pid = fork() ──────▶ pid == 0
execve("/bin/ls", ...)
[地址空间被替换]
ls 运行 ...
exit(0)
│
wait(&status) ◀── 内核通知 SIGCHLD
读取退出状态
释放进程表项
───────────────────────────────────────────────────────
孤儿进程:父进程先退出
子进程的 PPID → 1 (systemd),由 systemd 负责 wait
僵尸进程:子进程已退出,父进程未调用 wait
占据进程表槽,ps 显示 Z 状态
修复方法:向父进程发 SIGCHLD,或终止父进程
3. 进程状态机
Linux 内核用一个字母标识进程当前状态,ps 和 top 的 STAT/S 列即显示此字母:
| 状态 | 字母 | 含义 | 典型场景 |
|---|---|---|---|
| Running / Runnable | R | 正在 CPU 上运行,或在运行队列中等待 CPU | 计算密集型程序 |
| Interruptible Sleep | S | 等待事件(I/O、信号、锁),可被信号唤醒 | 等待键盘输入、网络数据 |
| Uninterruptible Sleep | D | 等待不可中断的 I/O(通常是磁盘/NFS),无法被 kill | 磁盘 I/O 阻塞、NFS 超时 |
| Zombie | Z | 已退出但父进程未 wait | 父进程 bug,未调用 waitpid |
| Stopped | T | 被信号(SIGSTOP/SIGTSTP)暂停 | Ctrl+Z 挂起,调试器断点 |
| Idle Kernel Thread | I | 空闲内核线程(内核 4.14+) | kworker 空闲时 |
进程状态转换图
─────────────────────────────────────────────────────────
fork()
│
▼
┌─────────────────┐
│ R (Runnable) │◀─── 被调度选中 ──▶ CPU 执行
└────────┬────────┘
│ 等待 I/O / 事件
┌──────▼──────┐ ┌─────────────────┐
│ S (Sleep) │ │ D (Uninterrupt) │
└──────┬──────┘ └────────┬────────┘
│ 事件就绪 │ I/O 完成
└──────────┐ ┌─────────┘
▼ ▼
┌───────────────┐
│ R (Runnable) │
└───────┬───────┘
│ SIGSTOP / Ctrl+Z
┌───────▼───────┐
│ T (Stopped) │
└───────┬───────┘
│ SIGCONT
┌───────▼───────┐
│ R (Runnable) │
└───────┬───────┘
│ exit()
┌───────▼───────┐
│ Z (Zombie) │──── parent wait() ────▶ 消亡
└───────────────┘
─────────────────────────────────────────────────────────
D 状态进程无法被 kill -9 终止。 因为 SIGKILL 只有在进程返回用户态时才会处理,而 D 状态进程卡在内核态等待 I/O。解决方案:等待 I/O 完成,或重启系统。大量 D 状态进程通常意味着磁盘或 NFS 故障。
4. ps 命令完全解析
ps aux 与 ps -ef 的区别
| 字段 | ps aux | ps -ef | 含义 |
|---|---|---|---|
| USER / UID | USER | UID | 进程所属用户 |
| PID | PID | PID | 进程 ID |
| PPID | — | PPID | 父进程 ID |
| %CPU | %CPU | C | CPU 使用率 |
| %MEM | %MEM | — | 物理内存占比 |
| VSZ | VSZ | SZ | 虚拟内存大小(KB) |
| RSS | RSS | — | 实际物理内存(KB) |
| TTY | TTY | TTY | 控制终端 |
| STAT/S | STAT | S | 进程状态 |
| START/STIME | START | STIME | 启动时间 |
| TIME | TIME | TIME | 累计 CPU 时间 |
| COMMAND | COMMAND | CMD | 命令行 |
# 查看所有进程(BSD 风格)
ps aux
# 查看所有进程(System V 风格,含 PPID)
ps -ef
# 过滤特定进程
ps aux | grep nginx
ps -ef | grep -v grep | grep sshd
# 显示进程树(ASCII 树形结构)
ps axjf
ps -ejH
# 按 CPU 排序(降序)
ps aux --sort=-%cpu | head -15
# 按内存排序
ps aux --sort=-%mem | head -10
# 显示特定 PID 的详细信息
ps -p 1234 -o pid,ppid,user,stat,cmd
# 显示某用户的所有进程
ps -u www-data
# 显示进程线程
ps -eLf | grep nginx
STAT 列附加字符含义:
s表示 Session Leader;l表示多线程;+表示前台进程组;N表示低优先级(nice > 0);` mylog.log 2>&1 & # 重定向输出
disown:将已运行的后台作业从 Shell 的作业表移除
sleep 999 & disown %1 # 移除后,关闭终端不会发 SIGHUP 给该进程 disown -a # 移除所有作业
screen:多终端会话管理(传统方案)
screen -S mysession # 创建命名会话 screen -ls # 列出所有会话 screen -r mysession # 重新连接
tmux:现代终端复用器(推荐)
tmux new -s work # 新建名为 work 的会话 tmux ls # 列出会话 tmux attach -t work # 重新连接
Ctrl+b d # 分离(detach),会话在后台继续
> **生产环境最佳实践:** 长时间运行的任务(数据处理、编译等)始终在 tmux 或 screen 中启动,而非依赖 nohup。tmux 支持窗口分割、会话共享,且在 SSH 断线后可以无缝恢复。
## 8. 进程优先级:nice / renice / ionice
### nice 值与调度优先级
Linux 进程有两个优先级概念:**nice 值**(-20 到 19,用户空间可见)和内核调度优先级(PR 列,通常 = nice + 20)。nice 值越小,优先级越高;普通用户只能降低自己的优先级(增大 nice 值),提高优先级需要 root。
```bash
# nice:启动时指定 nice 值
nice -n 10 ./cpu-heavy-task # nice 值 +10,低优先级
nice -n -5 ./important-daemon # nice 值 -5,需要 root
# renice:修改正在运行的进程的 nice 值
renice -n 15 -p 1234 # 将 PID 1234 的 nice 值改为 15
renice -n 5 -u worker # 将 worker 用户所有进程的 nice 值改为 5
# 查看 PR 和 NI 列
top # 按 r 交互修改
ps -eo pid,ni,pr,comm | sort -k2 -n
# ionice:I/O 调度优先级
# 三种 I/O 调度类(-c 参数):
# 1 = Realtime(实时,最高)
# 2 = Best-effort(尽力,默认;-n 0-7,0最高)
# 3 = Idle(空闲时才使用 I/O)
ionice -c 3 tar czf backup.tar.gz /data # 备份不影响其他 I/O
ionice -c 2 -n 0 -p 1234 # 提高 PID 1234 的 I/O 优先级
9. pmap / lsof:进程资源查看
# pmap:查看进程内存映射
pmap 1234 # 基本输出(地址、大小、权限、映射名)
pmap -x 1234 # 扩展格式,显示 RSS/Dirty
pmap -d 1234 # 设备格式
# pmap -x 输出字段说明:
# Address Kbytes RSS Dirty Mode Mapping
# 虚拟地址 虚拟大小 物理内存 脏页数 权限 映射源(文件/[heap]/[stack])
# 查看进程总内存
pmap -x 1234 | tail -1
# lsof:list open files(包括网络套接字)
lsof -p 1234 # 查看 PID 1234 打开的所有文件
lsof -u www-data # www-data 用户打开的文件
lsof /var/log/nginx.log # 谁在使用这个文件
lsof -i :80 # 哪个进程在监听 80 端口
lsof -i TCP # 所有 TCP 连接
lsof -i TCP:1-1024 # 1024 以下的 TCP 连接
lsof -i @192.168.1.100 # 与特定 IP 的连接
lsof +D /var/log # 目录下所有被打开的文件(慢)
# 查找删除但未释放的文件(磁盘空间没减少)
lsof | grep deleted
10. /proc/PID 虚拟文件系统
/proc 是内核导出进程信息的虚拟文件系统,不占用真实磁盘空间。每个 PID 下有一组标准文件:
# 查看进程命令行(参数以 \0 分隔)
cat /proc/1234/cmdline | tr '\0' ' '
# 查看进程环境变量
cat /proc/1234/environ | tr '\0' '\n'
# 查看进程内存映射(与 pmap 数据来源相同)
cat /proc/1234/maps
# 查看进程状态信息
cat /proc/1234/status
# 包含:Name/Pid/PPid/Threads/VmRSS/VmSize/SigCatch 等
# 查看进程打开的文件描述符
ls -la /proc/1234/fd
# 0 → stdin, 1 → stdout, 2 → stderr, 3+ → 其他
# 查看文件描述符指向的目标
readlink /proc/1234/fd/3
# 查看进程的 CPU/内存统计(原始数据)
cat /proc/1234/stat
cat /proc/1234/statm # 内存统计(以页为单位)
# 查看进程的网络连接(十六进制地址)
cat /proc/1234/net/tcp
# 查看 OOM 分数(越高越容易被内核 kill)
cat /proc/1234/oom_score
cat /proc/1234/oom_score_adj # 手动调整(-1000到1000)
调试技巧: 如果一个进程启动时的工作目录很重要,可以用
readlink /proc/PID/cwd查看,用readlink /proc/PID/exe查看实际执行文件路径——即使程序启动后原始二进制被删除,这里仍然可以看到路径(显示(deleted))。
上一章
← 第4章:文本三剑客
下一章
第6章:权限与用户管理 →