graph TB
subgraph "进程三态"
R["🟢 就绪态<br>Ready"]
B["🟡 阻塞态<br>Blocked"]
E["🔵 执行态<br>Running"]
end
R -->|"调度"| E
E -->|"时间片用完"| R
E -->|"等待IO/事件"| B
B -->|"事件就绪"| R
style R fill:#B5EAD7,stroke:#80CBC4,color:#333
style B fill:#FFF9C4,stroke:#F9A825,color:#333
style E fill:#C7CEEA,stroke:#9FA8DA,color:#333
// Linux 内核中的进程描述符 task_struct(简化) structtask_struct { pid_t pid; // 进程 ID pid_t tgid; // 线程组 ID structmm_struct *mm;// 内存描述符(页表等) structfiles_struct *files;// 打开的文件描述符表 structsignal_struct *sig;// 信号处理信息 structthread_structthread;// CPU 上下文(寄存器、栈指针) structcred *cred;// 凭证(UID、GID) // ... 还有调度信息、父子关系、文件系统信息等 };
fork 的写时复制(COW)
graph LR
A["父进程<br>虚拟页 P1"] -->|"fork()"| B["子进程<br>虚拟页 P1'"]
A -.->|"共享同一物理页<br>(只读)"| C["⚙️ 物理页<br>(标记为只读)"]
B -.->|"共享同一物理页"| C
B -->|"任一方写入"| D["⚡ 触发缺页异常<br>复制新物理页"]
A -->|"任一方写入"| D
D --> E["✅ 各自独立物理页"]
style A fill:#C7CEEA,stroke:#9FA8DA,color:#333
style B fill:#E8D5F5,stroke:#CE93D8,color:#333
style C fill:#FFDAB9,stroke:#FFAB76,color:#333
style D fill:#FFB3C6,stroke:#F48FB1,color:#333
style E fill:#B5EAD7,stroke:#80CBC4,color:#333
graph TB
A["🚀 父进程创建子进程"]
A -->|fork| B["子进程"]
B -->|正常 exit| C["父进程 wait"]
C --> D["✅ 正常回收"]
B -->|父进程先 exit| E["🔴 孤儿进程<br>被 init 收养"]
E -->|init 负责 wait| D
B -->|子 exit 父不 wait| F["⚠️ 僵尸进程<br>PCB 残留"]
F -->|父 wait| D
B -->|setsid + 脱离终端| G["🟣 守护进程<br>后台常驻"]
style A fill:#C7CEEA,stroke:#9FA8DA,color:#333
style B fill:#E8D5F5,stroke:#CE93D8,color:#333
style C fill:#B5EAD7,stroke:#80CBC4,color:#333
style D fill:#B5EAD7,stroke:#80CBC4,color:#333
style E fill:#FFB3C6,stroke:#F48FB1,color:#333
style F fill:#FFB3C6,stroke:#F48FB1,color:#333
style G fill:#FFF9C4,stroke:#F9A825,color:#333
graph LR
A["🔵 死锁处理"]
A --> B["🦆 鸵鸟策略<br>忽略不管"]
A --> C["🛡️ 死锁预防<br>打破四条件之一"]
A --> D["🧮 死锁避免<br>银行家算法"]
A --> E["🔍 死锁检测<br>资源分配图"]
A --> F["💥 死锁恢复<br>杀进程/回滚"]
style A fill:#C7CEEA,stroke:#9FA8DA,color:#333
style B fill:#FFB3C6,stroke:#F48FB1,color:#333
style C fill:#B5EAD7,stroke:#80CBC4,color:#333
style D fill:#E8D5F5,stroke:#CE93D8,color:#333
style E fill:#FFDAB9,stroke:#FFAB76,color:#333
style F fill:#FFF9C4,stroke:#F9A825,color:#333
// 找一个 need <= work 且未完成的进程 for (int count = 0; count < N; count++) { bool found = false; for (int i = 0; i < N; i++) { if (!finish[i]) { bool can = true; for (int j = 0; j < M; j++) if (s->need[i][j] > work[j]) { can = false; break; } if (can) { for (int j = 0; j < M; j++) work[j] += s->allocation[i][j]; finish[i] = true; found = true; } } } if (!found) return0; // 不安全 } return1; // 安全 }
五、线程同步:四件套
5.1 线程同步 4 大原语
原语
头文件
用途
特性
互斥锁 mutex
pthread_mutex.h
临界区互斥
加锁/解锁
读写锁 rwlock
pthread_rwlock.h
读多写少
读共享、写独占
条件变量 condvar
pthread.h
线程间等待唤醒
必须配合 mutex
信号量 semaphore
semaphore.h
资源计数
P/V 操作
5.2 互斥锁 mutex
1 2 3 4 5 6 7 8 9 10 11 12 13
#include<pthread.h>
pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER; int counter = 0;
void* increment(void* arg) { for (int i = 0; i < 100000; i++) { pthread_mutex_lock(&mtx); counter++; pthread_mutex_unlock(&mtx); } returnNULL; }
sequenceDiagram
participant App as 👤 应用程序
participant K as ⚙️ 内核
participant D as 🖥️ 设备/网卡
Note over App,K: 阶段1: 等待数据
App->>K: 调用 read()
K->>D: 等待网卡数据
D-->>K: 数据到达,复制到内核缓冲区
Note over App,K: 阶段2: 数据复制
K->>App: 复制到用户缓冲区
App-->>App: 处理数据
关键点:IO 模型的区别在于这两个阶段如何处理——是阻塞、非阻塞、还是异步通知。
9.2 五种 IO 模型对比
模型
阶段1 等待数据
阶段2 复制数据
编程复杂度
适用
阻塞 IO
阻塞
阻塞
简单
小并发
非阻塞 IO
轮询
阻塞
中等
罕见
IO 复用
阻塞(select/epoll)
阻塞
中等
高并发服务器
信号驱动 IO
信号
阻塞
复杂
实时信号
异步 IO
不阻塞
不阻塞
复杂(AIO/proactor)
高性能服务
graph TB
A["💎 同步 IO"]
A --> B["阻塞 IO<br>最简单"]
A --> C["非阻塞 IO<br>轮询"]
A --> D["IO 复用<br>select/poll/epoll"]
A --> E["信号驱动 IO<br>SIGIO"]
F["🟢 异步 IO (AIO)"]
style A fill:#C7CEEA,stroke:#9FA8DA,color:#333
style B fill:#B5EAD7,stroke:#80CBC4,color:#333
style C fill:#FFDAB9,stroke:#FFAB76,color:#333
style D fill:#E8D5F5,stroke:#CE93D8,color:#333
style E fill:#FFF9C4,stroke:#F9A825,color:#333
style F fill:#FFB3C6,stroke:#F48FB1,color:#333
9.3 阻塞 IO(最简单)
1 2 3
// 一请求一线程 int n = read(fd, buf, sizeof(buf)); // 阻塞直到有数据 printf("read %d bytes\n", n);
structepoll_eventevents[10]; int n = epoll_wait(epfd, events, 10, -1); // 阻塞等待 for (int i = 0; i < n; i++) { if (events[i].data.fd == 0) { // stdin 可读 char buf[128]; read(0, buf, sizeof(buf)); } } return0; }
10.5 epoll 三个核心函数
函数
作用
时间复杂度
epoll_create1(0)
创建 epoll 实例,返回 fd
O(1)
epoll_ctl(epfd, op, fd, &ev)
注册/修改/删除 fd
O(log n) 红黑树
epoll_wait(epfd, events, max, timeout)
等待就绪事件
O(1) 就绪事件数
10.6 epoll 高性能原理
graph LR
A["epoll_create"] --> B["🔴 红黑树<br>管理所有 fd"]
C["epoll_ctl ADD"] --> B
B -.->|"每个 fd 注册<br>callback"| D["⚙️ 内核<br>eventpoll"]
E["网卡/磁盘数据到达"] -->|"触发 callback"| D
D -->|"加入就绪链表"| F["🟢 就绪链表<br>rdlist"]
G["epoll_wait"] -->|"返回就绪 fd"| F
F --> G
style A fill:#C7CEEA,stroke:#9FA8DA,color:#333
style B fill:#FFB3C6,stroke:#F48FB1,color:#333
style C fill:#C7CEEA,stroke:#9FA8DA,color:#333
style D fill:#E8D5F5,stroke:#CE93D8,color:#333
style E fill:#FFDAB9,stroke:#FFAB76,color:#333
style F fill:#B5EAD7,stroke:#80CBC4,color:#333
style G fill:#C7CEEA,stroke:#9FA8DA,color:#333
// 3. 事件循环 structepoll_eventevents[MAX_EVENTS]; while (1) { int n = epoll_wait(epfd, events, MAX_EVENTS, -1); for (int i = 0; i < n; i++) { if (events[i].data.fd == listen_fd) { // 新连接 int client_fd = accept(listen_fd, NULL, NULL); ev.events = EPOLLIN; // 默认 LT ev.data.fd = client_fd; epoll_ctl(epfd, EPOLL_CTL_ADD, client_fd, &ev); } else { // 客户端数据 int fd = events[i].data.fd; char buf[BUF_SIZE]; int len = read(fd, buf, BUF_SIZE); if (len <= 0) { close(fd); epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL); } else { write(fd, buf, len); // echo } } } } return0; }
10.10 三种 IO 复用函数的演进
graph LR
A["1983 BSD select<br>bitmap 1024"]
B["1997 System V poll<br>链表无限制"]
C["2002 Linux 2.5.44 epoll<br>红黑树 + 回调"]
A -->|"改进 fd 限制"| B
B -->|"改进轮询 O n"| C
style A fill:#FFB3C6,stroke:#F48FB1,color:#333
style B fill:#FFDAB9,stroke:#FFAB76,color:#333
style C fill:#B5EAD7,stroke:#80CBC4,color:#333
graph LR
A["磁盘"] -->|"DMA 拷贝"| B["⚙️ 内核页缓存"]
B -.->|"mmap 共享<br>无 CPU 拷贝"| C["👤 用户缓冲区<br>(mmap 映射)"]
C -->|"CPU 拷贝"| D["📡 Socket 缓冲区"]
D -->|"DMA 拷贝"| E["🌐 网卡"]
style A fill:#FFB3C6,stroke:#F48FB1,color:#333
style B fill:#FFDAB9,stroke:#FFAB76,color:#333
style C fill:#C7CEEA,stroke:#9FA8DA,color:#333
style D fill:#E8D5F5,stroke:#CE93D8,color:#333
style E fill:#B5EAD7,stroke:#80CBC4,color:#333
11.4 sendfile 零拷贝(最常用)
1 2 3 4 5 6 7 8 9 10
#include<sys/sendfile.h>
int in_fd = open("file.txt", O_RDONLY); int out_fd = accept(server_fd, NULL, NULL);
graph LR
A["磁盘"] -->|"DMA 拷贝"| B["⚙️ 内核页缓存"]
B -->|"CPU 拷贝<br>sendfile 内核完成"| C["📡 Socket 缓冲区"]
C -->|"DMA 拷贝"| D["🌐 网卡"]
style A fill:#FFB3C6,stroke:#F48FB1,color:#333
style B fill:#FFDAB9,stroke:#FFAB76,color:#333
style C fill:#E8D5F5,stroke:#CE93D8,color:#333
style D fill:#B5EAD7,stroke:#80CBC4,color:#333
高级版:Linux 2.4+ 支持 DMA gather,连内核→Socket 的 CPU 拷贝都省了。
graph LR
A["栈溢出问题"] --> B["🟢 方法1<br>改为循环"]
A --> C["🟢 方法2<br>尾递归优化"]
A --> D["🟢 方法3<br>手动栈/堆"]
A --> E["🟢 方法4<br>增加栈大小"]
style A fill:#FFB3C6,stroke:#F48FB1,color:#333
style B fill:#B5EAD7,stroke:#80CBC4,color:#333
style C fill:#B5EAD7,stroke:#80CBC4,color:#333
style D fill:#B5EAD7,stroke:#80CBC4,color:#333
style E fill:#B5EAD7,stroke:#80CBC4,color:#333
方法 1:循环替代递归
1 2 3 4 5 6 7 8 9 10 11 12 13
// 递归版 intfactorial(int n) { if (n <= 1) return1; return n * factorial(n-1); }
// 循环版(无栈溢出风险) intfactorial_loop(int n) { int result = 1; for (int i = 2; i <= n; i++) result *= i; return result; }
方法 2:尾递归优化(编译器自动)
1 2 3 4 5 6
// 尾递归:递归调用是最后一步操作 intfactorial_tail(int n, int acc) { if (n <= 1) return acc; return factorial_tail(n - 1, n * acc); // 编译器可优化为循环 } // GCC 开启:-O2 即可
方法 3:手动维护栈
1 2 3 4 5 6 7 8 9 10 11 12
#include<vector>
intfactorial_manual(int n) { std::vector<int> stack; for (int i = 2; i <= n; i++) stack.push_back(i); int result = 1; while (!stack.empty()) { result *= stack.back(); stack.pop_back(); } return result; }
while (1) { structsockaddr_inclient_addr; socklen_t len = sizeof(client_addr); int client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &len);
Go 的方案:M:N 调度(GPM 模型),将大量 goroutine 映射到少量 OS 线程,自动调度。
17.2 实战行动建议
你的角色
行动建议
后端开发
深入 epoll 源码 + Redis/Netty 实现,写一个 epoll echo server
系统工程师
研读 Linux 内核 eventpoll.c,理解红黑树和就绪链表
面试准备
默写 7 种 IPC、5 种 IO 模型、死锁 4 条件、epoll LT/ET 区别
架构设计
用”进程-线程-协程”分层:进程做隔离,线程做并行,协程做高并发
17.3 进阶阅读路线
graph LR
A["📖 本篇基础概念"] --> B["📚 Linux 高性能服务器编程<br>游双"]
B --> C["🔧 Linux 内核设计与实现<br>Robert Love"]
C --> D["🚀 深入理解 Linux 内核<br>Bovet"]
D --> E["🧠 Unix 网络编程卷 1<br>Stevens"]
E --> F["🏆 C++ Concurrency in Action<br>Anthony Williams"]
style A fill:#C7CEEA,stroke:#9FA8DA,color:#333
style B fill:#B5EAD7,stroke:#80CBC4,color:#333
style C fill:#B5EAD7,stroke:#80CBC4,color:#333
style D fill:#E8D5F5,stroke:#CE93D8,color:#333
style E fill:#E8D5F5,stroke:#CE93D8,color:#333
style F fill:#FFB3C6,stroke:#F48FB1,color:#333
十八、面试题速查表
18.1 进程/线程/协程速查
题目
核心答案(一句话)
进程创建过程?
fork 复制 PCB + exec 替换映像
子进程与父进程通信?
pipe、signal、waitpid
进程与作业区别?
进程是动态执行,作业是静态任务
死锁必要条件?
互斥、持有并等待、不可剥夺、循环等待
IPC 方式?
管道/FIFO/消息队列/共享内存/信号量/信号/Socket
线程同步方式?
mutex、rwlock、condvar、semaphore
页和段的区别?
页固定大小等分,段变长按逻辑
孤儿 vs 僵尸?
孤儿被 init 收养,僵尸是 exit 后未 wait
守护进程?
脱离终端后台运行,二次 fork + setsid
线程 vs 进程?
线程共享地址空间,进程独立
多进程 vs 多线程?
进程隔离强开销大,线程轻量易通信
协程?
用户态线程,调度由程序控制
递归锁?
同一线程可多次加锁,增加引用计数
用户态→内核态?
系统调用、异常、中断
中断实现?
保护现场→执行 handler→恢复现场
函数调用 vs 系统调用?
用户态 vs 切内核态
虚拟内存优点?
隔离、扩展、保护、共享
线程安全?
多个线程并发执行结果一致
5 种 IO 模型?
阻塞、非阻塞、IO 复用、信号驱动、异步
异步 IO 缺点?
编程复杂、Linux 对 socket 支持弱
IO 复用原理?
一次等待多个 fd 就绪,避免单 fd 阻塞
零拷贝?
mmap、sendfile、splice 减少 CPU 拷贝
epoll LT vs ET?
LT 多次通知,ET 只通知一次
递归原理?
每次调用压栈,栈帧保存状态
栈溢出?
改循环、尾递归、手动栈、增栈大小
18.2 7 种 IPC 速查
方式
关键字
管道
pipe(),亲缘,半双工
FIFO
mkfifo,文件系统路径名
消息队列
msgget/msgsnd/msgrcv,结构化消息
共享内存
shmget/shmat,最快
信号量
sem_init,PV 同步
信号
kill/signal,异步通知
Socket
socket/bind/listen,跨主机
18.3 5 种 IO 模型速查
模型
阶段1
阶段2
阻塞
阻塞
阻塞
非阻塞
轮询
阻塞
IO 复用
select/epoll 阻塞
阻塞
信号驱动
信号
阻塞
异步
都不阻塞
都不阻塞
18.4 死锁 4 条件速查
条件
一句话
打破方法
互斥
一资源一时刻一进程
共享只读
持有并等待
握着旧资源等新资源
一次性申请
不可剥夺
资源不能强抢
申请不到释放已有
循环等待
进程-资源形成环
资源编号按序申请
系列导航
本系列共 16 篇,覆盖 C++ 面试全栈知识点:
篇数
主题
链接
第 1 篇
C++ 基础:指针、引用、const、static
[文章]
第 2 篇
面向对象:封装、继承、多态、虚函数
[文章]
第 3 篇
模板与泛型:函数模板、类模板、SFINAE
[文章]
第 4 篇
STL 源码:vector、list、map、unordered_map
[文章]
第 5 篇
内存管理:new/delete、malloc/free、智能指针
[文章]
第 6 篇
关键字:const、volatile、explicit、mutable
[文章]
第 7 篇
类型转换:static_cast、dynamic_cast、reinterpret_cast
[文章]
第 8 篇
异常处理:try/catch、noexcept、栈展开
[文章]
第 9 篇
C++11/14/17 新特性:lambda、右值引用、智能指针
[文章]
第 10 篇
C++20/23 新特性:concept、coroutine、module
[文章]
第 11 篇
网络编程:TCP/IP、socket、HTTP 协议
[文章]
第 12 篇
设计模式:单例、工厂、观察者、策略
[文章]
第 13 篇
进程/线程/IO:fork、IPC、epoll、零拷贝
本篇
第 14 篇
数据库与存储:MySQL 索引、事务、Redis 数据结构
[文章]
第 15 篇
分布式基础:CAP、BASE、共识算法
[文章]
第 16 篇
系统设计:短链、Feed、秒杀、限流
[文章]
结尾金句:并发编程的精髓不是”用多线程”,而是”让正确的执行单元在正确的时机访问正确的资源“。理解 fork/exec 的复制语义、epoll 的事件驱动、协程的用户态调度,你才真正掌握了 C++ 后端工程师的”九阳真经”。