IO 多路复用

IO 多路复用

IO 多路复用实现了代替进程监控大量描述符什么时候就绪,然后进程可以根据就绪的描述符作出相应的操作。

IO 过程分为等待和拷贝,等待完毕可以拷贝的时候,这个状态称为就绪。

常见的三种 IO 多路复用为 select、poll和 epoll。

select

原理:(已 C/C++ 中系统调用 select 函数为例)

  1. 用户将需要监控的文件描述符添加到一个集合中。
  2. select 系统调用将集合中的文件描述符拷贝到内核中进行监控,在内核中对所有的描述符进行遍历操作,且只关注是否有描述符就绪。
  3. 若有描述符就绪,则内核会把未就绪的描述符移除,然后返回就绪的文件描述符。
  4. 用户遍历返回的描述符,对比这些描述符是否在自己监控的集合中,若存在则对其进行处理。(对比操作使用 FD_ISSET 系统调用来执行)

内核中,用类似位图的方式来存放描述符,每一个 bit 表示一个描述符。

缺点:

  • 需要手动设置描述符集合,使用不方便。
  • 每次都需要把描述符拷贝到内核中,当描述符量大的时候,系统开销也会增加。
  • 因为内核对描述符是遍历判断,所以随着描述符的增多,该遍历操作会逐步变慢,影响效率。
  • select 支持的文件描述符数太小。
  • select 并不会告诉用户哪个描述符就绪了,需要用户手动判断。
  • select 会修改文件描述符集合(有描述符就绪时,新返回的集合就只包含就绪的描述符),因此每次监控都需要用户重新向内核中拷贝集合。

优点:

  • 可跨平台。
  • 监控的超时等待时间可以精确到微妙。
poll

原理

  1. 定时描述符事件结构,添加用户监控的描述符以及相应的事件
  2. 将事件结构信息拷贝到内核中进行监控,poll 同样采用轮询遍历的方式进行监控(性能随着描述符数量的增多而下降)。
  3. 当有描述符就绪,poll将就绪的事件写入到事件结构的revents成员变量中,然后调用返回。
  4. poll也并没有告诉用户具体哪些描述符就绪,需要用户对事件结构信息中revents进行判断,从而得知描述符是否就绪;进而对描述符进行相应事件操作revents&POLLIN/POLLOUT

优点:

  1. poll采用事件结构形式对描述符关心的事件进行监控,简化了select三种集合操作的流程。
  2. poll没有描述符上限设置。

缺点:

  1. 不能跨平台。
  2. 在内核中进行轮询遍历判断,性能随着描述符事件增多而下降。
  3. 也不会告诉用户具体哪一个描述符就绪,需要用户轮询遍历判断事件中的revents 。
  4. 需要每次都向内核中拷贝监控信息。
  5. 监控的超时等待时间只能精细到毫秒。
epoll

监控流程

红黑树保存需要监控的描述符和相应的监控事件,双链表保存就绪的描述符对应的事件结构体。

  • epoll对描述符对应的事件监控是一个异步操作;
  • epoll_wait发起调用,让操作系统对描述符进行相应事件监控;
  • 操作系统对每个要监控的描述符都定义了就绪事件回调函数;
  • 当描述符相应事件就绪的时候,触发事件,调用回调函数(ep_poll_callback)(将描述符事件结构信息指针添加到eventpoll的双向链表中)
  • 但是epoll_wait并没有直接返回(是一个阻塞操作),每隔一会就看一下event_poll中双向链表是否为空
    来判断是否有描述符就绪;
  • 若为空则没有描述符就绪,则等待一会重新查看;
  • 若双向链表不为空,表示有描述符事件就绪,将这个描述符对应的事件结构信息拷贝到epoll_wait传入的事件结构数组中后,调用返回。

优点

  1. epoll采用事件结构方式对描述符进行监控,简化了select集合操作流程。
  2. epoll描述符监控无上限。
  3. 每个epoll监控的描述符事件信息,只需要向内核靠背一次。
  4. epoll_wait使用异步阻塞操作在内核中完成事件监控。
    事件监控是操作系统通过事件回调的方式将就绪描述符事件信息添加到双向链表中;
    而epoll_wait只是每隔一段时间看一下双向链表是否为空判断是否有描述符就绪,并非轮询,性能不会随着描述符增多而降低
  5. eppoll直接通过epoll_wait传入的时间结构数组向用户返回就绪的时间信息,可以直接告诉用户哪些描述符就绪,不要要用户进行空遍历查找

缺点:
无法跨平台

epoll 的两种工作模式

水平触发和边缘触发

举个栗子:

假如你正在打游戏,这个时候你妈把饭做好了,你妈喊你吃饭;

喊了一次,你没理,你继续打游戏,你妈过一会还会继续喊你。—水平触发

喊了一次,你没理,你妈就不会喊你了。—边缘触发

水平触发

  • 当 epoll 检测到 socket 上事件就绪的时候,可以不立即处理或者只处理一部分。
  • 比如来了 10K 的数据,你只读了 1K,那么过一会还会继续通知你 socket 就绪,直到你把 10K 数据读完为止。

边缘触发

  • 当 epoll 检测到 socket 就绪,必须立刻处理。
  • 如果来了 10K 数据,你不处理或者只处理一部分,那么 epoll 也不会再继续通知你。也就是说,当epoll 通知你就绪后,你只有一次机会来处理。
### IO多路复用的概念 IO多路复用是一种同步I/O模型,允许一个线程同时监控多个文件描述符(如套接字)。这种技术使得程序可以在等待多个输入源中的任何一个变为可用时不会被阻塞。当任意一个文件描述符准备好了读取或写入操作,则会立即通知应用程序去处理相应的工作[^1]。 ### 实现原理 在Linux系统下,可以通过`select`, `poll` 或者更高效的`epoll` 来实现这一功能。这些函数可以让进程一次性监视多个文件描述符,并告知哪些已经准备好进行通信活动。具体来说: - **Select**: 可以监听一定数量的文件描述符集合,在指定时间内检查它们是否有待处理的数据。 - **Poll**: 类似于`select`但是没有最大文件数目的限制,并且性能更好一些因为不需要每次调用都重新构建文件列表。 - **Epoll**: 是一种更为先进的接口,特别适合大量并发连接的情况。它可以注册感兴趣的事件并只报告那些确实发生了变化的对象,从而减少了不必要的上下文切换和资源消耗[^4]。 对于每一个可能发生变化的状态——比如可读、可写或是异常情况发生——都可以设置回调函数以便及时响应。这种方式不仅提高了效率而且简化了编程逻辑。 ### 应用场景 在网络服务端开发领域广泛应用着IO多路复用技术,尤其是在高负载情况下需要保持大量的活跃TCP连接时表现尤为突出。例如Web服务器(Nginx), 缓存数据库(Redis) 都采用了类似的架构来优化其性能[^5]。 #### Nginx 和 Redis 的例子 这两个软件均采用Reactor模式下的IO多路复用来支持大规模并发请求: - 对于Nginx而言, 主要负责接收新到来的HTTP请求(`accept`)而具体的业务逻辑则由工作进程中完成. - 而像Redis这样的键值存储系统则是完全依赖单个工作循环就能高效地应对成千上万的同时在线客户端. ```python import select import socket server_socket = socket.socket() server_socket.bind(('localhost', 8080)) server_socket.listen(5) inputs = [server_socket] while True: readable, writable, exceptional = select.select(inputs,[],[]) for s in readable: if s is server_socket: client_socket, addr = s.accept() inputs.append(client_socket) else: data = s.recv(1024) if not data: inputs.remove(s) s.close() else: print(f"Received {data.decode()}") ```
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值