news 2026/7/7 15:19:16

计算机网络知识点(五) I/O 多路复用:函数select、poll和epoll

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
计算机网络知识点(五) I/O 多路复用:函数select、poll和epoll

什么是IO多路复用?

IO即为网络I/O,多路即为多个TCP连接,复用即为共用一个线程或者进程,模型最大的优势是系统开销小,不必创建也不必维护过多的线程或进程。

IO复用(IOmultiplexing)使得内核一旦发现进程指定的一个或多个IO条件就绪(也就是说输入已准备好被读取,或者描述符已能承接更多的输出),它就通知进程。

应用场景

I/O 多路复用通常用于以下场景的网络应用:

  • 当客户端处理多个描述符时(通常是交互式输入和网络套接字)
  • 当客户端同时处理多个套接字时(这是可能的,但很少见)
  • 如果 TCP 服务器同时处理侦听套接字及其连接的套接字
  • 如果服务器同时处理 TCP 和 UDP
  • 如果服务器处理多个服务,可能还有多个协议

I/O 多路复用不限于网络编程。许多重要的应用程序发现需要这些技术。

输入/输出模型

我们首先检查 Unix 下可用的五种 I/O 模型的基本区别:

[1]blockingIO - 阻塞I/O

[2]nonblockingIO - 非阻塞I/O

[3]IOmultiplexing – I/O多路复用(select和poll)

[4]signaldrivenIO - 信号驱动I/O

[5]asynchronousIO - 异步I/O(POSIX aio函数)

输入操作通常有两个不同的阶段:

  1. 等待数据准备好。这涉及等待数据到达网络。当数据包到达时,它被复制到内核中的缓冲区中。

  2. 将数据从内核复制到进程。这意味着将(准备好的)数据从内核缓冲区复制到我们的应用程序缓冲区。

阻塞 I/O 模型

最流行的 I/O 模型是阻塞 I/O 模型(我们在前面部分的所有示例中都使用了该模型)。默认情况下,所有套接字都是阻塞的。场景如下图所示:

使用UDP而不是TCP作为例子的原因在于就UDP而言,数据准备好读取的概念比较简单:要么整个数据报已经收到,要么还没有。然而对于TCP来说,诸如套接字低水位标记( low-water mark)等额外变量开始起作用,导致这个概念变得复杂。

在这个例子中,我们把recvfrom函数视为系统调用,因为我们正在区分应用进程和内核。不论它如何实现(在源自Berkeley的内核上是作为系统调用,在System V内核上是作为调用系统调用getmsg的函数),一般都会从在应用进程空间中运行切换到在内核空间中运行,一段时间之后再切换回来。

非阻塞 I/O 模型

当套接字设置为非阻塞时,我们是在告诉内核"当我请求的 I/O 操作在不让进程进入睡眠状态的情况下无法完成时,不要让进程进入睡眠状态,而是返回一个错误"。

  • 对于前三个recvfrom,没有要返回的数据,内核立即返回一个错误EWOULDBLOCK。
  • 我们第四次调用recvfrom,数据报准备好了,它被复制到我们的应用程序缓冲区,并recvfrom成功返回。然后我们处理数据。

当应用程序处于循环调用recvfrom这样的非阻塞描述符时,它被称为轮询。应用程序不断地轮询内核以查看某些操作是否已准备好。这通常会浪费 CPU 时间,但偶尔会遇到这种模型,通常在专用于一个功能的系统上。

非阻塞IO图示

视频演示:非阻塞模式下,前三次recvfrom返回 EWOULDBLOCK 错误,第四次数据就绪后成功读取并处理。该演示展示了应用程序不断轮询内核的过程。

I/O 复用模型

有了IO复用(IO multiplexing),我们就可以调用select或poll,阻塞在这两个系统调用中的某一个之上,而不是阻塞在真正的IO系统调用上。

我们阻塞于select调用,等待数据报套接字变为可读。当select返回套接字可读这一条件时,我们调用recvfrom把所读数据报复制到应用进程缓冲区。

与阻塞型IO比较,IO复用并不显得有什么优势,事实上由于使用select需要两个而不是单个系统调用,IO复用还稍有劣势。使用select的优势在于我们可以等待多个描述符就绪。

异步 I/O 模型

异步 I/O由 POSIX 规范定义,各种标准中出现的实时功能的各种差异共同形成了当前的 POSIX 规范。

我们也可以用信号,让内核在描述符就绪时发送SIGIO信号通知我们。我们称这种模型为信号驱动式IO ( signal-driven I/O)。

我们首先开启套接字的信号驱动式I/O功能,并通过sigaction系统调用安装一个信号处理函数。该系统调用将立即返回,我们的进程继续工作,也就是说它没有被阻塞。当数据报准备好读取时,内核就为该进程产生一个sIGIo信号。我们随后既可以在信号处理函数中调用recvfrom读取数据报,并通知主循环数据已准备好待处理,也可以立即通知主循环,让它读取数据报。

无论如何处理SIGIO信号,这种模型的优势在于等待数据报到达期间进程不被阻塞。主循环可以继续执行,只要等待来自信号处理函数的通知:既可以是数据已准备好被处理,也可以是数据报已准备好被读取。

各种I/O模型的比较

对比上述5种不同的IO模型。可以看出,前4种模型的主要区别在于第一阶段,因为它们的第二阶段是一样的:在数据从内核复制到调用者的缓冲区期间,进程阻塞于recvfrom调用。相反,异步I/O模型在这两个阶段都要处理,从而不同于其他4种模型。

同步IO和异步IO对比

POSIX把这两个术语定义如下:

同步IO操作(synchronous IO opetation)导致请求进程阻塞,直到IO操作完成;

异步IO操作( asynchronous I/O opetation)不导致请求进程阻塞。

select

select 是操作系统提供的系统调用函数,select()用来等待文件描述词(普通文件、终端、伪终端、管道、FIFO、套接字及其他类型的字符型)状态的改变。是一个轮循函数,循环询问文件节点,可设置超时时间,超时时间到了就跳过代码继续往下执行。

#include<sys/select.h>#include<sys/time.h>#defineFD_SETSIZE1024#defineNFDBITS(8*sizeof(unsignedlong))#define__FDSET_LONGS(FD_SETSIZE/NFDBITS)// fd_set结构体定义(位图实现)typedefstruct{unsignedlongfds_bits[__FDSET_LONGS];}fd_set;// select函数原型intselect(intmax_fd,// 最大文件描述符+1fd_set*readset,// 读描述符集合fd_set*writeset,// 写描述符集合fd_set*exceptset,// 异常描述符集合structtimeval*timeout// 超时时间);// 返回值:就绪描述符的数目// 操作宏FD_ZERO(fd,fd_set*fds)// 清空集合FD_SET(intfd,fd_set*fds)// 将给定的描述符加入集合FD_ISSET(intfd,fd_set*fds)// 判断指定描述符是否在集合中FD_CLR(intfd,fd_set*fds)// 将给定的描述符从集合中删除

select本质上是通过设置或者检查存放fd标志位的数据结构来进行下一步处理。这样所带来的缺点是:

  1. 单个进程可监视的fd数量被限制,即能监听端口的大小有限。一般来说这个数目和系统内存关系很大,具体数目可以cat /proc/sys/fs/file-max察看。32位机默认是1024个。64位机默认是2048。

  2. 对socket进行扫描时是线性扫描,即采用轮询的方法,效率较低:当套接字比较多的时候,每次select()都要通过遍历FD_SETSIZE个Socket来完成调度,不管哪个Socket是活跃的,都遍历一遍。这会浪费很多CPU时间。

  3. 需要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大。

select效果图示

poll

相关函数定义

#include<poll.h>intpoll(structpollfd*fds,nfds_t nfds,inttimeout);#include<signal.h>#include<poll.h>intppoll(structpollfd*fds,nfds_t nfds,conststructtimespec*tmo_p,constsigset_t*sigmask);// pollfd结构体structpollfd{intfd;// 文件描述符shortevents;// 关注的事件(输入)shortrevents;// 返回的事件(输出)};

和select用三组文件描述符不同的是,poll只有一个pollfd数组,数组中的每个元素都表示一个需要监听IO操作事件的文件描述符。events参数是我们需要关心的事件,revents是所有内核监测到的事件。

poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态,如果设备就绪则在设备等待队列中加入一项并继续遍历,如果遍历完所有fd后没有发现就绪设备,则挂起当前进程,直到设备就绪或者主动超时,被唤醒后它又要再次遍历fd。这个过程经历了多次无谓的遍历。

它没有最大连接数的限制,原因是它是基于链表来存储的,但是同样有一个缺点:

  1. 大量的fd的数组被整体复制于用户态和内核地址空间之间,而不管这样的复制是不是有意义。

  2. poll还有一个特点是"水平触发",如果报告了fd后,没有被处理,那么下次poll时会再次报告该fd。

epoll

相关函数定义如下

#include<sys/epoll.h>// 创建epoll实例intepoll_create(intsize);intepoll_create1(intflags);// 注册/修改/删除监听事件intepoll_ctl(intepfd,intop,intfd,structepoll_event*event);// 等待事件就绪intepoll_wait(intepfd,structepoll_event*events,intmaxevents,inttimeout);intepoll_pwait(intepfd,structepoll_event*events,intmaxevents,inttimeout,constsigset_t*sigmask);
  • epoll有EPOLLLT和EPOLLET两种触发模式,LT是默认的模式,ET是"高速"模式。
  • LT模式下,只要这个fd还有数据可读,每次 epoll_wait都会返回它的事件,提醒用户程序去操作
  • ET模式下,它只会提示一次,直到下次再有数据流入之前都不会再提示了,无论fd中是否还有数据可读。所以在ET模式下,read一个fd的时候一定要把它的buffer读完,或者遇到EAGAIN错误。

epoll效果演示

视频演示:epoll 使用回调机制,只有活跃的文件描述符才会触发事件通知。演示了 ET(边缘触发)和 LT(水平触发)两种模式的区别,以及 epoll 的高效性。

边缘触发模式ET:

当被监控的Socket描述符上有可读事件发生时,服务器只会从 epoll_wait中苏醒一次,即使进程没有调用read函数从内核读取数据,也依然只苏醒一次,因此我们程序要保证一次性将内核缓冲区的数据读取完,只有第一次满足条件的时候才触发,之后就不会再传递同样的事件了。

水平触发模式LT:

当被监控的Socket上有可读事件发生时,服务器不断地从 epoll_wait中苏醒,直到内核缓冲区数据被read函数读完才结束,目的是告诉我们有数据,只要满足事件的条件,比如内核中有数据需要读,就一直不断地把这个事件传递给用户。

epoll的优点:

  1. 没有最大并发连接的限制,能打开的FD的上限远大于1024(1G的内存上能监听约10万个端口);

  2. 效率提升,不是轮询的方式,不会随着FD数目的增加效率下降。只有活跃可用的FD才会调用callback函数;即Epoll最大的优点就在于它只管你"活跃"的连接,而跟连接总数无关,因此在实际的网络环境中,Epoll的效率就会远远高于select和poll。

  3. 内存拷贝,利用mmap()文件映射内存加速与内核空间的消息传递;即epoll使用mmap减少复制开销。

epoll缺点:

epoll只能工作在linux下。

select和epoll的区别:

  1. select和poll采用轮询的方式检查就绪事件,每次都要扫描整个文件描述符,复杂度O(N);

  2. epoll采用回调方式检查就绪事件,只会返回有事件发生的文件描述符的个数,复杂度O(1);

  3. select 只工作在低效的LT模式,epoll可以在ET高效模式工作;

  4. epoll是Linux所特有,而select则应该是POSIX所规定,一般操作系统均有实现;

  5. select单个进程可监视的fd数量有限,即能监听端口的大小有限,64位是2048;epoll没有最大并发连接的限制,能打开的fd的上限远大于2048 (1G的内存上能监听约10万个端口);

  6. select 内核需要将消息传递到用户空间,都需要内核拷贝动作;epoll通过内核和用户空间共享一块内存来实现的。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/7 15:17:09

气象大模型又变了:2026年,真正的竞争已经不在“天气预报”

这半年,我一直在关注全球气象大模型的进展。 说实话,变化比预想中还要快。 两三年前,我们讨论的还是盘古、GraphCast、风乌、伏羲,大家关心的问题也比较直接: AI做天气预报,到底能不能超过传统数值模式? 到了2026年,这个问题其实已经没那么重要了。 因为AI气象模型…

作者头像 李华
网站建设 2026/7/7 15:16:21

如何快速备份QQ空间历史数据:GetQzonehistory完整指南

如何快速备份QQ空间历史数据&#xff1a;GetQzonehistory完整指南 【免费下载链接】GetQzonehistory 获取QQ空间发布的历史说说 项目地址: https://gitcode.com/GitHub_Trending/ge/GetQzonehistory 你是否担心QQ空间里的珍贵回忆会随着时间流逝而消失&#xff1f;GetQz…

作者头像 李华
网站建设 2026/7/7 15:12:27

EB Garamond 12:如何让16世纪经典字体在现代数字时代焕发新生

EB Garamond 12&#xff1a;如何让16世纪经典字体在现代数字时代焕发新生 【免费下载链接】EBGaramond12 项目地址: https://gitcode.com/gh_mirrors/eb/EBGaramond12 你是否正在寻找一款既有深厚历史底蕴又能完美适配现代数字排版的字体&#xff1f;EB Garamond 12正是…

作者头像 李华
网站建设 2026/7/7 15:09:54

从零开始搭建个人游戏串流服务器:Sunshine全攻略

从零开始搭建个人游戏串流服务器&#xff1a;Sunshine全攻略 【免费下载链接】Sunshine Self-hosted game stream host for Moonlight. 项目地址: https://gitcode.com/GitHub_Trending/su/Sunshine 你是否曾经梦想过在客厅的电视上玩电脑游戏&#xff0c;或者躺在床上用…

作者头像 李华
网站建设 2026/7/7 15:02:42

PIC18微控制器外部EEPROM扩展与I²C接口实践

1. 为什么需要外部存储扩展 在嵌入式系统开发中&#xff0c;存储空间往往是制约功能实现的关键因素。以PIC18LF45K50为例&#xff0c;这款8位微控制器虽然性能可靠、功耗低&#xff0c;但其内部Flash存储通常只有32KB&#xff0c;EEPROM更是仅有256字节。当项目需要记录大量数据…

作者头像 李华