第 5 章

进程与作业控制

第5章:进程与作业控制

进程是 Linux 的基本执行单元。理解进程的生命周期——从 fork 复制父进程,到 exec 加载新程序,再到 exit 退出被父进程 wait 回收——是读懂整个操作系统内核的钥匙。本章从进程 ID 体系讲起,深入 fork/exec/wait 的系统调用原理,剖析进程状态机,掌握 ps/top/htop 监控工具,信号机制,作业控制,优先级调度,以及 /proc 虚拟文件系统。

1. 进程基础概念:PID / PPID / PGID / SID

每个进程的身份证

Linux 中每个进程拥有四个核心 ID:

进程树与 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_init exec 出来的 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 内核用一个字母标识进程当前状态,pstop 的 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章:权限与用户管理 →
本章评分
4.9  / 5  (59 评分)

💬 留言讨论