本文是一份系统性的 Linux 进程管理入门教程,从最底层的硬件原理讲起,层层递进,直到进程监控与实践。每一节都配有图解、代码示例和动手实验,帮助你从"看懂"到"会用",彻底掌握进程的本质。
🗺️ 学习知识地图
硬件基础(冯·诺依曼) → 操作系统(资源管理) → 进程概念 → 进程创建(fork) → 进程标识(PID/进程树)
↓
进程监控(/proc) ← 进程间通信(FIFO) ← 进程调度(CFS) ← 存储体系 ← 进程状态 ← 进程回收
↓
信号控制
1. 计算机的骨架:冯·诺依曼体系结构
本节解决的问题:为什么要先讲硬件?因为进程本质上是硬件资源的抽象,理解硬件才能真正理解进程。
1945 年,冯·诺依曼提出存储程序思想:将程序指令和数据统一存放在存储器中,由控制器逐条取出执行。这一思想奠定了所有现代计算机的基础。
1.1 五大核心部件
现代计算机由五大部件组成:
- 运算器:执行算术和逻辑运算(CPU 中的 ALU)
- 控制器:控制取指令、译码、执行、响应中断(与运算器合成 CPU)
- 存储器:即内存(RAM),存放正在运行的程序和数据
- 输入设备:键盘、鼠标、网卡(接收数据)、摄像头等
- 输出设备:显示器、打印机、网卡(发送数据)、扬声器等
1.2 冯·诺依曼结构的核心数据流
[输入设备] → 数据 → [内存] ← 指令 ← [磁盘(程序文件)]
↑↓
[CPU(运算器+控制器)]
↑↓
[输出设备]
1.3 为什么这个结构决定了进程的本质
冯·诺依曼结构的核心是"存储程序,顺序执行"。这意味着三个铁律:
- 任何程序必须先加载到内存才能运行
- CPU 只能从内存取指令,不能直接从磁盘执行
- 所有设备的操作都必须通过 CPU 协调
这三点直接定义了进程:进程就是加载到内存中、正在被 CPU 执行的程序实例。
注意:许多设备兼具输入输出功能,最典型的是网卡。没有 CPU 的参与,设备之间无法直接通信。当你在键盘上按键,键盘控制器会向 CPU 发出中断,CPU 暂停当前任务,执行内核的中断处理程序读取按键并分发给应用程序。
2. 操作系统:硬件的大管家(先描述,再组织)
本节解决的问题:操作系统到底做了什么?为什么会有"进程"这个概念?
操作系统是运行在硬件之上的第一层软件,负责管理 CPU、内存、磁盘、I/O 设备,并向应用程序提供统一的系统调用 (如 read、write、fork)。
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 aux 或 ps 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 中显示为 Z 或 defunct。
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 耗尽,无法创建新进程。
避免僵尸的三种方法:
- 父进程主动调用
wait()或waitpid()等待子进程 - 设置信号处理:
signal(SIGCHLD, SIG_IGN),让内核自动回收子进程 - 使用双
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
- 磁盘 → 内存:OS 加载
hello可执行文件 - 内存 → Cache:指令和数据进入高速缓存
- Cache → 寄存器:数据加载到 CPU 寄存器
- 寄存器 → 运算器:执行计算
- 结果写回寄存器 → 内存 → 磁盘
- (内存不足时)内存 → 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 上下文切换的完整过程
当内核决定切换进程时:
- 保存当前进程的全部寄存器到其
task_struct - 从就绪队列中选取下一个要运行的进程
- 恢复该进程的寄存器值
- 切换地址空间(更新页表,刷新 TLB)
- 跳转到新进程的下一条指令继续执行
上下文切换的开销 :虽然单次切换只有几微秒,但频繁切换会带来巨大开销。可以用 vmstat 1 命令查看 cs 列(每秒上下文切换次数),一个健康的系统应该在每秒几千次以内。
注意:
vmstat 1输出中的si和so列分别表示每秒从 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 知识链回顾
- 硬件基础:冯·诺依曼结构规定程序和数据都在内存中,由 CPU 执行
- 操作系统:管理资源,通过时间片轮转让多个程序"同时"运行,形成进程
- 进程创建 :
fork()一次调用两次返回,利用写时复制高效复制地址空间 - 进程标识 :PID 和 PPID 定位每个进程,
pstree查看进程家族树 - 进程状态:R、S、D、Z、T 等状态反映进程当前在做什么
- 回收机制 :僵尸需要父进程
wait(),孤儿由 init 自动收养 - 信号控制 :
kill发送信号可暂停、恢复、终止进程 - 存储体系:寄存器、Cache、内存、Swap、磁盘各有分工,Swap 是内存的扩展
- 调度原理:就绪队列和等待队列管理进程,CFS 追求公平
- 进程间通信:FIFO 命名管道让无亲缘关系的进程也能通信
- 信息监控 :
/proc文件系统提供查看和干预进程的窗口 - 实战工具 :
ps、top、vmstat等命令是日常运维的利器
13.3 学习建议
学习 Linux 进程管理最好的方法是动手实践。建议你:
- 把文中所有的 C 代码都编译运行一遍,观察输出结果
- 用
ps、top、/proc等工具查看自己运行的进程状态 - 尝试制造僵尸进程、孤儿进程,观察它们的状态变化
- 用
kill命令发送不同的信号,观察进程的反应 - 故意让系统内存不足,观察 Swap 的使用情况和进程状态变化
- 遇到问题时,多查
man手册(man fork、man 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 性能优化技巧
- 使用 SSD 作为 Swap:SSD 的随机读写性能远高于 HDD,能大幅提升 Swap 性能
- 优先使用更快的磁盘分区:将 Swap 分区放在磁盘的开头部分,访问速度更快
- 多个 Swap 分区并行:如果有多个磁盘,可以创建多个 Swap 分区,内核会自动并行访问
- 避免过度使用 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:什么是上下文切换?它的开销来自哪里?
- 上下文切换是指内核从一个进程切换到另一个进程的过程
- 主要开销来自:
- 保存和恢复寄存器状态
- 切换地址空间,更新页表和刷新 TLB
- 缓存失效,导致后续指令执行变慢
C.3 实践调优题
Q11:如何避免僵尸进程?
- 父进程主动调用
wait()或waitpid()等待子进程 - 设置
signal(SIGCHLD, SIG_IGN),让内核自动回收子进程 - 使用双
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设置为 -1000bashecho -1000 > /proc/<PID>/oom_score_adj
C.4 综合题
Q16:请描述 Shell 执行一个命令的完整过程。
- Shell 读取用户输入的命令
- Shell 调用
fork()创建一个子进程 - 子进程调用
exec()系列函数加载命令程序 - 父进程(Shell)调用
wait()等待子进程结束 - 子进程执行命令,完成后调用
exit()退出 - 父进程回收子进程资源,显示提示符,等待下一个命令
Q17:如何排查系统卡顿问题?请列出你的排查步骤。
- 查看系统负载 :
uptime查看 1/5/15 分钟负载 - 查看 CPU 使用 :
top或htop,按 P 排序,查看是否有 CPU 密集型进程 - 查看内存使用 :
free -h查看内存和 Swap 使用情况 - 查看 Swap 活动 :
vmstat 1查看 si 和 so 列,如果持续大于 0,说明内存不足 - 查看磁盘 I/O :
iostat 1查看 %util 列,如果接近 100%,说明磁盘瓶颈 - 查看上下文切换 :
vmstat 1查看 cs 列,如果每秒几万次,说明上下文切换过于频繁 - 查看 D 状态进程 :
ps aux | awk '$8 ~ /D/ {print $0}',是否有大量不可中断睡眠进程
Q18:/proc 文件系统有什么作用?请列举 5 个常用的 /proc 路径及其功能。
/proc是内存中的伪文件系统,内核通过它暴露系统和进程的内部信息- 常用路径:
/proc/cpuinfo:CPU 详细信息/proc/meminfo:内存使用概况/proc/<pid>/status:进程基本状态/proc/<pid>/fd/:进程打开的文件描述符/proc/loadavg:系统负载