深入理解 Linux 网络 I/O 模型
在网络编程领域,BIO、NIO、epoll、AIO 等名词频繁出现。它们究竟是什么?为什么 Nginx 和 Redis 能支撑海量并发?答案藏在操作系统的网络 I/O 模型中。
本文将深入剖析 Linux 下的五种经典网络 I/O 模型。
核心前提:一次 I/O 到底经历了什么?
在讲解具体模型之前,必须统一一个概念:在操作系统眼中,一次完整的网络读取操作,实际上分为两个截然不同的阶段。
- 阶段一:等待数据准备就绪 (Waiting for data)。 网卡接收到网络传来的数据,然后操作系统内核将这些数据读取到内核缓冲区 (Kernel Buffer) 中。
- 阶段二:将数据从内核空间拷贝到用户空间 (Copying data from kernel to user)。 操作系统将内核缓冲区中的数据,搬运到我们应用程序定义的用户缓冲区 (User Buffer) 中。
理解了这两个阶段,接下来的五种模型无非就是在这两个阶段上做了不同的取舍和优化。
1. 阻塞 I/O 模型 (Blocking I/O)
这是最经典、也是我们在 C 语言中调用 socket() 默认创建的模型。
当应用程序调用 recvfrom() 时,如果内核中没有数据,进程就会原地挂起(交出 CPU 执行权),阻塞等待。直到数据到达网卡并被装入内核缓冲区(阶段一完成),内核接着把数据拷贝到用户空间(阶段二完成),函数才返回成功。
用户进程 (User Space) 内核 (Kernel Space)
----------------- -------------------
| | |
|------ 1. 调用 recvfrom() --------->|
| 阻 | [阶段一:等待数据到达网卡]
| 塞 | ... (数据准备就绪)
| 等 | [阶段二:内核拷贝数据到用户态]
| 待 | ...
| |<----- 2. 拷贝完成,返回成功 ----------|
| | (处理数据)
- 痛点:整个过程应用进程全被阻塞。一个线程只能处理一个连接,面对高并发时,只能靠开启海量的线程来应对,上下文切换的开销会瞬间压垮服务器。
2. 非阻塞 I/O 模型 (Non-blocking I/O)
为了不让线程被死死卡住,我们可以将 Socket 设置为 O_NONBLOCK 标志。
在这种模型下,应用进程调用 recvfrom() 时,如果内核里没数据,内核会立刻返回一个 EWOULDBLOCK 错误。进程收到错误后,就知道数据没好,可以去干点别的,然后隔三差五地来轮询检查。但是,一旦数据准备好了,阶段二的数据拷贝依然是阻塞的。
用户进程 (User Space) 内核 (Kernel Space)
----------------- -------------------
| | |
|------ 1. 调用 recvfrom() --------->|
|<----- 2. 返回 EWOULDBLOCK (未就绪) ---|
| 做 |------ 3. 再次调用 recvfrom() --------->|
| 点 |<---- 4. 返回 EWOULDBLOCK (未就绪) ---|
| 别 |------ . 再次调用 () --------->|
| 的 | ... (此时数据准备就绪!)
| 阻 | 阻
| 塞 | 塞
| |
| |<----- . 拷贝完成,返回成功 ----------|
| |

