IO多路复用
一、背景
之前对IO多路复用,听过,也看了相关博客,但是内化方面的话,实在是不敢恭维自己。
今天刚好这个时间,把这块重新梳理下,打一个mark,同时抽象下这块的底层思路。
二、普及
1、拉齐认知
- 服务端IO,大概分为两个步骤,获取连接,读取数据
- 服务端IO,会涉及到用户态和内核态两个,一般IO会从网卡拷贝数据到内核,内核再拷贝数据到用户态
2、传统IO问题
(1)资源利用率低:传统的阻塞IO模型中,每个IO操作都需要一个独立的线程来处理,这会导致大量的线程创建和销毁,消耗系统资源。
(2)并发处理能力差:在高并发场景下,传统的阻塞IO模型难以高效处理大量的并发连接。
(3)线程上下文切换开销大:大量的线程会导致频繁的线程上下文切换,增加系统开销。
(4)可扩展性差:传统的阻塞IO模型在面对大量连接时,性能会急剧下降,难以扩展。
3、演化节奏
第一步优化:如果用户态上自己写代码,采用一个线程做来网络连接的事,等待读取和处理数据的事交给另一个线程
问题:只是连接线程没有阻塞,仍然阻塞在了读取上,所以需要系统态提供一个 ”真正的非阻塞的函数“, 该函数可以在没有消息时候返回-1
第二步优化:用户态上,循环调用系统态的 READ 函数,或者停顿一段时间调用,
问题:当网络有数据可读取时候,READ函数仍然是阻塞的 为每一个连接分配一个线程,很快就会耗光系统资源
第三步优化:用户态来了连接,其中一个线程负责放在一个数组内,然后用另一个线程,轮流调用每一个元素的READ函数,
问题:仍然是用户态需要轮询,且每次调用时候,都需要copy 文件描述符到内核态,检验完再返回用户态。
4、总结问题
(1)可否一次性把文件描述符给了系统态,系统自己去监听文件描述符的情况
(2)系统态每次都需要轮询描述符的集合,判断是否有消息可读,耗费资源,高并发场景下,无法处理大量的连接,性能会很差。
(3)系统态返回的仍然是全部的描述符集合,还需要用户态自己去遍历判断哪个连接可读。
三、IO多路复用
select
(1)起源:Unix中引入,
(2)优化:用户态直接拷贝文件描述符到系统态,
(3)问题:a、select 仍然是阻塞的,b、最大为1024个描述符,c、select是系统态不停的轮询集合 d、需要返回给用户态全部描述符,用户态自己判断
poll
(1)优化:结构体数组来存储文件描述符,连接没有了限制
(2)问题:a、仍然是阻塞的,b、是系统态不停的轮询集合 c、需要返回给用户态全部描述符,用户态自己判断
epoll
(1)优化:
a、无需全量拷贝:系统态保存了描述符的集合,用户态只需要告诉增删改,不需要每次都全量拷贝
b、基于事件回调:内核不再通过轮询的方式找到就绪的文件描述符,而是通过异步 IO 事件唤醒。
c、增量回传信息:内核仅会将有 IO 事件的文件描述符返回给用户,用户也无需遍历整个文件描述符集合。
四、用户侧如何感知
1、轮询
用户线程可以定期调用epoll_wait,检查是否有事件发生。这种方法简单但效率较低。
2、信号
可以给文件描述符配置信号机制,当epoll有事件发生时,通过信号通知用户线程。
五、总结
用户态完全可以通过线程分割,来实现非阻塞io,但是这并不是多路复用的核心,多路复用的核心,是系统底层提供了一整套批量操作的能力。
那复用了什么东西?
(1):多个文件描述符共用一个epoll实例
(2):一个线程处理多个IO操作。
(3):用户态到系统态的文件描述符集合,只需要一次的拷贝,后续只需要增删改。
(4):通知机制,多个描述符,都可以服用消息通知机制,只有有了对应的事件,才会通知到用户层。