【网络编程基础】I/O 多路复用(select,poll,epoll)

I/O 多路复用

I/O 多路复用(multiplexing)的本质是通过一种机制(系统内核缓冲 I/O 数据),让单个进程可以监视多个文件描述符,一旦某个描述符就绪(一般是读就绪或写就绪),能够通知程序进行相应的读写操作。

为了解决单个应用进程能同时处理多个网络连接的问题,通常采用 select、poll、epoll 作为解决方案。它们的区别主要体现在以下三个方面:

  1. 系统如何知道进程需要监控哪些连接和事件(也就是fd)。
  2. 系统知道进程需要监控的连接和事件后,采用什么方式去对fd进行状态的监控。
  3. 系统监控到活跃事件后如何通知进程。

select

应用进程通过 select 去监控多个连接(也就是fd)的机制大概如下:

  1. 在调用 select 之前告诉 select 应用进程需要监控哪些 fd 可读、可写、异常事件,并保存在 fd_set 中。
  2. 应用进程调用 select 的时候把上述 3 个事件类型的 fd_set 传给内核(产生了一次 fd_set 在用户空间到内核空间的复制),内核收到 fd_set 后对 fd_set 进行遍历,然后一个个去扫描对应 fd 是否满足可读写事件。
  3. 如果发现了有对应的 fd 存在读写事件后,内核会把 fd_set 里没有事件状态的 fd 句柄清除,然后把有事件的 fd 返回给应用进程(这里又会把更新后的 fd_set 从内核空间复制用户空间)。
  4. 最后应用进程收到了select返回的活跃事件类型的fd句柄后,再向对应的 fd 发起数据读取或者写入数据操作。

select 提供一种可以用一个进程监控多个网络连接的方式,但也还遗留了一些问题,这些问题也是后来 select 面对高并发环境的性能瓶颈

  1. 每调用一次 select 就需要 3 个事件类型的 fd_set 需从用户空间拷贝到内核空间去,返回时 select 也会把保留了活跃事件的 fd_set 返回(从内核拷贝到用户空间)。当 fd_set 数据大的时候,这个过程消耗是很大的。
  2. select 需要逐个遍历 fd_set 集合 ,然后去检查对应 fd 的可读写状态,如果 fd_set 数据量多,那么遍历 fd_set 就是一个比较耗时的过程。
  3. fd_set 是个集合类型,它的数据结构有长度限制,32位系统长度1024,62位系统长度2048,这个就限制了select 最多能同时监控 1024 个连接

poll

随着网络的高速发展,高并发的网络请求程序越来越多,吸取了 select 的教训,poll 模式不再使用数组的方式来保存自己监控的 fd 信息了。poll 模型使用链表的形式来保存自己监控的fd信息,从而没有了连接限制,可以支持高并发的请求。

select 调用返回的 fd_set 只包含了上次返回的活跃事件的 fd_set 集合,下一次调用 select 又需要把这几个 fd_set 清空,重新添加上自己感兴趣的 fd 和事件类型。poll 需要监控的 fd 信息采用的是 pollfd 的文件格式,它保存着对应 fd 需要监控的事件集合,也保存了一个返回于激活事件的 fd 集合,所以重新发请求时不需要重置感兴趣的事件类型参数。

因此 poll 通过改变存储方式,只解决了连接限制的问题,其他方面与 select 没有太大差别。

epoll

不同于 select 和 poll 的直接调用方式,epoll 采用的是一组方法调用的方式,它的工作流程大致如下:

  1. 创建内核事件表(epoll_create)。这里主要是向内核申请创建一个 fd 的文件描述符作为内核事件表(B+树结构的文件,没有数量限制),这个描述符用来保存应用进程需要监控哪些 fd 和对应类型的事件。
  2. 添加或移出监控的 fd 和事件类型(epoll_ctl)。调用此方法可以是向内核的内核事件表动态地添加和移出 fd 和对应事件类型。
  3. epoll_wait 绑定回调事件。内核向事件表的 fd 绑定一个回调函数。当监控的 fd 活跃时,会调用 callback 函数把事件加到一个活跃事件队列里;最后在 epoll_wait 返回的时候内核会把活跃事件队列里的 fd 和事件类型返回给应用进程。

从 epoll 整体思路上来看,采用事先就在内核创建一个事件监听表,后面只需要往里面添加移出对应事件。因为本身事件表就在内核空间,所以就避免了像 select、poll 一样每次都要把自己需要监听的事件列表来回传输,这也就避免了事件信息需要在用户空间和内核空间相互拷贝的问题。

然后 epoll 并不是像 select 一样去遍历事件列表,逐个轮询地监控 fd 的事件状态,而是事先就建立了 fd 与之对应的回调函数,当事件激活后主动回调 callback 函数,这也就避免了遍历事件列表的这个操作,所以 epoll 并不会像 select 和 poll 一样随着监控的 fd 变多而效率降低,这种事件机制也是 epoll 要比 select 和 poll 高效的主要原因。

水平触发 (LT 模式)

默认工作模式,对代码编写要求比较低,不容易出现问题。当 epoll_wait 检测到某描述符事件就绪并通知应用程序时,应用程序可以不立即处理该事件;下次调用 epoll_wait 时,会再次通知此事件。即只要有数据没有被获取并处理,内核就不断通知你,因此不用担心事件丢失的情况。

LT 模式效率会低于 ET 模式,尤其在大并发,大流量的情况下。

边沿触发(ET 模式)

对编程要求高,需要细致的处理每个请求,否则容易发生丢失事件的情况。当 epoll_wait 检测到某描述符事件就绪并通知应用程序时,应用程序必须立即处理该事件。如果不处理,下次调用 epoll_wait 时,不会再次通知此事件,直到做了某些操作导致该描述符变成未就绪状态了,也就是说边缘触发只在状态由未就绪变为就绪时通知一次。

ET 模式在并发、大流量的情况下,很大程度上减少了 epoll 事件的触发次数,会比 LT 模式少很多 epoll 的系统调用,因此效率比 LT 模式下高。

epoll 工作在 ET 模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读操作阻塞写操作把处理多个文件描述符的任务饿死。

在 socket 中的表现

Linux 中基于 socket 的通信本质也是一种 I/O,使用 socket() 函数创建的套接字默认都是阻塞的,这意味着当 sockets API 的调用不能立即完成时,线程一直处于等待状态,直到操作完成获得结果或者超时出错。会引起阻塞的 socket API 分为以下四种:

  • 输入操作: recv()、recvfrom()。以阻塞套接字为参数调用该函数接收数据时,如果套接字缓冲区内没有数据可读,则调用线程在数据到来前一直睡眠。
  • 输出操作: send()、sendto()。以阻塞套接字为参数调用该函数发送数据时,如果套接字缓冲区没有可用空间,线程会一直睡眠,直到有空间。
  • 接受连接:accept()。以阻塞套接字为参数调用该函数,等待接受对方的连接请求。如果此时没有连接请求,线程就会进入睡眠状态。
  • 外出连接:connect()。对于TCP连接,客户端以阻塞套接字为参数,调用该函数向服务器发起连接。该函数在收到服务器的应答前,不会返回。这意味着TCP连接总会等待至少服务器的一次往返时间。

select的设计思想很直接,假设预先传入一个socket列表,如果列表中的 socket 都没有数据,挂起进程,直到有一个 socket 收到数据,唤醒进程。通常会准备一个数组来存放所有需要监视的 socket。然后调用select,如果所有的 socket 都没有数据,那么 select 会阻塞,当任何一个socket收到数据后,中断程序将唤起进程,将进程从所有的等待队列中移除,加入到工作队列里面。用户此时再通过遍历数组,通过 FD_ISSET 判断具体哪个 socket 收到数据,然后做出处理。

select 这样的处理方式通常有两个缺点:

  1. 每次调用 select 都需要将进程加入到所有监视 socket 的等待队列,每次唤醒都需要从每个队列中移除。这里涉及了两次遍历,而且每次都要将整个 fd 列表传递给内核,有一定的开销。正是因为遍历操作开销大,出于效率的考量,才会规定select的最大监视数量,默认只能监视1024个socket。
  2. 进程被唤醒后,程序并不知道哪些 socket 收到数据,还需要遍历一次。

epoll 的实际思路则是:

  1. 功能分离

    select 低效的原因之一是将“维护等待队列”和“阻塞进程”两个步骤合二为一。每次调用select都需要这两步操作,然而大多数应用场景中,需要监视的 socket 相对固定,并不需要每次都修改。epoll 将这两个操作分开,先用 epoll_ctl 维护等待队列,再调用 epoll_wait 阻塞进程。显而易见的,效率就能得到提升。

  2. 就绪列表

    select 低效的另一个原因在于程序不知道哪些 socket 收到数据,只能一个个遍历。如果内核维护一个“就绪列表”,引用收到数据的 socket,就能避免遍历。当进程被唤醒后,只要获取 rdlist 的内容,就能够知道哪些 socket 收到数据。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值