【记录】io多路复用的学习和理解

本文通过机场空管的比喻,生动形象地介绍了I/O多路复用的基本原理及select、poll、epoll等不同实现方式的特点。并对比了它们之间的优缺点。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

io多路复用在网络编程的过程中经常遇到,这让我在学习不深刻的我有种莫名的畏惧。所以下狠心恶补一把,彻底弄清楚后发现没这么难理解。

一下是我在知乎上看到的前辈的讲解,我感觉浅显易懂,基本解答了所以现阶段我想知道的所以问题。

(原作者措词比较.....幽默风趣,又比较长,所以我根据自己的理解和想知道的内容进行了增删<删除!!!!>
链接:http://www.zhihu.com/question/32163005/answer/55772739
来源:知乎)


假设你是一个机场的空管, 你需要管理到你机场的所有的航线, 包括进港,出港, 有些航班需要放到停机坪等待,有些航班需要去登机口接乘客。

你会怎么做?

最简单的做法,就是你去招一大批空管员,然后每人盯一架飞机, 从进港,接客,排位,出港,航线监控,直至交接给下一个空港,全程监控。

那么问题就来了:
  • 很快你就发现空管塔里面聚集起来一大票的空管员,交通稍微繁忙一点,新的空管员就已经挤不进来了。
  • 空管员之间需要协调,屋子里面就1, 2个人的时候还好,几十号人以后 ,基本上就成菜市场了。
  • 空管员经常需要更新一些公用的东西,比如起飞显示屏,比如下一个小时后的出港排期,最后你会很惊奇的发现,每个人的时间最后都花在了抢这些资源上。

现实上我们的空管同时管几十架飞机稀松平常的事情, 他们怎么做的呢?
他们用这个东西
&amp;lt;img src=&quot;https://i-blog.csdnimg.cn/blog_migrate/7925cf648453043de4c6ee76b2ea5525.jpeg&quot; data-rawwidth=&quot;550&quot; data-rawheight=&quot;534&quot; class=&quot;origin_image zh-lightbox-thumb&quot; width=&quot;550&quot; data-original=&quot;https://pic2.zhimg.com/583d5ba3cee12e78befa8e2b749f4269_r.jpg&quot;&amp;gt;这个东西叫flight progress strip. 每一个块代表一个航班,不同的槽代表不同的状态,然后一个空管员可以管理一组这样的块(一组航班),而他的工作,就是在航班信息有新的更新的时候,把对应的块放到不同的槽子里面。 这个东西叫flight progress strip. 每一个块代表一个航班,不同的槽代表不同的状态,然后一个空管员可以管理一组这样的块(一组航班),而他的工作,就是在航班信息有新的更新的时候,把对应的块放到不同的槽子里面。

这个东西现在还没有淘汰哦,只是变成电子的了而已。。

是不是觉得一下子效率高了很多,一个空管塔里可以调度的航线可以是前一种方法的几倍到几十倍。

如果你把每一个航线当成一个Sock(I/O 流), 空管当成你的服务端Sock管理代码的话.

第一种方法就是最传统的多进程并发模型 (每进来一个新的I/O流会分配一个新的进程管理。)
第二种方法就是I/O多路复用 (单个线程,通过记录跟踪每个I/O流(sock)的状态,来同时管理多个I/O流 。)


其实“I/O多路复用”这个坑爹翻译可能是这个概念在中文里面如此难理解的原因。所谓的I/O多路复用在英文中其实叫 I/O multiplexing. 如果你搜索multiplexing啥意思,基本上都会出这个图:
&amp;lt;img src=&quot;https://i-blog.csdnimg.cn/blog_migrate/4303afae3405a8c2abf19420bb8556e6.png&quot; data-rawwidth=&quot;250&quot; data-rawheight=&quot;177&quot; class=&quot;content_image&quot; width=&quot;250&quot;&amp;gt;
于是大部分人都直接联想到"一根网线,多个sock复用" 这个概念,包括上面的几个回答, 其实不管你用多进程还是I/O多路复用, 网线都只有一根好伐。多个Sock复用一根网线这个功能是在内核+驱动层实现的

重要的事情再说一遍: I/O multiplexing 这里面的 multiplexing 指的其实是在单个线程通过记录跟踪每一个Sock(I/O流)的状态(对应空管塔里面的Fight progress strip槽)来同时管理多个I/O流. 发明它的原因,是尽量多的提高服务器的吞吐能力。

是不是听起来好拗口,看个图就懂了.

&amp;lt;img src=&quot;https://i-blog.csdnimg.cn/blog_migrate/605e2a4aa37cd0f8bb974efb4df62bbe.jpeg&quot; data-rawwidth=&quot;400&quot; data-rawheight=&quot;119&quot; class=&quot;content_image&quot; width=&quot;400&quot;&amp;gt;

在同一个线程里面, 通过拨开关的方式,来同时传输多个I/O流, (学过EE的人现在可以站出来义正严辞说这个叫“时分复用”了)。

什么,你还没有搞懂“一个请求到来了,nginx使用epoll接收请求的过程是怎样的”, 多看看这个图就了解了。提醒下,ngnix会有很多链接进来, epoll会把他们都监视起来,然后像拨开关一样,谁有数据就拨向谁,然后调用相应的代码处理。

------------------------------------------
了解这个基本的概念以后,其他的就很好解释了。

select, poll, epoll 都是I/O多路复用的具体的实现,之所以有这三个鬼存在,其实是他们出现是有先后顺序的。

I/O多路复用这个概念被提出来以后, select是第一个实现 (1983 左右在BSD里面实现的)。

select 被实现以后,很快就暴露出了很多问题。
  • select 会修改传入的参数数组,这个对于一个需要调用很多次的函数,是非常不友好的。
  • select 如果任何一个sock(I/O stream)出现了数据,select 仅仅会返回,但是并不会告诉你是那个sock上有数据,于是你只能自己一个一个的找,10几个sock可能还好,要是几万的sock每次都找一遍。
  • select 只能监视1024个链接,linux 定义在头文件中的,参见FD_SETSIZE。
  • select 不是线程安全的,如果你把一个sock加入到select, 然后突然另外一个线程发现,尼玛,这个sock不用,要收回。对不起,这个select 不支持的,如果你丧心病狂的竟然关掉这个sock, select的标准行为是。。呃。。不可预测的, 这个可是写在文档中的哦.
“If a file descriptor being monitored by select() is closed in another thread, the result is unspecified”
霸不霸气

于是14年以后(1997年)一帮人又实现了poll, poll 修复了select的很多问题,比如
  • poll 去掉了1024个链接的限制,于是要多少链接呢, 主人你开心就好。
  • poll 从设计上来说,不再修改传入数组,不过这个要看你的平台了,所以行走江湖,还是小心为妙。
其实拖14年那么久也不是效率问题, 而是那个时代的硬件实在太弱,一台服务器处理1千多个链接简直就是神一样的存在了,select很长段时间已经满足需求。

但是poll仍然不是线程安全的, 这就意味着,不管服务器有多强悍,你也只能在一个线程里面处理一组I/O流。你当然可以那多进程来配合了,不过然后你就有了多进程的各种问题。

于是5年以后, 在2002, 大神 Davide Libenzi 实现了epoll.

epoll 可以说是I/O 多路复用最新的一个实现,epoll 修复了poll 和select绝大部分问题, 比如:
  • epoll 现在是线程安全的。
  • epoll 现在不仅告诉你sock组里面数据,还会告诉你具体哪个sock有数据,你不用自己去找了。

epoll 当年的patch,现在还在,下面链接可以看得到:
/dev/epoll Home Page


可是epoll 有个致命的缺点。。只有linux支持。比如BSD上面对应的实现是kqueue。



而ngnix 的设计原则里面, 它会使用目标平台上面最高效的I/O多路复用模型咯,所以才会有这个设置。一般情况下,如果可能的话,尽量都用epoll/kqueue吧。

详细的在这里:


PS: 上面所有这些比较分析,都建立在大并发下面,如果你的并发数太少,用哪个,其实都没有区别。 如果像是在欧朋数据中心里面的转码服务器那种动不动就是几万几十万的并发,不用epoll我可以直接去撞墙了

<think>嗯,IO多路复用这个概念我之前在学习网络编程的时候遇到过,但一直理解得不是很透彻。现在要详细解释的话,得先回忆一下相关的基础知识。首先,IO多路复用是解决高并发网络请求的一种技术,对吧?比如,当服务器需要同时处理多个客户端连接时,如果每个连接都用一个线程或者进程来处理,资源消耗会很大,效率也不高。这时候就需要一种机制,能够同时监控多个IO流的状态,一旦某个流就绪了(比如有数据可读或者可写),就能通知程序进行处理。 那传统的阻塞IO有什么问题呢?比如说,假设一个服务器使用阻塞的方式处理连接,每个连接都阻塞在read或者accept这样的系统调用上,那么当有多个客户端连接时,就需要为每个连接创建一个线程或者进程。这在连接数较多的时候,线程或进程的切换开销会变得非常大,导致系统性能下降。而IO多路复用就是为了解决这个问题,允许一个线程监控多个文件描述符,从而减少线程的数量,提高效率。 接下来,我需要理清楚IO多路复用的具体实现方式。常见的实现方式有select、poll、epollkqueue等。selectpoll是比较早期的系统调用,而epoll是Linux下性能更好的实现,kqueue则是在BSD系统上使用的。它们的核心思想都是让内核监控多个文件描述符,当有事件发生时,通知应用程序进行处理。 不过,selectepoll有什么区别呢?记得select有一个限制,就是它能监控的文件描述符数量有限,默认是1024个,这在一些高并发的场景下可能不够用。而epoll没有这个限制,它使用红黑树来管理文件描述符,效率更高。另外,select每次调用都需要将文件描述符集合从用户态拷贝到内核态,而epoll通过内核与用户空间共享内存来避免这种拷贝,减少了开销。还有,select采用的是轮询的方式,每次都要遍历所有文件描述符,而epoll是事件驱动的,只返回就绪的文件描述符,这样在处理大量连接时效率更高。 那IO多路复用异步IO有什么区别呢?异步IO的概念是,当应用程序发起一个IO操作后,不需要等待IO完成,可以继续执行其他任务,等IO完成后,系统会通知应用程序。而IO多路复用本质上还是同步IO,因为当检测到文件描述符就绪后,应用程序还是需要自己进行实际的IO操作(比如recvfrom),这个过程是阻塞的。不过,由于IO多路复用可以同时等待多个文件描述符,所以它能够更高效地处理多个IO请求。 还有,在编程模型上,使用IO多路复用时,通常会有一个事件循环,不断地调用select、poll或者epoll_wait来等待事件的发生。当有事件发生时,遍历就绪的文件描述符,处理相应的IO操作。这种模型在单线程下就可以处理大量连接,非常适合高并发的服务器程序,比如Nginx、Redis等。 不过,可能有人会问,为什么IO多路复用会比多线程或多进程更高效?因为线程或进程的创建切换需要较大的开销,而IO多路复用通过单线程或少量线程来管理多个连接,减少了上下文切换的次数资源消耗。当然,这也需要应用程序设计得当,避免在处理事件时出现阻塞操作,否则会影响到其他连接的响应。 再想想,实际应用中,比如使用epoll的时候,一般会设置文件描述符为非阻塞模式,然后使用边缘触发(ET)或者水平触发(LT)模式。水平触发模式下,只要文件描述符处于就绪状态,每次调用epoll_wait都会返回该事件;而边缘触发则只在状态变化时通知一次。使用ET模式需要更谨慎地处理所有数据,否则可能会遗漏事件,但可以减少epoll_wait的调用次数,提高效率。 总结一下,IO多路复用的核心思想是通过一种机制(如select、poll、epoll)让单个线程能够监控多个IO事件,当有事件发生时进行处理,从而提高系统的并发处理能力。它与多线程或多进程模型相比,减少了资源消耗上下文切换的开销,适用于高并发的网络编程场景。不过,具体的实现优化还需要根据不同的操作系统实际需求来选择合适的多路复用机制。</think>IO多路复用是网络编程中**解决高并发问题的核心技术**,其本质是通过**单线程监控多个IO流状态**,实现高效资源利用。以下通过类比原理逐步解释: --- ### 一、生活场景类比(快递驿站) 假设你开了一家快递驿站: - **传统阻塞模式**:每来一个包裹就雇专人盯着(一个线程对应一个连接),100个包裹需要100人,成本极高。 - **多路复用模式**:安装一个智能显示屏(内核机制),所有包裹到货都会在屏幕显示。你只需1人(单线程)盯着屏幕,哪个包裹就绪就处理哪个。 --- ### 二、核心原理拆解 1. **同步非阻塞基础** - 每个IO操作(如`socket.read()`)设置为非阻塞:调用后立即返回,无需等待数据就绪。 - **问题**:需要不断轮询所有连接,检查是否就绪,CPU空转浪费资源。 2. **多路复用核心思想** - 将**轮询工作交给内核**:通过`select/poll/epoll`系统调用,内核监控多个文件描述符(fd)。 - **事件驱动**:当某个fd就绪(可读/可写/异常),内核通知应用层处理。 --- ### 三、关键技术对比 | 机制 | 核心特点 | 适用场景 | |------------|--------------------------------------------------------------------------|------------------------| | **select** | • 线性扫描所有fd集合<br>• 最大1024个fd<br>• 需每次重置fd集合 | 跨平台兼容,低并发场景 | | **poll** | • 改用链表存储fd,突破数量限制<br>• 仍需遍历所有fd | 改进版select | | **epoll** | • 红黑树管理fd,事件回调机制<br>• 仅返回就绪fd<br>• 支持ET/LT模式 | Linux高并发场景 | | **kqueue** | • FreeBSD特有<br>• 类似epoll的高效实现 | BSD系统 | --- ### 四、Epoll的深度解析(以Linux为例) 1. **数据结构** - **红黑树**:存储所有待监听的fd(插入/删除O(logN)复杂度)。 - **就绪链表**:记录已就绪的fd,无需遍历全部。 2. **工作模式** - **水平触发(LT)**:只要fd处于就绪状态,每次epoll_wait都会通知。 - *示例*:收到2KB数据,应用只读取1KB,下次调用仍会提醒。 - **边缘触发(ET)**:仅在fd状态变化时通知一次。 - *示例*:必须一次性读完所有数据,否则可能丢失事件。 3. **核心API** ```c int epoll_create(int size); // 创建epoll实例 int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); // 管理fd int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout); // 等待事件 ``` --- ### 五、与异步IO的区别 | 特性 | IO多路复用 | 异步IO(如Linux AIO) | |---------------|------------------------|---------------------------| | **操作阶段** | 同步(应用需自行读写) | 异步(内核完成读写后通知) | | **编程复杂度**| 较低 | 较高 | | **性能** | 高吞吐,适合短连接 | 适合大文件连续读写 | --- ### 六、典型应用场景 1. **Web服务器**(Nginx、Tornado) - 单线程处理数万并发连接。 2. **数据库中间件**(Redis) - 高吞吐量的命令处理。 3. **即时通讯系统** - 同时管理大量长连接。 --- ### 七、代码示例(简化版Epoll流程) ```python import selectors import socket sel = selectors.DefaultSelector() # 自动选择最佳实现(如epoll) def accept(sock): conn, addr = sock.accept() conn.setblocking(False) sel.register(conn, selectors.EVENT_READ, read) def read(conn): data = conn.recv(1024) if data: print("Received:", data) else: sel.unregister(conn) conn.close() sock = socket.socket() sock.bind(('', 8080)) sock.listen() sock.setblocking(False) sel.register(sock, selectors.EVENT_READ, accept) while True: events = sel.select() # 阻塞等待事件 for key, mask in events: callback = key.data # accept或read函数 callback(key.fileobj) ``` --- ### 八、关键总结 - **核心价值**:用单线程/少量线程管理海量连接,减少资源消耗。 - **选择建议**: - Linux优先用epoll(尤其>1000并发)。 - 跨平台考虑libevent/libuv抽象层。 - **注意事项**: - ET模式下必须处理完所有数据。 - 避免在事件回调中执行阻塞操作。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值