Linux驱动 2026年5月1日 37 分钟

同步、异步、阻塞、非阻塞详解

同步、异步、阻塞、非阻塞详解 第一部分:四个概念的本质区别与联系 一、两个维度 维度 关注的问题 概念 消息通知机制 结…

同步、异步、阻塞、非阻塞详解

第一部分:四个概念的本质区别与联系

一、两个维度

维度关注的问题概念
消息通知机制结果怎么拿到?同步vs 异步
等待时的状态等待时线程干嘛?阻塞 vs 非阻塞

二、逐个拆解

1. 同步 (Synchronous)

调用者主动等待/轮询结果。

调用者 --调用--> 被调用者
调用者 <--返回-- 被调用者   // 调用者一直等到结果返回

核心:结果由调用者自己去获取,在结果返回前,控制流不往下走。

2. 异步 (Asynchronous)

调用者发出请求后立即返回,结果通过回调/通知/事件送达。

调用者 --调用--> 被调用者// 立刻返回,不等结果
...调用者做其他事...
被调用者 --通知/回调--> 调用者// 结果好了主动通知

核心:结果由被调用者主动推送给调用者

3. 阻塞 (Blocking)

线程被挂起,让出 CPU,直到条件满足才被唤醒。

线程: 发起操作 →挂起(sleep) → ... → 被唤醒 → 继续执行

核心:线程不占用 CPU,操作系统层面被挂起。

4. 非阻塞 (Non-blocking)

调用立即返回(可能返回”未就绪”),线程不会被挂起。

线程: 发起操作 → 立即返回(可能是EAGAIN) → 可以做别的事/再次尝试

核心:线程始终保持运行态,不会被内核挂起。


三、组合关系(关键)

这两个维度是正交的,可以自由组合,共4 种:

1) 同步 + 阻塞(最常见)

// 传统 read(),线程卡住直到数据到达
data = read(fd, buf, size);  // 线程挂起,等数据来了才返回

你去餐厅柜台点餐,站在柜台前干等,饭做好了递给你。

2) 同步 + 非阻塞(轮询模型)

// 非阻塞 read,没数据就返回 EAGAIN,调用者反复尝试
while ((n = read(fd, buf, size)) == -1 && errno == EAGAIN) {
   //忙等或做点别的,然后再试
}

你去柜台点餐,每隔几秒去问一次”好了没”,没好就走开一会。

3) 异步 + 阻塞(较少见,如 select/epoll)

// select 本身是阻塞的,但它监听的I/O 模型是"就绪通知"式的
select(nfds, &readfds, NULL, NULL, NULL);  // 阻塞等待事件通知
// 事件到达后再去读

你坐在大厅等叫号(阻塞等通知),叫到你的号再去取餐。

4) 异步 + 非阻塞(真正的异步 I/O)

// Linux AIO / Windows IOCP / Node.js
aio_read(&aiocb);  // 立刻返回,内核完成后触发回调
// 线程继续做别的事

你点完餐就走了去逛街(不等待),饭好了服务员打电话通知你


四、一张图总结

                    ┌──────────────────────────────────────┐
│ 结果如何获取? │
│ 同步(自己取) 异步(对方送) │
┌───────────┼──────────────────┬───────────────────┤
等 │ 阻塞 │ 传统 read() │ select/epoll │
待 │ (挂起) │ 最简单最直觉 │ I/O 多路复用 │
时 ├───────────┼──────────────────┼───────────────────┤
状 │ 非阻塞 │ 轮询 read() │ AIO / IOCP │
态 │ (不挂起) │ CPU 空转浪费 │ 性能最优 │
└───────────┴──────────────────┴───────────────────┘

五、本质区别一句话

概念一句话
同步 vs 异步谁来推动结果的传递——调用者拉取 vs 被调用者推送
阻塞 vs 非阻塞等待期间线程的状态——挂起让出CPU vs 保持运行

第二部分:从数据移动两阶段理解 I/O 模型

I/O 的本质就是数据搬运,以网络读取 (recv) 为例,数据要经历两个阶段:

┌──────────┐         ┌──────────┐         ┌──────────┐
│ 网卡/磁盘 │ ──①──> │ 内核缓冲区 │ ──②──> │ 用户缓冲区 │
│ (设备) │ │ (kernel) │ │(user space)│
└──────────┘ └──────────┘ └──────────┘
阶段一:数据到达设备, 阶段二:内核将数据
DMA 拷贝到内核缓冲区 拷贝到用户进程空间

所有 I/O 模型的差异,就在于线程在这两个阶段分别处于什么状态。


一、五种 I/O 模型逐一拆解

1. 同步阻塞 I/O(Blocking I/O)

用户进程         内核
│ │
│── recvfrom() ─>│
│ (系统调用) │
│ │ 阶段一:数据还没到网卡
│ 【进程挂起】 │ ......等待设备数据......
│ 【进程挂起】 │ ......DMA 拷贝到内核缓冲区......
│ │
│ │ 阶段二:内核→用户空间拷贝
│ 【进程仍挂起】 │ ......copy_to_user()......
│ │
│<── 返回数据 ────│
│ 【进程唤醒】 │
▼ ▼

两个阶段都阻塞:

  • 阶段一:数据没到内核,进程 sleep,让出 CPU
  • 阶段二:内核拷贝数据到用户空间,进程继续 sleep
  • 直到拷贝完成才唤醒进程

最简单,但线程被完全卡住,什么也干不了。


2. 同步非阻塞 I/O(Non-blocking I/O)

用户进程              内核
│ │
│── recvfrom() ────> │ 数据没到
│<── EAGAIN ─────────│ 立即返回
│ │
│── recvfrom() ────> │ 数据没到
│<── EAGAIN ─────────│ 立即返回
│ │
│ (反复轮询...) │
│ │
│── recvfrom() ────> │ 阶段一完成!数据已在内核缓冲区
│ │
│ 【进程阻塞】 │ 阶段二:内核→用户空间拷贝
│ │ ......copy_to_user()......
│<── 返回数据 ───────│
│ 【进程唤醒】 │
▼ ▼

阶段一不阻塞,阶段二仍阻塞:

  • 阶段一:内核没有数据就立即返回 EAGAIN,进程不挂起,但需要反复轮询
  • 阶段二:一旦数据到达内核,copy_to_user() 期间进程仍被阻塞

阶段一解放了线程,但轮询浪费 CPU;阶段二本质没变。


3. I/O 多路复用(select / poll / epoll)

用户进程                  内核
│ │
│── select(fds) ───────> │
│ │ 阶段一:等待任意一个fd数据就绪
│ 【进程阻塞在select】 │ ......监控多个fd......
│ │ fd3 数据到达内核缓冲区!
│<── select 返回就绪fd ──│
│ │
│── recvfrom(fd3) ─────> │
│ 【进程阻塞】 │ 阶段二:内核→用户空间拷贝
│ │ ......copy_to_user()......
│<── 返回数据 ───────────│
▼ ▼

两个阶段都阻塞,但阶段一的阻塞方式不同:

  • 阶段一:进程阻塞在 select/epoll 上,而非阻塞在具体的 I/O 操作上。好处是一个线程可以同时等待成千上万个 fd
  • 阶段二:数据就绪后调用 recvfromcopy_to_user() 期间仍阻塞

本质仍是同步的(数据拷贝时进程阻塞),但阶段一实现了”一对多”监听。


4. 信号驱动 I/O(Signal-driven I/O)

用户进程                     内核
│ │
│── sigaction(SIGIO) ─────> │ 注册信号处理函数
│<── 立即返回 ──────────────│
│ │
│ 【进程不阻塞,做其他事】 │ 阶段一:等待数据到达
│ │ ......设备→内核缓冲区......
│ │
│<── SIGIO 信号 ────────────│ 数据到达内核!发信号通知
│ │
│── recvfrom() ───────────> │
│ 【进程阻塞】 │ 阶段二:内核→用户空间拷贝
│ │ ......copy_to_user()......
│<── 返回数据 ──────────────│
▼ ▼

阶段一不阻塞且不轮询,阶段二仍阻塞:

  • 阶段一:注册信号后立即返回,内核数据就绪后主动发信号通知——这是异步通知
  • 阶段二:收到信号后调用 recvfromcopy_to_user() 期间依然阻塞

阶段一是异步的(内核主动通知),但阶段二仍是同步的,所以整体仍归为同步 I/O。


5. 异步 I/O(AIO / IOCP)—— 真正的异步

用户进程                        内核
│ │
│── aio_read(buf, callback) ─> │ 告诉内核:数据准备好直接放到buf
│<── 立即返回 ─────────────────│
│ │
│ 【进程完全不阻塞】 │ 阶段一:等待数据到达
│ 【继续做任何其他事】 │ ......设备→内核缓冲区......
│ │
│ 【进程仍不阻塞】 │ 阶段二:内核→用户空间拷贝
│ │ ......copy_to_user() 由内核完成......
│ │
│<── 信号/回调通知 ────────────│ 两个阶段全部完成!
│ 数据已经在 buf 里了 │
▼ ▼

两个阶段都不阻塞:

  • 阶段一:发起调用后立即返回,进程不等待
  • 阶段二:内核自己完成拷贝到用户空间,进程完全不参与
  • 全部完成后通过回调/信号通知进程,此时数据已经在用户缓冲区里了

唯一真正的异步——进程在两个阶段都不需要等待。


二、对比总结表

I/O 模型阶段一(设备→内核)阶段二(内核→用户)本质
同步阻塞阻塞等待阻塞等待全程卡死
同步非阻塞轮询(不阻塞)阻塞等待阶段一轮询浪费CPU
I/O 多路复用阻塞在select上(可监控多fd)阻塞等待阶段一一对多
信号驱动不阻塞(内核信号通知)阻塞等待阶段一异步通知
异步 I/O不阻塞不阻塞(内核完成)全程无感知

三、关键洞察

同步与异步的分界线在阶段二

              阶段二:内核 → 用户空间拷贝

┌───────────┼───────────┐
│ │
进程自己参与等待 内核独立完成
(recvfrom 阻塞) (拷贝完回调通知)
│ │
同步 I/O 异步 I/O
(前四种都是) (只有 AIO)

POSIX 对同步/异步 I/O 的定义:

  • 同步 I/O:导致进程在 I/O 操作期间被阻塞(阶段二 copy_to_user 时进程阻塞)
  • 异步 I/O:不导致进程在 I/O 操作期间被阻塞(阶段二由内核全权完成)

所以前四种模型,无论阶段一多么”异步”,只要阶段二进程仍被阻塞,都属于同步 I/O。只有 AIO 在两个阶段都不需要进程参与,才是真正的异步。


第三部分:阻塞的本质 —— 进程挂起与 CPU 使用权

一、阻塞的本质:进程被挂起,让出 CPU

当进程发生阻塞时:

进程状态转换:
Running (R) ──阻塞事件──> Sleeping (S/D) ──事件就绪──> Running (R)
运行态 睡眠态 运行态
【占用CPU】 【不占用CPU】 【重新占用CPU】

关键点:

  1. 阻塞 = 进程进入睡眠状态(S 或 D 状态)
  2. 睡眠状态的进程不会被调度器分配 CPU 时间片
  3. CPU 被释放出来,去执行其他就绪进程

二、Linux 进程状态详解

状态含义CPU 使用
RRunning/Runnable(运行/就绪)正在使用或等待分配 CPU
SInterruptible Sleep(可中断睡眠)不占用 CPU
DUninterruptible Sleep(不可中断睡眠)不占用 CPU
ZZombie(僵尸)不占用 CPU
TStopped(停止)不占用 CPU

阻塞 I/O 会让进程进入 S 或 D 状态,此时进程完全不占用 CPU。


三、阻塞 vs 忙等待对比

┌─────────────────┬──────────────┬──────────────┐
│ │ 阻塞 I/O │ 忙等待 │
├─────────────────┼──────────────┼──────────────┤
│ 进程状态 │ S/D (睡眠) │ R (运行) │
│ CPU 占用 │ 0% │ 接近 100% │
│ 能否被调度 │ 不能 │ 能 │
│ 其他进程受影响 │ 不受影响 │ 抢占 CPU │
│ 唤醒机制 │ 内核中断 │ 自己检测 │
└─────────────────┴──────────────┴──────────────┘

第四部分:copy_to_user() 时进程的真实状态

一、关键纠正

错误理解 ❌

“内核拷贝数据时,进程阻塞(睡眠状态),没有 CPU 使用权”

正确理解 ✅

“内核拷贝数据时,进程处于内核态运行状态占用 CPU 执行拷贝操作”


二、完整的数据拷贝过程

read() 系统调用为例:

用户进程                    内核
│ │
│ 用户态 (User Mode) │
│ 进程状态: R (运行) │
│ │
├─ read(fd, buf, size) ────┤ ① 系统调用,陷入内核
│ │
│ ╔════════════════════╗ │
│ ║ 切换到内核态 ║ │
│ ║ (Kernel Mode) ║ │
│ ╚════════════════════╝ │
│ │
│ ├─ ② 检查内核缓冲区有无数据
│ │
│ 【情况A:数据未就绪】 │
│ 进程状态: S (睡眠) ├─ ③ 数据未就绪,进程睡眠
│ CPU使用: 0% │ schedule() 让出CPU
│ ......等待...... │ ......时间流逝......
│ │
│ ├─ ④ 数据到达,中断唤醒进程
│ 进程状态: R (就绪) │ wake_up()
│ │
│ 【情况B:数据已就绪】 │
│ 进程状态: R (运行) │
│ CPU使用: 100% ├─ ⑤ copy_to_user() 执行拷贝
│ (在内核态运行) │ 【进程占用CPU执行拷贝】
│ │ memcpy(user_buf, kernel_buf, size)
│ │
│ ╔════════════════════╗ │
│ ║ 返回用户态 ║ │
│ ║ (User Mode) ║ │
│ ╚════════════════════╝ │
│ │
│ 进程状态: R (运行) │
│ 继续执行后续代码 │
▼ ▼

三、两个阶段的进程状态对比

阶段进程状态CPU占用说明
阶段一:等待数据到达内核S (睡眠)0%进程挂起,让出CPU
阶段二:内核→用户空间拷贝R (运行,内核态)100%进程占用CPU执行拷贝

四、为什么拷贝时进程必须占用 CPU?

1. copy_to_user() 是 CPU 指令序列

// 内核源码简化版
unsigned long copy_to_user(void __user *to, const void *from, unsigned long n)
{
// 这是一系列 CPU 指令:
// - 检查地址合法性
// - 逐字节/逐字拷贝(或使用 SIMD 指令)
// - 处理页错误(page fault)

while (n--) {
*to++ = *from++; // CPU 执行的内存拷贝指令
}
return 0;
}

这些指令需要 CPU 来执行,所以进程必须占用 CPU。

2. 进程在内核态运行

进程的两种运行模式:
┌─────────────────────────────────────┐
│ 用户态 (User Mode) │
│ - 执行用户代码 │
│ - 受限的指令集 │
│ - 只能访问用户空间内存 │
├─────────────────────────────────────┤
│ 内核态 (Kernel Mode) │
│ - 执行内核代码 │ ← copy_to_user() 在这里
│ - 完整的指令集 │
│ - 可以访问所有内存 │
└─────────────────────────────────────┘

进程在内核态运行时,仍然是这个进程在占用 CPU,只是执行的是内核代码。


五、为什么说”阻塞 I/O 在拷贝时阻塞”?

这里的”阻塞”指的是:

1. 从应用程序视角

ssize_t n = read(fd, buf, size);  // 这个调用会"卡住"
// 在 read() 返回之前,无法执行下一行代码
printf("读取完成\n");

应用程序被”阻塞”在 read() 调用上,无法继续执行后续代码。

2. 从进程调度视角

虽然进程在内核态运行(占用 CPU),但:

  • 进程无法被抢占去做其他用户态的事情
  • 必须等待 copy_to_user() 完成才能返回用户态

这种”不能做其他事”的状态,在 I/O 模型讨论中被称为”阻塞”。


六、更精确的术语

术语含义
睡眠阻塞进程状态 S/D,让出 CPU,等待事件唤醒
内核态阻塞进程在内核态运行,占用 CPU,但无法返回用户态

阶段一是”睡眠阻塞”,阶段二是”内核态阻塞”。


七、DMA 的特殊情况

DMA 只负责设备到内核的拷贝:

设备 ──DMA──> 内核缓冲区    ← 这个过程不占用 CPU(由 DMA 控制器完成)
内核缓冲区 ──CPU──> 用户缓冲区 ← 这个过程必须占用 CPU

DMA 只负责设备到内核的拷贝,内核到用户空间的拷贝仍需要 CPU。


八、总结修正

完整图示:

阶段一:等待数据到达内核
进程状态: S (睡眠)
CPU 占用: 0%
进程位置: 等待队列

【数据到达,中断唤醒】

阶段二:内核→用户空间拷贝
进程状态: R (运行,内核态) ← 关键!
CPU 占用: 100% ← 关键!
进程位置: 运行队列
正在执行: copy_to_user()

【拷贝完成,返回用户态】

继续执行用户代码
进程状态: R (运行,用户态)
CPU 占用: 取决于代码

所以,”阻塞 I/O 在两个阶段都阻塞”的准确含义是:

  • 阶段一:进程睡眠,不占用 CPU
  • 阶段二:进程在内核态运行,占用 CPU,但无法执行用户代码

总结

  1. 同步/异步 关注的是结果获取方式(主动拉取 vs 被动推送)
  2. 阻塞/非阻塞 关注的是等待时的线程状态(挂起让出CPU vs 保持运行)
  3. 阻塞 = 进程挂起,让出 CPU,不是占用 CPU 但什么也不做
  4. copy_to_user() 时进程在内核态运行,占用 CPU 执行拷贝指令
  5. 只有异步 I/O (AIO) 在两个阶段都不需要进程参与,是真正的异步
上一篇 struct inode 和 struct file 详解

这两个是 Linux 内核中 VFS(虚拟文件系统)层的核心结构体,驱动开发者不需要自己定义它们,而是由内核在调用驱动函...

下一篇 IO模型详解(数据流本质视角)

阻塞、非阻塞、同步、异步详解(数据流本质视角) 本文档从”数据从设备到用户空间的完整旅途”这一角...