彻底理解非阻塞IO(NIO)

本文详细介绍了Linux下的BIO、NIO和IO多路复用(select/poll/epoll)的概念与区别。NIO通过非阻塞IO和IO多路复用实现同步非阻塞,降低了高并发场景下的线程资源消耗,而epoll作为IO多路复用的高效实现,解决了轮询带来的CPU占用问题,提升了系统资源利用率。


前言

BIO和NIO是基于Linux的后端开发的重要IO模型.

NIO(Non-blocking I/O) :
非阻塞IO, 由于还有一个AIO(Asynchronous I/O, 异步IO), 为了区分, NIO也被称为同步非阻塞IO,
现在常用的NIO, 是NIO+IO多路复用(IO Multiplexing)的结合体, epoll还是会阻塞线程, 不是真正的非阻塞IO了.

BIO(Blocking I/O) :
阻塞IO, 为了和NIO保持队形, 也被称为同步阻塞IO.

不同操作系统对于IO模型的支持程度不同,
Linux对AIO支持得并不好, 因此基于Linux的后端开发一般不用AIO, 本文也不多说它了.


一、同步/异步 和 阻塞/非阻塞

1.概念

在linux中涉及到同步和阻塞时, 一般来说, 调用方是指应用(application), 被调用方是指linux内核(kernel)

同步
调用方发起一个功能调用时, 在没有得到功能的结果之前, 该调用不会返回.
也就是调用方会等待被调用方返回功能的结果.

异步
调用方发起一个功能调用时, 没有得到功能的结果立即返回, 后续被调用方再通过回调等手段, 把功能的结果通知调用方.
也就是调用方立即得到返回,但是返回中不包含功能的结果.

在linux的AIO中, 异步的结果是通过内核的回调拿到的, 本文的NIO只涉及到同步, 没有异步.

阻塞
线程发起一个调用时, 在调用返回之前, 线程会被阻塞, 在这个状态下会交出当前CPU的使用权而暂停.
也就是调用方会等待调用结果, 调用阻塞了调用方的线程, 线程不在运行处理中.

非阻塞
线程发起一个调用时, 调用会立即返回, 避免线程被阻塞.
但是, 返回的结果只是被调用方当前状态的值, 实际使用时, 调用方需要轮询, 直到返回结果符合预期(直到数据准备好).

2.区别

2.1. 同步和异步的区别:
区别是, 调用方是否能够直接得到功能的结果
同步会一直等待, 直到得到功能的结果
异步会立即得到一个不包含结果的返回, 功能的结果要由被调用方通知.

2.2. 阻塞和非阻塞的区别:
区别是, 调用是否会立即返回, 进而是否阻塞调用方的线程, 在网络IO中, 是指在数据未准备好的情况下, 是否会阻塞线程直到数据准备好
阻塞会阻塞线程, 调用方只能等待
非阻塞由于调用立即返回, 而不阻塞线程, 调用方可以继续做别的事

2.3. 同步和阻塞区别:
区别是, 是否会阻塞调用方线程
同步不会阻塞调用方线程, 线程在运行处理中, 只是逻辑上调用未返回而已
阻塞会阻塞调用方线程, 线程不在运行处理中

2.4. 同步异步和阻塞非阻塞的区别:
区别是, 讨论点不同
同步异步讨论的是 被调用方的返回结果的通知机制
阻塞非阻塞讨论的是 调用方的线程状态

二、NIO的演变

1.NIO的诞生

1.1 前置知识

假设在Linux上运行应用, 该应用监听了某个端口, 通过网络获取外部数据.
应用(application)是运行在Linux的用户空间的,
而外部数据是在Linux的内核空间中接收到的,

在这里插入图片描述
外部数据先保存到网卡, 再保存到内核缓冲区, 最后进入应用缓冲区.

应用调用read方法读取外部数据, Linux的内核操作分为2步:
第一步: 等待数据就绪
第二步: 把内核缓冲区的数据复制到用户缓冲区
参照下面1.2中的图

1.2 BIO和NIO

Linux的IO模型最初的是BIO
BIO:
在这里插入图片描述
BIO一次read(), 触发了内核的两步操作, 全程都阻塞.

优点: 线程阻塞时, 消耗的CPU资源小
缺点: 高并发时容易耗尽线程资源, 且大量线程会引起线程切换的巨大开销, 造成响应不及时

NIO:
在这里插入图片描述

NIO, 靠着Linux提供的可以立即返回的read(), 在第一步中是不阻塞的, 在第二步中仍然是阻塞的.

优点: 把应用线程的read调用, 分割出了第一步和第二步, 应用线程可以做更多实时的控制
缺点: 第一步中的轮询, 大量占用CPU时间, 降低系统资源利用率, 所以一般web服务器也不使用这样的NIO

为了解决NIO轮询的缺点, 引入了IO多路复用, 用操作系统的IO多路复用, 来替代轮询

2. IO多路复用的演变

2.1 IO多路复用是什么?

IO多路复用是一种同步IO模型, 是操作系统提供的能力, 使用少量线程监听Linux的多个IO文件描述符, 也就是线程复用于多个IO请求.
如果有任意IO文件描述符就绪, 就会告知应用进行读取,
如果没有IO文件描述符就绪, 就会阻塞应用线程.
Linux中, IO多路复用的实现是select/poll/epoll

IO多路复用的作用: 线程复用于多个请求, 用较小的线程开销来支持更多的并发请求

在这里插入图片描述
NIO结合IO多路复用后, select/poll/epoll替代了轮询.

2.2 select/poll/epoll

Linux上的IO多路复用演变经历了3个阶段:
一.select
二.poll
三.epoll
目前使用的是epoll, 默认是epoll的水平触发模式(LT), 还有一个边缘触发模式(ET), 应用一般是用LT, 感兴趣的可以自行搜索

一.select
轮询遍历IO文件描述符, O(n)的线性时间复杂度, 因此随着IO文件描述符的增多, 性能下降
单进程能够打开的文件描述符有数量限制, 导致应用的最大连接数是1024
调用select时, 还需要文件描述符在用户空间和内核空间的拷贝, 影响性能

二.poll
同样是轮询遍历IO文件描述符, O(n)的线性时间复杂度, 因此随着IO文件描述符的增多, 性能下降
无文件描述符的数量限制
调用poll时, 还需要文件描述符在用户空间和内核空间的拷贝, 影响性能

三.epoll
解决了select和poll的全部缺点,
采用注册回调机制, 替代了轮询遍历,
无文件描述符的数量限制
利用mmap减少了文件描述符在用户空间和内核空间的拷贝
因此性能高


总结

和BIO相比, NIO主要是把"等待数据就绪"这一步拆分出来, 其本身是利用轮询来获取IO数据就绪状态.
轮询操作是同步(直接得到是否就绪的结果)的,
轮询期间, 由于调用会立即返回, 所以调用方线程是非阻塞的,
这就是NIO同步非阻塞的含义.
轮询的缺点是CPU占用大, 用IO多路复用(epoll)替代轮询后, 才使得NIO真正拥有了高并发能力.

<think>我们正在讨论poll机制。用户的问题集中在两点:poll机制的作用,以及为何使用poll后仍然感觉像是阻塞IO。我们需要结合之前提供的引用来回答。 首先,根据引用[1]中的描述: > 如果想要即使按键没有按下,在一定的时间后也能返回,要实现这种功能,可以使用poll机制。 这表明poll机制的一个关键作用就是设置超时时间,使得即使没有事件发生,也能在超时后返回,而不是无限期阻塞。 其次,引用[2]中提到: > select/poll/epoll 都是实现 Multiplexing IO 的三种不同的方式。 并且将Multiplexing IO归类为同步阻塞IO(实际上严格来说,它们属于IO多路复用,是同步的一种形式,但并非传统阻塞)。同时,引用[2]也区分了阻塞非阻塞的概念:阻塞表示一直在等待,非阻塞表示不等待去执行其他任务。 现在,我们来分析为什么使用poll后仍感觉是阻塞IO: 1. **poll机制的本质**:poll是一种同步IO多路复用机制。它允许一个进程监视多个文件描述符(fd),当其中任何一个fd就绪(有事件发生)时,poll就会返回;或者当设置的超时时间到达时,poll也会返回。 2. **阻塞行为**: - 当调用poll时,如果没有就绪的事件,且超时时间未到,那么调用进程会被阻塞(进入睡眠状态),直到有事件发生或超时。 - 如果设置了超时时间为0,那么poll不会阻塞,立即返回(非阻塞模式)。 - 如果设置了超时时间大于0(如100ms),那么在这段时间内,如果没有事件发生,进程就会阻塞在这段时间内。因此,在等待期间,进程仍然是被阻塞的(处于不可中断睡眠或可中断睡眠状态)。 3. **为何感觉是阻塞**: - 因为poll调用本身是阻塞的(除非超时设置为0)。尽管它可以同时监视多个fd,但调用poll的进程在等待期间无法执行其他任务(除非使用多线程/多进程)。也就是说,在poll返回之前,调用它的线程会被挂起。 - 虽然我们可以设置超时时间,使得阻塞时间有限(而不是无限等待),但在超时时间内,线程仍然是被阻塞的。所以用户感觉它还是阻塞的,只是这个阻塞有一个最大时间限制。 4. **与真正非阻塞IO的区别**: - 非阻塞IONIO)是指当我们设置文件描述符为非阻塞模式(O_NONBLOCK)后,对该fd的read/write等操作会立即返回,如果数据没有准备好,则返回一个错误(如EAGAIN),这样进程可以继续执行其他任务。 - 而poll机制本身是一个等待就绪事件的系统调用,在事件未就绪且未超时的情况下,调用线程会被阻塞住。因此,它仍然是同步阻塞的(尽管可以设置超时时间,但阻塞仍然发生)。 5. **IO多路复用的同步特性**: - 引用[2]中将Multiplexing IO归类为同步阻塞IO(实际上更准确的说法是同步非阻塞IO?但这里我们按照引用中的说法)。实际上,IO多路复用(select/poll/epoll)本身是同步的,因为它们需要进程自己主动去检查IO状态(就绪后,数据从内核空间拷贝到用户空间的过程仍然是阻塞的)。 因此,用户感觉使用poll后仍然是阻塞IO,原因在于: - **poll调用本身的阻塞行为**:在等待事件发生或超时的这段时间里,线程是被阻塞的。 - **对比异步IO(AIO)**:真正的异步IO(如AIO)在发起IO操作后,进程可以立即返回去做其他事情,当IO操作完成时,内核会通过信号或回调函数通知进程。而poll并不是这样,它需要进程主动去轮询(或等待)事件。 **总结**: - **poll机制的作用**:允许进程同时监视多个文件描述符,并且可以设置超时时间,避免无限期阻塞。 - **感觉仍是阻塞IO的原因**:因为poll调用在等待期间会阻塞调用线程,直到有事件发生或超时,所以它仍然具有阻塞特性(尽管可以设置超时,但等待期间是阻塞的)。 为了更清晰地说明,我们可以用代码示例来解释: ```c #include <poll.h> #include <stdio.h> #include <unistd.h> int main() { struct pollfd fds[1]; fds[0].fd = STDIN_FILENO; // 监视标准输入 fds[0].events = POLLIN; // 监视可读事件 int ret = poll(fds, 1, 1000); // 超时时间设为1000ms if (ret > 0) { printf("Data is available!\n"); } else if (ret == 0) { printf("Timeout occurred!\n"); } else { perror("poll error"); } return 0; } ``` 在这个例子中,调用`poll`后,程序会阻塞最多1000ms(1秒)。如果在这1秒内标准输入有数据可读,poll会立即返回;否则,1秒后超时返回。在等待的这段时间里,程序是阻塞的。 因此,虽然poll避免了无限期阻塞(因为有超时),但它在等待过程中仍然是阻塞的,这就是为什么用户感觉它还是阻塞IO。 相关问题: 1. 如何实现真正的非阻塞IO? 2. poll、select和epoll之间有什么区别? 3. 异步IO(AIO)是如何工作的? 4. 在什么情况下应该使用poll而不是epoll? 5. 如何避免使用poll时的阻塞等待? 以上问题可以帮助用户进一步理解IO模型。 </think>### Poll机制详解 #### Poll机制的核心作用 Poll机制是Linux系统中实现**I/O多路复用**的关键技术,主要解决两个核心问题: 1. **避免无限期阻塞**:通过设置超时时间,即使没有I/O事件发生,也能在指定时间后返回(如100ms超时)[^1] 2. **单线程监控多路I/O**:允许单个线程同时监控多个文件描述符(fd)的就绪状态,替代传统的多线程阻塞方案[^2] 数学表达其行为模式: $$ \text{poll\_return} = \begin{cases} >0 & \text{就绪fd数量} \\ 0 & \text{超时触发} \\ -1 & \text{错误发生} \end{cases} $$ #### 为何仍感觉是阻塞I/O 尽管poll具备超时机制,但在特定场景下仍表现为阻塞行为,原因在于: 1. **同步等待的本质** poll属于**同步I/O多路复用模型**(Multiplexing I/O)[^2]。当调用`poll(fds, nfds, timeout=100)`时: - 若有就绪fd:立即返回(此时非阻塞) - 若无就绪fd:线程进入休眠状态,最多阻塞100ms ```c // 典型调用流程 while (1) { int ret = poll(fds, 3, 100); // 监控3个fd if (ret > 0) { // 处理就绪fd(非阻塞段) } else if (ret == 0) { // 超时处理(阻塞段结束) } } ``` 2. **阻塞发生的场景** | 场景 | 表现 | 用户感知 | |---------------------|--------------------------|---------------| | 所有fd均未就绪 | 线程休眠等待 ≤100ms | 明显阻塞感 | | 处理就绪事件耗时过长| 下一轮poll被延迟 | 间接阻塞 | | timeout设为-1 | 无限等待直到事件发生 | 完全阻塞 | 3. **与异步I/O的本质区别** 引用[2]中的奶茶店比喻可直观解释: - **poll机制**:店员(内核)让顾客(进程)在店里等待(阻塞),但提供闹钟(超时) - **真正异步I/O**:店员给顾客呼叫机(回调函数),顾客可自由活动(非阻塞) $$\begin{array}{c|c} \text{特性} & \text{poll} & \text{异步I/O(AIO)} \\ \hline \text{进程状态} & \text{同步等待} & \text{异步回调} \\ \text{CPU占用} & \text{等待时0\%} & \text{全程可执行任务} \\ \text{响应延迟} & \leq \text{timeout} & \text{事件触发即响应} \end{array}$$ #### 解决阻塞感的实践方案 1. **设置合理超时** - 网络服务:10-100ms(平衡延迟与CPU消耗) - 实时系统:1-10ms(需配合高精度定时器) 2. **结合非阻塞fd** ```c fcntl(fd, F_SETFL, O_NONBLOCK); // 设置非阻塞标志 poll(&fds, 1, 100); // 即使就绪也避免read阻塞 ``` 3. **事件驱动架构** 使用epoll(Linux特有)替代poll,时间复杂度从$O(n)$降至$O(1)$: ```c epoll_wait(epfd, events, maxev, 100); // 高性能事件等待 ``` 4. **线程池配合** ```mermaid graph LR A[主线程poll监控] -->|就绪fd| B[线程池处理I/O] A -->|超时| C[执行定时任务] ``` > **关键认知**:poll通过超时机制避免了**无限阻塞**,但其同步等待特性本质上仍属于**有限时间阻塞**。要彻底消除阻塞感,需升级为epoll+非阻塞fd的组合方案或异步I/O模型[^2]。 --- ### 相关问题 1. 如何通过epoll实现真正的非阻塞I/O? 2. poll与select在文件描述符数量限制上有何差异? 3. 在Java NIO中,Selector.select(timeout)的实现是否等同于poll机制? 4. 如何量化评估不同timeout值对系统吞吐量的影响? 5. 为什么Kafka消费者使用poll而非epoll作为默认机制?[^1]
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值