Linux(六)深入理解 Linux 进程管理:从硬件到调度

本文是一份系统性的 Linux 进程管理入门教程,从最底层的硬件原理讲起,层层递进,直到进程监控与实践。每一节都配有图解、代码示例和动手实验,帮助你从"看懂"到"会用",彻底掌握进程的本质。

🗺️ 学习知识地图

复制代码
硬件基础(冯·诺依曼) → 操作系统(资源管理) → 进程概念 → 进程创建(fork) → 进程标识(PID/进程树)
                                                                 ↓
进程监控(/proc) ← 进程间通信(FIFO) ← 进程调度(CFS) ← 存储体系 ← 进程状态 ← 进程回收
                                                                 ↓
                                                              信号控制

1. 计算机的骨架:冯·诺依曼体系结构

本节解决的问题:为什么要先讲硬件?因为进程本质上是硬件资源的抽象,理解硬件才能真正理解进程。

1945 年,冯·诺依曼提出存储程序思想:将程序指令和数据统一存放在存储器中,由控制器逐条取出执行。这一思想奠定了所有现代计算机的基础。

1.1 五大核心部件

现代计算机由五大部件组成:

  • 运算器:执行算术和逻辑运算(CPU 中的 ALU)
  • 控制器:控制取指令、译码、执行、响应中断(与运算器合成 CPU)
  • 存储器:即内存(RAM),存放正在运行的程序和数据
  • 输入设备:键盘、鼠标、网卡(接收数据)、摄像头等
  • 输出设备:显示器、打印机、网卡(发送数据)、扬声器等

1.2 冯·诺依曼结构的核心数据流

复制代码
[输入设备] → 数据 → [内存] ← 指令 ← [磁盘(程序文件)]
                        ↑↓
                    [CPU(运算器+控制器)]
                        ↑↓
                   [输出设备]

1.3 为什么这个结构决定了进程的本质

冯·诺依曼结构的核心是"存储程序,顺序执行"。这意味着三个铁律:

  1. 任何程序必须先加载到内存才能运行
  2. CPU 只能从内存取指令,不能直接从磁盘执行
  3. 所有设备的操作都必须通过 CPU 协调

这三点直接定义了进程:进程就是加载到内存中、正在被 CPU 执行的程序实例

注意:许多设备兼具输入输出功能,最典型的是网卡。没有 CPU 的参与,设备之间无法直接通信。当你在键盘上按键,键盘控制器会向 CPU 发出中断,CPU 暂停当前任务,执行内核的中断处理程序读取按键并分发给应用程序。


2. 操作系统:硬件的大管家(先描述,再组织)

本节解决的问题:操作系统到底做了什么?为什么会有"进程"这个概念?

操作系统是运行在硬件之上的第一层软件,负责管理 CPU、内存、磁盘、I/O 设备,并向应用程序提供统一的系统调用 (如 readwritefork)。

2.1 操作系统解决的核心矛盾

CPU 只有一个,但需要同时运行多个程序

操作系统的对策是时间片轮转 :让每个程序轮流占用 CPU 一小段时间(通常几毫秒),然后切换到下一个。因为切换极快,用户感觉所有程序都在"同时"运行。这种"正在执行的程序的幻象"就是进程

2.2 操作系统的分层抽象

复制代码
┌─────────────────────────────────┐
│ 应用程序(浏览器、编辑器、Shell) │
├─────────────────────────────────┤
│ 系统调用接口(read/write/fork)   │
├─────────────────────────────────┤
│ 操作系统内核(进程/内存/文件/I/O)│
├─────────────────────────────────┤
│ 硬件(CPU/内存/磁盘/网卡/键盘)   │
└─────────────────────────────────┘

2.3 通俗类比:操作系统就像公司老板

  • 公司里只有一个 CEO(CPU),但有很多任务要做
  • 老板把时间分成很多小段,轮流处理每个任务(时间片轮转)
  • 每个任务就是一个"进程"
  • 老板的秘书(操作系统内核)负责:
    • 记录每个任务的进度(进程上下文)
    • 安排任务的执行顺序(进程调度)
    • 给每个任务分配办公室和办公用品(内存和资源)
    • 协调任务之间的沟通(进程间通信)

2.4 完整的用户交互链条

复制代码
用户操作 → 输入设备 → 中断 → CPU → 操作系统内核 → 应用程序 → 系统调用 → 操作系统 → 输出设备 → 用户看到结果

3. 进程(内核数据结构对象+代码数据)的诞生:fork() 系统调用

本节解决的问题 :进程是怎么"生"出来的?为什么 fork() 会返回两次?

3.1 程序 vs 进程

  • 程序 :静态的可执行文件,存储在硬盘上(如 a.out
  • 进程:程序的一次执行实例,是动态的、有生命周期的实体

同一个程序可以启动多次,产生多个进程(例如打开三个终端,运行的都是 bash,但它们是三个独立的进程,有不同的 PID)。

3.2 fork():创建进程的唯一方法

在 Linux 中,新进程只能通过 fork() 系统调用创建。调用 fork() 的进程称为父进程 ,新产生的进程称为子进程

函数原型:

c 复制代码
#include <unistd.h>
pid_t fork(void);
3.2.1 "调用一次,返回两次"的本质

很多初学者不理解为什么 fork() 会返回两次。其实不是同一个函数返回了两次,而是**fork() 创建了一个新的进程,现在有两个几乎完全相同的进程在执行同一个代码**。

就像你在看书看到第 10 页时,复印了一本一模一样的书,也翻到第 10 页。现在你和复印件都从第 10 页开始读,但你们是两个独立的人,可以读不同的内容。

复制代码
父进程执行fork()
    ↓
┌─────────────────┐  返回子PID>0  ┌─────────────────┐
│ 父进程          │ ←──────────── │ 内核创建子进程  │
└─────────────────┘               └─────────────────┘
                                      ↓ 返回0
                                  ┌─────────────────┐
                                  │ 子进程          │
                                  └─────────────────┘
  • 在父进程中返回子进程的 PID(大于 0)
  • 在子进程中返回 0
  • 若出错返回 -1

通过返回值可以区分父子,让它们执行不同代码:

c 复制代码
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int main() {
    pid_t pid = fork();
    if (pid < 0) { perror("fork"); exit(1); }
    else if (pid == 0) {
        printf("子进程 PID: %d, 父进程 PPID: %d\n", getpid(), getppid());
    } else {
        printf("父进程 PID: %d, 子进程 PID: %d\n", getpid(), pid);
    }
    return 0;
}

注意:父子进程的执行顺序由内核调度决定,不固定。

3.3 写时复制(Copy-on-Write)

fork() 后,父子进程最初共享同一块物理内存。内核将内存页标记为只读,只有当其中一方尝试修改某页时,内核才为该页创建副本。

复制代码
fork()前:父进程拥有物理内存页A、B、C
fork()后:
┌─────────────┐       ┌─────────────┐
│ 父进程页表  │       │ 子进程页表  │
│ A → 物理页A │       │ A → 物理页A │
│ B → 物理页B │       │ B → 物理页B │
│ C → 物理页C │       │ C → 物理页C │
└─────────────┘       └─────────────┘
        ↓ 父进程修改页B
┌─────────────┐       ┌─────────────┐
│ 父进程页表  │       │ 子进程页表  │
│ A → 物理页A │       │ A → 物理页A │
│ B → 物理页B'│       │ B → 物理页B │
│ C → 物理页C │       │ C → 物理页C │
└─────────────┘       └─────────────┘

写时复制让 fork() 速度极快,且大幅节省内存。

3.4 exec():让子进程执行新程序

fork() 创建的子进程和父进程执行相同的代码。如果想让子进程执行不同的程序,需要调用 exec() 系列函数。

exec()替换 子进程的整个地址空间,加载新的可执行文件,从新程序的 main() 开始执行。它不创建新进程,只是改变当前进程的执行内容。

Shell 执行命令的标准流程:fork() 创建子进程 → 子进程调用 exec() 加载命令程序 → 父进程调用 wait() 等待子进程结束。

3.5 fork 的常见用途

  • 守护进程 :父进程 fork() 后立即退出,子进程成为孤儿被 init 收养,再调用 setsid() 脱离终端
  • 并发服务器 :主进程 fork() 子进程处理每个客户端请求
  • 执行新程序:如上所述,Shell 运行命令的方式

4. 进程的标识:PID 与进程树

本节解决的问题:如何唯一标识一个进程?进程之间是什么关系?

4.1 PID 和 PPID

每个进程拥有唯一的进程 ID,即 PID 。函数 getpid() 返回当前进程 PID。同时,每个进程还记录其父进程的 ID,称为 PPID ,可通过 getppid() 获取。

4.2 进程树

系统启动后,内核创建的第一个用户态进程是 init(或 systemd),其 PID 为 1。此后所有进程都是它的后代,形成一棵进程树

复制代码
systemd(1)
├─ systemd-journald(456)
├─ NetworkManager(789)
├─ sshd(1234)
│  └─ sshd(5678)
│     └─ bash(5679)
│        └─ vim(7890)
└─ gnome-shell(2345)
   ├─ firefox(3456)
   └─ gnome-terminal(4567)
      └─ bash(4568)

查看进程树的命令:

bash 复制代码
pstree -p
ps axjf
ps aux --forest

5. 进程状态完全解析

本节解决的问题:进程在生命周期中会经历哪些状态?每个状态代表什么?

进程在生命周期中会在多种状态间切换。使用 ps auxps axj 可查看,STAT 列的第一个字符代表基本状态。

5.1 核心:进程状态转换图

这是全文最重要的一张图,务必理解每个转换的触发条件:

复制代码
┌─────────────┐   被调度选中   ┌─────────────┐
│ 就绪(R)     │ ─────────────→ │ 运行(R)     │
└─────────────┘ ←───────────── └─────────────┘
       ↑                时间片用完
       │ 等待事件发生
       │ (I/O完成、信号)
       ↓
┌─────────────┐   事件发生     ┌─────────────┐
│ 睡眠(S/D)   │ ─────────────→ │ 就绪(R)     │
└─────────────┘                └─────────────┘
       │
       │ 进程退出
       ↓
┌─────────────┐   父进程wait() ┌─────────────┐
│ 僵尸(Z)     │ ─────────────→ │ 死亡(X)     │
└─────────────┘                └─────────────┘
       ↑
       │ 收到SIGSTOP/SIGTSTP
       │
┌─────────────┐   收到SIGCONT  ┌─────────────┐
│ 暂停(T/t)   │ ─────────────→ │ 就绪(R)     │
└─────────────┘                └─────────────┘

5.2 基本状态详解

状态 含义 常见触发场景
R 运行中或可运行(在就绪队列等待 CPU) 计算密集型死循环 while(1);
S 可中断睡眠(等待事件,可被信号唤醒) sleep()、等待用户输入
D 不可中断睡眠(通常等待 I/O,信号不能打断) 大量读写磁盘、NFS 访问、Swap 换入换出
Z 僵尸(进程已退出,父进程尚未回收) 子进程 exit(),父进程未 wait()
T 暂停(收到 SIGSTOP 或 SIGTSTP) kill -19 <PID> 或 Ctrl+Z
t 跟踪停止(被调试器控制) gdb 断点处
X 死亡(即将销毁,极短暂,几乎观察不到) 进程退出瞬间

⚠️ 重要:D 状态进程不能被 kill -9 杀死!因为它此时正在执行内核态的 I/O 操作,内核为了保证数据一致性,不允许在 I/O 过程中中断进程。Swap 换入换出是导致 D 状态的常见原因之一。解决 D 状态的唯一方法是等待 I/O 完成或重启系统。

5.3 状态附加标志

符号 意义
+ 属于前台进程组,可直接接受键盘输入
+ 后台进程(用 & 启动或 Ctrl+Z 放到后台)
s 会话领导者
< 高优先级(nice 值为负)
N 低优先级(nice 值为正)
l 多线程进程

5.4 动手实验:观察各种进程状态

R+(前台死循环)

c 复制代码
int main() { while(1); return 0; }

编译运行后,在另一个终端执行 ps aux | grep 程序名,看到 STAT 为 R+

S+(死循环 + 睡眠)

c 复制代码
#include <unistd.h>
int main() { while(1) { write(1, ".", 1); sleep(1); } }

状态为 S+,因为进程大部分时间在等待 sleep 结束。

Z(僵尸)

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main() {
    pid_t pid = fork();
    if (pid == 0) { exit(0); }          // 子进程立即退出
    else { sleep(60); }                 // 父进程 60 秒内不回收
    return 0;
}

子进程在 ps 中显示为 Zdefunct

T(暂停)

bash 复制代码
./程序 &          # 后台运行
kill -19 <PID>    # 发送 SIGSTOP 信号暂停
ps aux | grep <PID>   # 状态变为 T
kill -18 <PID>    # 发送 SIGCONT 信号恢复

t(跟踪停止) :用 gdb 打断点运行程序,在断点处执行 ps,显示状态为 t

5.5 通过 /proc 查看状态

bash 复制代码
cat /proc/<PID>/status | grep State

6. 僵尸进程与孤儿进程

本节解决的问题:什么是僵尸进程?什么是孤儿进程?它们有什么危害?如何避免?

6.1 僵尸进程(Zombie)

子进程退出时,内核会释放其代码、数据、堆栈等内存,但保留 task_struct 和 PID 以存放退出状态,直到父进程调用 wait()waitpid() 获取。在父进程回收之前,子进程就是僵尸。

僵尸进程的危害:不泄漏内存,但会占用 PID。大量僵尸可能导致 PID 耗尽,无法创建新进程。

避免僵尸的三种方法

  1. 父进程主动调用 wait()waitpid() 等待子进程
  2. 设置信号处理:signal(SIGCHLD, SIG_IGN),让内核自动回收子进程
  3. 使用双 fork() 技巧:让孙进程被 init 收养

代码示例:父进程主动回收子进程

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

int main() {
    pid_t pid = fork();
    if (pid < 0) { perror("fork"); exit(1); }
    else if (pid == 0) {
        printf("子进程%d运行中,2秒后退出\n", getpid());
        sleep(2);
        exit(0);
    } else {
        printf("父进程%d等待子进程%d退出\n", getpid(), pid);
        waitpid(pid, NULL, 0); // 阻塞等待指定子进程退出
        printf("子进程已回收,父进程退出\n");
    }
    return 0;
}

6.2 孤儿进程(Orphan)

如果父进程先于子进程退出,子进程成为孤儿。内核会立即将它的 PPID 改为 1(init/systemd)。

init 进程会循环调用 wait() 回收任何终止的子进程,因此孤儿进程完全无害,不会变成僵尸,也不会泄漏资源。

模拟孤儿进程

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main() {
    pid_t pid = fork();
    if (pid == 0) {
        sleep(2);   // 等待父进程退出
        printf("孤儿 PID:%d, 新 PPID:%d\n", getpid(), getppid());
        exit(0);
    } else {
        printf("父进程退出\n");
        exit(0);
    }
}

6.3 对比图解

复制代码
正常情况:
父进程 → fork() → 子进程 → exit() → 父进程wait() → 子进程完全销毁

僵尸进程:
父进程 → fork() → 子进程 → exit() → 父进程不wait() → 子进程变成僵尸(Z)
                                                  (保留task_struct和PID)

孤儿进程:
父进程 → fork() → 子进程 → 父进程exit() → 子进程被init收养 → 子进程exit() → init wait() → 销毁

7. 信号与 kill -l:操控进程的遥控器

本节解决的问题:什么是信号?如何用信号控制进程?

信号是操作系统通知进程发生某些事件的机制。kill 命令用于发送信号,kill -l 可以列出所有信号名称和编号。

⚠️ 易混淆点:kill 命令不是"杀死"命令!它的全称是"send signal to a process",即向进程发送信号。我们平时用的 kill <PID> 只是发送默认的 SIGTERM(15) 信号,请求进程优雅退出。

7.1 常用信号速查表

信号名 编号 含义 与进程状态的关系
SIGINT 2 中断 (Ctrl+C) 终止进程
SIGQUIT 3 退出 (Ctrl+) 终止并产生 core dump
SIGKILL 9 强制杀死 不可捕获、忽略
SIGTERM 15 终止 (默认 kill) 可捕获,让程序优雅退出
SIGSTOP 19 停止进程 进入 T 状态,不可捕获
SIGCONT 18 继续运行 从 T 恢复到 R/S
SIGCHLD 17 子进程状态改变 父进程可据此回收子进程
SIGTSTP 20 终端停止 (Ctrl+Z) 进入 T 状态,放入后台

7.2 命令示例

bash 复制代码
kill -l            # 列出所有信号
kill -l 9          # 输出 KILL
kill -l SIGSTOP    # 输出 19
kill -9 <PID>      # 强制杀死进程
kill -19 <PID>     # 暂停进程
kill -18 <PID>     # 恢复进程

7.3 动手实验:捕获信号

信号可以被进程捕获和处理(除了 SIGKILL 和 SIGSTOP)。

c 复制代码
#include <stdio.h>
#include <signal.h>
#include <unistd.h>

void sigint_handler(int sig) {
    printf("\n收到SIGINT信号(Ctrl+C),但我不退出!\n");
    printf("请用kill -9 %d来杀死我\n", getpid());
}

int main() {
    // 注册SIGINT信号的处理函数
    signal(SIGINT, sigint_handler);
    printf("进程%d运行中,按Ctrl+C试试\n", getpid());
    while(1) { sleep(1); }
    return 0;
}

运行后按 Ctrl+C,进程不会退出,而是执行我们自定义的处理函数。


8. 存储金字塔:寄存器、Cache、内存、Swap、磁盘

本节解决的问题:不同层次的存储有什么区别?它们和进程有什么关系?

计算机存储体系按速度、容量、价格分层,越靠近 CPU 的层次越快、越小、越贵。

8.1 完整的存储金字塔图

复制代码
          ┌─────────────┐
          │ 寄存器      │  <1ns | 几十字节 | 断电丢失
          └─────────────┘
                 ↓
          ┌─────────────┐
          │ L1/L2/L3 Cache│ 1~10ns | 几MB | 断电丢失
          └─────────────┘
                 ↓
          ┌─────────────┐
          │ 内存(RAM)   │ 50~100ns | 几GB | 断电丢失
          └─────────────┘
                 ↓
          ┌─────────────┐
          │ Swap分区    │ 50µs~10ms | 几GB | 永久保存
          └─────────────┘
                 ↓
          ┌─────────────┐
          │ 磁盘(SSD/HDD)│ 50µs~10ms | 几百GB | 永久保存
          └─────────────┘

8.2 通俗类比:存储体系就像书桌

  • 寄存器:你手里拿着的东西,拿起来就能用,但是只能拿几样
  • Cache:你桌面上的书,伸手就能拿到,能放几十本
  • 内存:你房间里的书架,需要走几步去拿,能放几百本
  • Swap分区:你家的储物间,不常用的东西放进去,需要时再搬到书架
  • 磁盘:楼下的图书馆,需要下楼去借,能放几万本书

程序运行的过程,就是不断把需要的书从图书馆搬到储物间,再搬到书架,再搬到桌面,最后拿到手里的过程。

8.3 什么是 Swap 分区

Swap 分区是磁盘上划分出的一块特殊区域 ,被操作系统用作内存的扩展 。当物理内存不足时,内核会将内存中不常用的内存页(称为"冷页")交换到 Swap 分区,释放出物理内存给更需要的进程使用。

当进程需要访问已经被交换到 Swap 的内存页时,会触发页错误(Page Fault) ,内核会将该页从 Swap 分区重新读回物理内存。这个过程称为"换入 ",反之将内存页写入 Swap 称为"换出"。

8.4 Swap 与进程的关系

  • 影响进程性能:Swap 的速度比物理内存慢 1000 倍以上。如果系统频繁进行 Swap 换入换出,会导致所有进程的响应速度急剧下降,系统变得卡顿。
  • 导致 D 状态 :进程在等待 Swap 换入完成时,会进入不可中断睡眠状态(D 状态),此时进程无法被任何信号打断。
  • OOM 杀手的最后防线:当物理内存和 Swap 都耗尽时,内核会触发 OOM(Out Of Memory)杀手,杀死评分最高的进程以释放内存。

8.5 查看和管理 Swap 分区

bash 复制代码
# 查看 Swap 使用情况
free -h
cat /proc/swaps

# 查看系统 Swap 配置
sysctl vm.swappiness

# 临时关闭 Swap
sudo swapoff -a

# 临时开启 Swap
sudo swapon -a

swappiness 参数:控制内核使用 Swap 的积极性,取值范围 0-100。

  • 0:尽可能不使用 Swap,只有在物理内存完全耗尽时才使用
  • 60:默认值,比较平衡
  • 100:积极使用 Swap,尽可能将不常用的内存页交换出去

最佳实践:

  • 服务器:建议设置 swappiness=10,优先使用物理内存
  • 桌面系统:可以保持默认值 60
  • 数据库服务器:强烈建议设置 swappiness=1 或 0,避免数据库数据被交换到磁盘

8.6 各层存储与进程的关系

  • 寄存器 :进程上下文的核心。进程切换时,所有寄存器值必须保存到该进程的 task_struct.thread 中,下次恢复时再写回。这是上下文切换的核心开销。
  • Cache:利用局部性原理缓存指令和数据,对软件透明。
  • 内存:程序的运行场所。每个进程拥有独立的虚拟地址空间,通过 MMU 映射到物理内存,实现隔离。
  • Swap:内存的扩展,用于存放不常用的内存页。
  • 磁盘:持久化存储,可执行文件、源码、文档存放处。速度比内存慢数千至数万倍。

8.7 数据流动示例:运行 ./hello

  1. 磁盘 → 内存:OS 加载 hello 可执行文件
  2. 内存 → Cache:指令和数据进入高速缓存
  3. Cache → 寄存器:数据加载到 CPU 寄存器
  4. 寄存器 → 运算器:执行计算
  5. 结果写回寄存器 → 内存 → 磁盘
  6. (内存不足时)内存 → Swap:不常用的内存页被交换出去

9. 进程调度队列:CPU 如何分配时间

本节解决的问题:内核如何决定哪个进程先运行?上下文切换到底做了什么?

9.1 就绪队列与等待队列

内核用两个队列管理所有进程:

  • 就绪队列(runqueue):每个 CPU 一个,存放所有 R 状态的进程

  • 等待队列(wait queue):按事件分类,存放 S 和 D 状态的进程

    CPU 0 就绪队列:[进程A][进程B][进程C] → 调度器选择vruntime最小的运行
    CPU 1 就绪队列:[进程D][进程E]

    等待队列(等待键盘输入):[进程F][进程G]
    等待队列(等待磁盘I/O):[进程H]
    等待队列(等待sleep时间到):[进程I]
    等待队列(等待Swap换入):[进程J]

当事件发生(如键盘输入、磁盘 I/O 完成、Swap 换入完成),对应等待队列上的进程被唤醒,移入就绪队列。

9.2 上下文切换的完整过程

当内核决定切换进程时:

  1. 保存当前进程的全部寄存器到其 task_struct
  2. 从就绪队列中选取下一个要运行的进程
  3. 恢复该进程的寄存器值
  4. 切换地址空间(更新页表,刷新 TLB)
  5. 跳转到新进程的下一条指令继续执行

上下文切换的开销 :虽然单次切换只有几微秒,但频繁切换会带来巨大开销。可以用 vmstat 1 命令查看 cs 列(每秒上下文切换次数),一个健康的系统应该在每秒几千次以内。

注意:vmstat 1 输出中的 siso 列分别表示每秒从 Swap 换入和换出的内存大小(KB)。如果这两个值持续大于 0,说明系统内存不足,正在频繁使用 Swap,性能会严重下降。

9.3 CFS 完全公平调度器

Linux 默认采用 CFS(Completely Fair Scheduler)调度器,核心思想是"完全公平"。

CFS 为每个进程维护一个虚拟运行时间 (vruntime)。每次调度时,选择 vruntime 最小的进程运行。

nice 值影响 vruntime 的增长速度:

  • nice 值越低(优先级越高),vruntime 增长越慢
  • nice 值越高(优先级越低),vruntime 增长越快

这样高优先级的进程能获得更多的 CPU 时间。

查看进程调度信息:

bash 复制代码
cat /proc/<PID>/sched

10. 进程间通信:FIFO 命名管道

本节解决的问题:没有亲缘关系的进程如何通信?

进程拥有独立的地址空间,默认情况下无法直接通信。Linux 提供了多种进程间通信(IPC)机制,FIFO 是其中最简单的一种。

10.1 FIFO 原理

FIFO(First In First Out)是一种特殊的文件(类型为 p),本质上是内核中的一个缓冲区。文件系统中的 FIFO 文件只是一个访问入口。

复制代码
进程A(写端) → 打开FIFO文件 → 写入数据 → FIFO内核缓冲区 → 读取数据 → 打开FIFO文件 → 进程B(读端)

与匿名管道不同,FIFO 存在于文件系统中,可被任意两个不相关的进程打开。

10.2 FIFO 特性

  • 半双工:数据单向流动。双向通信需要两个 FIFO
  • 阻塞:读端或写端单独打开时会阻塞,直到另一端也打开

10.3 动手实验:FIFO 通信

创建 FIFO

bash 复制代码
mkfifo mypipe

或在 C 代码中:mkfifo("mypipe", 0666)

终端 1(读端)

bash 复制代码
cat < mypipe      # 阻塞等待写端打开

终端 2(写端)

bash 复制代码
echo "Hello World" > mypipe   # 终端1立即输出 Hello World

10.4 匿名管道 vs 命名管道

特性 匿名管道(pipe) 命名管道(FIFO)
存在形式 内存中,无文件系统入口 文件系统中的特殊文件
通信范围 只能在有亲缘关系的进程间 任意两个进程间
生命周期 随进程结束而销毁 随文件系统存在,除非删除

11. /proc 虚拟文件系统:进程的"档案柜"

本节解决的问题:如何查看和干预正在运行的进程?

/proc 是内存中的伪文件系统,不占用磁盘。内核通过它将进程和系统的内部信息以文件形式暴露出来,是我们查看和控制进程的最重要窗口。

11.1 进程目录中的关键文件

每个进程在 /proc/<PID>/ 下都有以下重要条目:

文件/目录 作用
cmdline 启动进程的完整命令行
environ 环境变量(用 tr '\0' '\n' 格式化)
fd/ 打开的文件描述符目录
status 进程状态、内存使用、PID/PPID、信号等
maps 内存映射详情(堆、栈、共享库、Swap 映射)
limits 资源限制(打开文件数、内存大小等)
oom_score/oom_score_adj OOM 杀手评分
smaps 详细的内存使用情况,包括每个内存段的 Swap 占用

11.2 两个神奇的符号链接

11.2.1 cwd:当前工作目录

/proc/<PID>/cwd 是一个符号链接,指向进程的当前工作目录。进程内的所有相对路径操作都基于此目录。

实用技巧:即使进程正在运行,也可以通过 gdb 临时修改它的工作目录:

bash 复制代码
gdb -p <PID> -ex "call chdir('/new/working/directory')" -ex "detach" -ex "quit"
11.2.2 exe:可执行文件路径

/proc/<PID>/exe 指向启动进程的可执行文件。即使该文件在磁盘上被删除,只要进程仍在运行,就能通过此链接恢复二进制:

bash 复制代码
cp /proc/<PID>/exe /tmp/recovered_bin

11.3 查看进程的 Swap 使用情况

bash 复制代码
# 查看系统整体 Swap 使用
free -h

# 查看每个进程的 Swap 占用(需要 root 权限)
for pid in $(ls /proc | grep '^[0-9]\+$'); do
    swap=$(grep VmSwap /proc/$pid/status 2>/dev/null | awk '{print $2}')
    if [ "$swap" -gt 0 ]; then
        name=$(cat /proc/$pid/comm 2>/dev/null)
        echo "PID: $pid, Name: $name, Swap: ${swap}KB"
    fi
done | sort -k4 -n

11.4 OOM 杀手评分

/proc/<PID>/oom_score 是内核为每个进程计算的"被 OOM 杀手杀死的优先级",分数越高越容易被杀死。

OOM 杀手评分的计算考虑了以下因素:

  • 进程占用的内存大小(包括物理内存和 Swap)
  • 进程的运行时间
  • 进程的 nice 值
  • 是否是 root 进程
  • 是否是内核进程

可以通过修改 /proc/<PID>/oom_score_adj 来调整评分,范围是 -1000 到 1000。设置为 -1000 表示永远不会被 OOM 杀手杀死:

bash 复制代码
echo -1000 > /proc/<PID>/oom_score_adj

11.5 系统全局文件

  • /proc/cpuinfo:CPU 详细信息
  • /proc/meminfo:内存使用概况(包括 Swap 总量和使用量)
  • /proc/loadavg:系统负载
  • /proc/uptime:系统运行时间
  • /proc/version:内核版本
  • /proc/sys/:可动态调整的内核参数(包括 vm.swappiness

11.6 动手实验:用 /proc 追踪进程

c 复制代码
#include <stdio.h>
#include <unistd.h>
int main() {
    printf("我的 PID: %d\n", getpid());
    printf("按回车继续...\n");
    getchar();
    return 0;
}

编译运行后,在按下回车前,从另一个终端执行:

bash 复制代码
# 查看进程基本信息
cat /proc/<PID>/status | grep -E "Name|State|PID|PPID|VmSwap"

# 查看进程打开的文件
ls -l /proc/<PID>/fd

# 查看进程的当前工作目录
ls -l /proc/<PID>/cwd

# 查看进程的可执行文件
ls -l /proc/<PID>/exe

12. 常用进程监控命令实战

本节解决的问题:日常工作中,如何快速查看和管理系统中的进程?

12.1 ps:查看进程快照

bash 复制代码
ps aux          # 查看所有用户的所有进程(BSD风格)
ps -ef          # 查看所有进程(System V风格)
ps axjf         # 查看进程树
ps aux --sort=-%cpu  # 按CPU使用率降序排列
ps aux --sort=-%mem  # 按内存使用率降序排列
ps -u root      # 查看root用户的所有进程

12.2 top/htop:实时监控进程

bash 复制代码
top             # 经典的实时进程监控工具
htop            # 更友好的top替代工具(推荐安装)
top -p 1234,5678  # 只监控指定PID的进程

top 常用快捷键

  • P:按 CPU 使用率排序
  • M:按内存使用率排序
  • T:按运行时间排序
  • k:杀死进程(输入 PID 后回车)
  • q:退出

在 top 输出中,VIRT 列表示进程的虚拟内存大小(包括物理内存、Swap 和共享库),RES 列表示进程占用的物理内存大小,SHR 列表示共享内存大小。

12.3 其他实用命令

bash 复制代码
pidof bash      # 查找名为bash的进程的PID
pgrep -l bash   # 查找名字包含bash的进程及其PID
pstree          # 查看进程树
pkill bash      # 杀死所有名为bash的进程
killall bash    # 杀死所有名为bash的进程
lsof -p <PID>   # 查看进程打开的所有文件
netstat -tulpn | grep <PID>  # 查看进程的网络连接
ss -tulpn | grep <PID>       # 更快速的网络连接查看
vmstat 1        # 实时查看系统资源使用(包括Swap换入换出)
iostat 1        # 实时查看磁盘I/O使用

13. 总结:进程完整生命周期回顾

13.1 进程完整生命周期流程图

复制代码
程序文件(磁盘)
    ↓ 加载到内存
内存
    ↓ fork()创建
进程诞生 → 就绪状态(R) → 运行状态(R)
                ↓ ↑
                睡眠/暂停状态(S/D/T)
                ↓ (内存不足时,部分内存页被交换到Swap)
            进程退出
                ↓
            僵尸状态(Z)
                ↓ 父进程wait()
            进程销毁(X)

13.2 知识链回顾

  1. 硬件基础:冯·诺依曼结构规定程序和数据都在内存中,由 CPU 执行
  2. 操作系统:管理资源,通过时间片轮转让多个程序"同时"运行,形成进程
  3. 进程创建fork() 一次调用两次返回,利用写时复制高效复制地址空间
  4. 进程标识 :PID 和 PPID 定位每个进程,pstree 查看进程家族树
  5. 进程状态:R、S、D、Z、T 等状态反映进程当前在做什么
  6. 回收机制 :僵尸需要父进程 wait(),孤儿由 init 自动收养
  7. 信号控制kill 发送信号可暂停、恢复、终止进程
  8. 存储体系:寄存器、Cache、内存、Swap、磁盘各有分工,Swap 是内存的扩展
  9. 调度原理:就绪队列和等待队列管理进程,CFS 追求公平
  10. 进程间通信:FIFO 命名管道让无亲缘关系的进程也能通信
  11. 信息监控/proc 文件系统提供查看和干预进程的窗口
  12. 实战工具pstopvmstat 等命令是日常运维的利器

13.3 学习建议

学习 Linux 进程管理最好的方法是动手实践。建议你:

  1. 把文中所有的 C 代码都编译运行一遍,观察输出结果
  2. pstop/proc 等工具查看自己运行的进程状态
  3. 尝试制造僵尸进程、孤儿进程,观察它们的状态变化
  4. kill 命令发送不同的信号,观察进程的反应
  5. 故意让系统内存不足,观察 Swap 的使用情况和进程状态变化
  6. 遇到问题时,多查 man 手册(man forkman ps 等)

当你能亲手验证文中的每一个知识点时,你就真正理解了 Linux 进程管理。这将为你后续学习系统编程、运维和性能分析打下坚实的基础。


附录 A:Swap 性能调优最佳实践

A.1 Swap 分区大小建议

物理内存大小 推荐 Swap 大小 启用休眠时推荐
< 2GB 2 × 物理内存 3 × 物理内存
2GB ~ 8GB 等于物理内存 2 × 物理内存
8GB ~ 64GB 4GB ~ 8GB 1.5 × 物理内存
> 64GB 4GB ~ 16GB 不建议启用休眠

注意:现代服务器通常拥有大量内存(32GB+),Swap 主要作为安全冗余,不需要设置太大。

A.2 swappiness 参数调优

bash 复制代码
# 临时设置 swappiness=10
sudo sysctl vm.swappiness=10

# 永久设置(重启后生效)
echo "vm.swappiness=10" | sudo tee -a /etc/sysctl.conf
sudo sysctl -p

不同场景的推荐值:

  • 数据库服务器:1 或 0(避免数据库缓存被交换到磁盘)
  • Web 服务器:10 ~ 30
  • 桌面系统:60(默认值,平衡性能和内存使用)
  • 嵌入式系统:0(通常禁用 Swap)

A.3 创建和配置 Swap 文件

如果没有单独的 Swap 分区,可以创建 Swap 文件:

bash 复制代码
# 创建 4GB 的 Swap 文件
sudo fallocate -l 4G /swapfile

# 设置正确的权限
sudo chmod 600 /swapfile

# 格式化为 Swap
sudo mkswap /swapfile

# 启用 Swap
sudo swapon /swapfile

# 永久启用(添加到 /etc/fstab)
echo "/swapfile none swap sw 0 0" | sudo tee -a /etc/fstab

A.4 Swap 性能优化技巧

  1. 使用 SSD 作为 Swap:SSD 的随机读写性能远高于 HDD,能大幅提升 Swap 性能
  2. 优先使用更快的磁盘分区:将 Swap 分区放在磁盘的开头部分,访问速度更快
  3. 多个 Swap 分区并行:如果有多个磁盘,可以创建多个 Swap 分区,内核会自动并行访问
  4. 避免过度使用 Swap:如果系统 Swap 使用率持续超过 30%,应该考虑增加物理内存

A.5 Swap 问题排查

bash 复制代码
# 查看系统 Swap 使用情况
free -h
cat /proc/swaps

# 查看每个进程的 Swap 占用
sudo smem -t -u

# 查看 Swap 换入换出统计
vmstat 1
sar -W 1

# 查看哪些进程正在等待 Swap I/O(D 状态)
ps aux | awk '$8 ~ /D/ {print $0}'

附录 B:进程管理常用命令速查表

B.1 进程查看命令

命令 功能 常用选项
ps aux 查看所有进程 --sort=-%cpu 按CPU排序 --sort=-%mem 按内存排序
ps axjf 查看进程树 -u <user> 查看指定用户的进程
top 实时监控进程 P 按CPU排序 M 按内存排序 k 杀死进程
htop 增强版 top -p <pid> 只监控指定进程
pstree 查看进程树 -p 显示PID -u 显示用户
pidof <name> 查找进程的 PID -s 只返回一个PID
pgrep <name> 按名称查找进程 -l 显示进程名 -u <user> 按用户查找

B.2 进程控制命令

命令 功能 常用选项
kill <pid> 向进程发送信号 -9 强制杀死 -19 暂停 -18 恢复
killall <name> 杀死所有同名进程 -9 强制杀死
pkill <name> 按名称杀死进程 -u <user> 杀死指定用户的进程
nice <command> 以指定优先级运行程序 -n <value> 设置nice值(-20~19)
renice <value> <pid> 调整已运行进程的优先级 -u <user> 调整指定用户所有进程

B.3 内存和 Swap 查看命令

命令 功能 常用选项
free -h 查看内存和 Swap 使用情况 -m 以MB为单位 -g 以GB为单位
cat /proc/swaps 查看 Swap 分区信息
vmstat 1 实时查看系统资源 si 换入 so 换出 cs 上下文切换
smem 查看进程内存使用 -t 显示总计 -u 按用户统计
cat /proc/<pid>/status 查看进程详细状态 grep VmSwap 查看Swap占用

B.4 系统监控命令

命令 功能 常用选项
uptime 查看系统运行时间和负载
w 查看登录用户和他们的进程
lsof -p <pid> 查看进程打开的文件 -i 查看网络连接
netstat -tulpn 查看网络连接
ss -tulpn 更快速的网络连接查看
iostat 1 查看磁盘 I/O 统计
sar 系统活动报告 -u CPU -r 内存 -b I/O

B.5 /proc 文件系统常用路径

路径 功能
/proc/<pid>/status 进程基本状态
/proc/<pid>/cmdline 进程启动命令
/proc/<pid>/fd/ 进程打开的文件描述符
/proc/<pid>/cwd 进程当前工作目录
/proc/<pid>/exe 进程可执行文件
/proc/<pid>/maps 进程内存映射
/proc/cpuinfo CPU 信息
/proc/meminfo 内存信息
/proc/loadavg 系统负载
/proc/version 内核版本

附录 C:Linux 进程管理常见面试题与答案

C.1 基础概念题

Q1:程序和进程有什么区别?

  • 程序是静态的可执行文件,存储在磁盘上,没有生命周期
  • 进程是程序的一次执行实例,是动态的、有生命周期的实体
  • 同一个程序可以启动多次,产生多个独立的进程

Q2:Linux 进程有哪些基本状态?请简要说明。

  • R:运行中或可运行(在就绪队列等待 CPU)
  • S:可中断睡眠(等待事件,可被信号唤醒)
  • D:不可中断睡眠(通常等待 I/O,信号不能打断)
  • Z:僵尸(进程已退出,父进程尚未回收)
  • T:暂停(收到 SIGSTOP 或 SIGTSTP)
  • t:跟踪停止(被调试器控制)
  • X:死亡(即将销毁,极短暂)

Q3:僵尸进程和孤儿进程有什么区别?它们有什么危害?

  • 僵尸进程 :子进程已退出,但父进程未调用 wait() 回收,保留 task_struct 和 PID
    • 危害:不泄漏内存,但会占用 PID,大量僵尸可能导致 PID 耗尽
  • 孤儿进程 :父进程先于子进程退出,子进程被 init 收养
    • 危害:完全无害,init 会自动回收其资源

Q4:什么是写时复制(Copy-on-Write)?它有什么优点?

  • fork() 后,父子进程最初共享同一块物理内存,内核将内存页标记为只读
  • 只有当其中一方尝试修改某页时,内核才为该页创建副本
  • 优点:让 fork() 速度极快,且大幅节省内存

Q5:什么是 Swap 分区?它有什么作用?

  • Swap 分区是磁盘上的一块特殊区域,用作内存的扩展
  • 当物理内存不足时,内核将不常用的内存页交换到 Swap,释放物理内存
  • 作为 OOM 杀手的最后防线,避免系统因内存耗尽而崩溃

C.2 核心机制题

Q6:为什么 fork() 会返回两次?

  • 不是同一个函数返回了两次,而是 fork() 创建了一个新的进程
  • 现在有两个几乎完全相同的进程在执行同一个代码
  • 在父进程中返回子进程的 PID(>0),在子进程中返回 0

Q7:exec() 函数的作用是什么?它会创建新进程吗?

  • exec() 会替换当前进程的整个地址空间,加载新的可执行文件
  • 从新程序的 main() 开始执行
  • 不会创建新进程,只是改变当前进程的执行内容

Q8:SIGKILL(9) 和 SIGTERM(15) 有什么区别?

  • SIGTERM(15) :默认的 kill 信号,请求进程优雅退出
    • 可以被捕获、忽略或处理
    • 进程有机会清理资源、保存数据后再退出
  • SIGKILL(9) :强制杀死信号
    • 不可捕获、不可忽略,直接由内核终止进程
    • 进程没有机会清理资源,可能导致数据丢失

Q9:Linux 默认的进程调度器是什么?它的工作原理是什么?

  • Linux 默认采用 CFS(完全公平调度器)
  • 为每个进程维护一个虚拟运行时间 (vruntime)
  • 每次调度时,选择 vruntime 最小的进程运行
  • nice 值影响 vruntime 的增长速度:优先级越高,vruntime 增长越慢

Q10:什么是上下文切换?它的开销来自哪里?

  • 上下文切换是指内核从一个进程切换到另一个进程的过程
  • 主要开销来自:
    1. 保存和恢复寄存器状态
    2. 切换地址空间,更新页表和刷新 TLB
    3. 缓存失效,导致后续指令执行变慢

C.3 实践调优题

Q11:如何避免僵尸进程?

  1. 父进程主动调用 wait()waitpid() 等待子进程
  2. 设置 signal(SIGCHLD, SIG_IGN),让内核自动回收子进程
  3. 使用双 fork() 技巧,让孙进程被 init 收养

Q12:如何查看哪个进程占用了最多的 Swap?

bash 复制代码
# 方法1:使用 smem(推荐)
sudo smem -t -u

# 方法2:使用 /proc 文件系统
for pid in $(ls /proc | grep '^[0-9]\+$'); do
    swap=$(grep VmSwap /proc/$pid/status 2>/dev/null | awk '{print $2}')
    if [ "$swap" -gt 0 ]; then
        name=$(cat /proc/$pid/comm 2>/dev/null)
        echo "PID: $pid, Name: $name, Swap: ${swap}KB"
    fi
done | sort -k4 -n

Q13:D 状态进程是什么?为什么不能被 kill -9 杀死?如何解决?

  • D 状态是不可中断睡眠状态,通常进程正在执行内核态的 I/O 操作
  • 内核为了保证数据一致性,不允许在 I/O 过程中中断进程
  • 解决方法:等待 I/O 完成,或者重启系统

Q14:swappiness 参数的作用是什么?不同场景应该如何设置?

  • swappiness 控制内核使用 Swap 的积极性,取值范围 0-100
  • 值越小,越倾向于使用物理内存;值越大,越倾向于使用 Swap
  • 推荐设置:
    • 数据库服务器:1 或 0
    • Web 服务器:10 ~ 30
    • 桌面系统:60(默认)

Q15:OOM 杀手的工作原理是什么?如何保护重要进程不被杀死?

  • 当系统内存(物理内存+Swap)耗尽时,内核触发 OOM 杀手

  • OOM 杀手根据 oom_score 评分杀死进程,分数越高越容易被杀死

  • 评分考虑因素:内存占用、运行时间、nice 值、是否是 root 进程等

  • 保护重要进程:将其 oom_score_adj 设置为 -1000

    bash 复制代码
    echo -1000 > /proc/<PID>/oom_score_adj

C.4 综合题

Q16:请描述 Shell 执行一个命令的完整过程。

  1. Shell 读取用户输入的命令
  2. Shell 调用 fork() 创建一个子进程
  3. 子进程调用 exec() 系列函数加载命令程序
  4. 父进程(Shell)调用 wait() 等待子进程结束
  5. 子进程执行命令,完成后调用 exit() 退出
  6. 父进程回收子进程资源,显示提示符,等待下一个命令

Q17:如何排查系统卡顿问题?请列出你的排查步骤。

  1. 查看系统负载uptime 查看 1/5/15 分钟负载
  2. 查看 CPU 使用tophtop,按 P 排序,查看是否有 CPU 密集型进程
  3. 查看内存使用free -h 查看内存和 Swap 使用情况
  4. 查看 Swap 活动vmstat 1 查看 si 和 so 列,如果持续大于 0,说明内存不足
  5. 查看磁盘 I/Oiostat 1 查看 %util 列,如果接近 100%,说明磁盘瓶颈
  6. 查看上下文切换vmstat 1 查看 cs 列,如果每秒几万次,说明上下文切换过于频繁
  7. 查看 D 状态进程ps aux | awk '$8 ~ /D/ {print $0}',是否有大量不可中断睡眠进程

Q18:/proc 文件系统有什么作用?请列举 5 个常用的 /proc 路径及其功能。

  • /proc 是内存中的伪文件系统,内核通过它暴露系统和进程的内部信息
  • 常用路径:
    1. /proc/cpuinfo:CPU 详细信息
    2. /proc/meminfo:内存使用概况
    3. /proc/<pid>/status:进程基本状态
    4. /proc/<pid>/fd/:进程打开的文件描述符
    5. /proc/loadavg:系统负载
相关推荐
BizViewStudio1 小时前
2026 年 GEO 成为企业线上流量增长核心风口|2026 品牌 GEO 运营指南,6 家全链路优化服务商解析
运维·网络·人工智能·microsoft·ai
不昀1 小时前
VOOHU沃虎:如何选择卡侬自锁RJ45的安装方式?面板安装、PCB安装和穿墙式有何区别?
网络·以太网·电子元器件·rj45
曦月合一2 小时前
在 Linux 服务器上执行这些命令来导入 SSL 证书
linux·服务器·ssl
GensAI2 小时前
2026 电话机器人系统并发量与响应延迟实测,6款产品压力测试对比
网络
sdm0704272 小时前
网络原理-5.NAT技术
服务器·网络·智能路由器
一拳一个娘娘腔2 小时前
CVE-2026-46300 — “Fragnesia“ 深度拆解:当修复补丁亲手唤醒了另一只恶魔
linux·安全
奥莱维2 小时前
RCU改造避坑指南-蓝牙Mesh不拆墙升级
网络
AI科技星2 小时前
《数术工坊:无穷套娃录》 一部用数学套娃写成的“天书小说”
c语言·开发语言·网络·量子计算·agi
sdm0704272 小时前
网络原理-3.网络层&协议IP
网络·网络协议·tcp/ip