I/O复用详解
I/O复用
简介
I/O多路复用(I/O multiplexing)是一种高效的I/O处理技术,它允许单个线程或进程同时监视多个文件描述符的状态,以确定哪些文件描述符已准备好进行I/O操作。
处理场景
- 客户端程序要同时处理多个socket,非阻塞的connect。
- 客户端程序要同时处理用户输入和网络连接。比如本章将要讨 论的聊天室程序。
- TCP服务器要同时处理监听socket和连接socket。
- 服务器要同时处理TCP请求和UDP请求。比如本章将要讨论的 回射服务器。
- 服务器要同时监听多个端口,或者处理多种服务。
IO多路复用技术是为了解决高并发场景下的资源利用问题。IO多路复用通过内核级别的机制,如select、poll、epoll等,能够在一个线程或进程中同时监控多个IO通道的状态,仅在有事件发生时才进行相应的IO操作。
I/O复用是阻塞I/O
I/O复用虽然能同时监听多个文件描述符,但它本身是阻塞的。并且当多个文件描述符同时就绪时,如果不采取额外的措施,程序就只能按顺序依次处理其中的每一个文件描述符,这使得服务器程序看起来像是串行工作的。如果要实现并发,只能使用多进程或多线程等编程手段。
select
简介
select 通过在用户态和内核态之间拷贝文件描述符集合来实现监视功能,这种设计使得它能够检测哪些文件描述符已经就绪可以进行IO操作。
API
1.select
1 | int select(int nfds, fd_set *readfds,fd_set *writefds, |
功能:监听文件描述符集合内的fd。
参数:
nfds, Linux 下 socket 也称 fd,这个参数的值设置成所有需要使用 select 函数检测事件的描述符中的最大 fd 值加 1readfds,需要监听可读事件的 fd 集合writefds,需要监听可写事件的 fd 集合exceptfds,需要监听异常事件 fd 集合
1 | typedef struct |
文件描述符集合的实现实际上就是采用的位图,这是一种算法思想,在《编程珠玑》一书中有所提及,感兴趣的读者也可上网搜索。
__fds_bits是long int型数组,long int占 8 个字节,每个字节 8 bit,每个 bit 对应一个 fd 的事件状态,0 表示无事件,1 表示有事件,数组长度是 16,因此一共可以表示8 * 8 * 16 = 1024个 fd 的状态,可以将其视作长度为1024的bit数组, 1024是select函数支持的最大 fd 数量。
timeout,超时时间,即在这个参数设定的时间内检测这些 fd 的事件,超过这个时间后select函数将立即返回。
1 |
|
select 函数的总超时时间是 timeout->tv_sec 和 timeout->tv_usec 之和, 前者的时间单位是秒,后者的时间单位是微秒。
返回值:将所有的文件描述符集合清空
- 成功:返回就绪描述符总数,如果在超时时间内没有任何文件描述符就绪,
select将返回0。 - 失败:返回-1,并设置
errno为EINTR。
在 select IO复用中主要使用四个宏操作来完成,如下图所示。
2.FD_SET
1 | void FD_SET(int fd, fd_set *set); |
功能:将一个 fd 添加到 fd_set 这个集合中。
参数:
fd:需要添加的文件描述符set:文件描述符集合
返回值:无
原理实现:
1 |
3.FD_CLR
1 | void FD_CLR(int fd, fd_set *set); |
功能:从文件描述符集合中移除指定文件描述符,从 fd_set 上删除一个 fd,即对应的 bit 位置 0。
参数:
fd:需要添加的文件描述符set:文件描述符集合
原理实现:
1 |
4.FD_ZERO
1 | void FD_ZERO(fd_set *set); |
功能:清空文件描述符集合,fd_set 中所有的 fd 都清掉,即将所有 bit 位置 0。
参数:
fd:需要添加的文件描述符set:文件描述符集合
返回值:无
5.FD_ISSET
1 | int FD_ISSET(int fd, fd_set *set); |
功能:检测对应的 bit 上是否置位,判断指定文件描述符是否可读。
参数:
fd:需要添加的文件描述符set:文件描述符集合
返回值:无
原理实现:
1 |
实例
1 |
|
select的特点
优点
- 可扩展性:select可以同时监视多个文件描述符(通常是套接字),这使得程序能够同时处理多个连接,提高了系统的吞吐量和效率。
- 非阻塞性:select(本身是阻塞的)允许程序在等待I/O操作完成时继续执行其他任务,而不是被阻塞。这意味着程序可以在等待数据到达时处理其他请求或执行其他任务,提高了系统的并发性能。
缺点
- 文件描述符限制:select默认的最大文件描述符数为1024,这在处理大规模并发连接时有所限制。当出现
fd = 1500这样的描述符时,会出现错误。 - 性能开销:每次调用select都需要将文件描述符集合从用户态拷贝到内核态,并且需要在内核中遍历所有文件描述符,这在文件描述符数量较多时会带来很大的性能开销。
- 使用不便:在每次调用之后,就绪的文件描述符需要从集合中移除,这意味着每次循环都需要重新设置这些集合。
- 轮询效率低:select通过不断轮询来检测就绪的文件描述符,这种方式在文件描述符数量较多且大多数未就绪时效率较低。
poll
简介
select有一个致命的缺陷就是文件描述符大小受限,其默认最大值为1024,因此出现了 poll 技术用于突破限制。
poll 通过维护一个文件描述符集合来检查各个文件描述符的状态,从而有效管理多个IO流。这种机制允许单个线程或进程同时处理多个IO操作,提高了系统的性能和资源利用率。
poll 使用一个结构体数组来作为监听集合,这个数组中的每一个元素都记录了一个文件描述符及其对应的事件。
1 | struct pollfd { |
当调用poll函数时,系统会遍历这个数组,检查每个文件描述符是否有相应的事件发生。如果有,poll函数将返回就绪的文件描述符数量及具体哪些描述符就绪;如果没有,系统将根据设定的超时时间决定是继续等待还是返回超时信息。
poll事件
| 事件宏 | 事件描述 | 是否可以作为输入(events) | 是否可以作为输出(revents) |
|---|---|---|---|
| POLLIN | 数据可读(包括普通数据&优先数据) | 是 | 是 |
| POLLOUT | 数据可写(普通数据&优先数据) | 是 | 是 |
| POLLRDNORM | 等同于 POLLIN | 是 | 是 |
| POLLRDBAND | 优先级带数据可读(一般用于 Linux 系统) | 是 | 是 |
| POLLPRI | 高优先级数据可读,例如 TCP 带外数据 | 是 | 是 |
| POLLWRNORM | 等同于 POLLOUT | 是 | 是 |
| POLLWRBAND | 优先级带数据可写 | 是 | 是 |
| POLLRDHUP | TCP连接被对端关闭,或者关闭了写操作,由 GNU 引入 | 是 | 是 |
| POPPHUP | 挂起 | 否 | 是 |
| POLLERR | 错误 | 否 | 是 |
| POLLNVAL | 文件描述符没有打开 | 否 | 是 |
API
1 |
|
功能:创建pollfd数组,其用于管理查找socket。
参数:
fds:指向一个结构体数组的首个元素的指针,每个数组元素都是一个struct pollfd结构,用于指定检测某个给定的 fd 的条件。nfds:参数fds结构体数组的长度,nfds_t本质是unsigned long inttimeout:表示poll函数的超时时间,单位为毫秒
返回值:
- 成功:返回就绪描述符总数,如果在超时时间内没有任何文件描述符就绪,
poll将返回0。 - 失败:返回-1,并设置
errno为EINTR。
实例
1 |
|
poll的特点
优点
- 相比于
select,poll在处理大数目的文件描述符的时候速度更快; poll没有最大连接数的限制,原因是它是基于链表来存储的;- 在调用
poll函数时,只需要对参数进行一次设置就好了。
缺点
- 在调用
poll函数时,不管有没有有意义,大量的 fd 的数组被整体在用户态和内核地址空间之间复制; - 与
select函数一样,poll 函数返回后,需要遍历 fd 集合来获取就绪的 fd,这样会使性能下降; - 同时连接的大量客户端在一时刻可能只有很少的就绪状态,因此随着监视的描述符数量的增长,其效率也会线性下降。
- 时间精度上没有
select精确。
epoll
简介
epoll是Linux内核为处理大批量文件描述符而作了改进的poll,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。
epoll理解为event poll,是一种事件驱动的I/O模型,可以用来代替select和poll模型。
epoll的优势在于它可以同时处理大量的文件描述符,而且不会随着文件描述符数量的增加而降低效率。
API
epoll主要使用三个api分别为:
1.epoll_create
1 |
|
功能:创建epoll文件
参数:
size:从 Linux 2.6.8 开始,参数 size 被自动忽略,但是该值仍需要一个大于 0 的整数。
返回值:
- 成功:返回epoll文件描述符
- 失败:返回-1,并设置
errno值
2.epoll_ctrl
1 |
|
功能:增加,删除,修改epoll事件,epoll事件会存储于内核epoll结构体红黑树中。
参数:
epdf:epoll文件描述符op:操作码,表示监听描述符控制的动作。EPOLL_CTL_ADD:插入事件EPOLL_CTL_DEL:删除事件EPOLL_CTL_MOD:修改事件
fd:需要监听的文件描述符event:告诉内核需要监听的事件,struct epoll_event为epoll事件结构体。
返回值:
成功:返回0。
失败:返回-1,并设置
errno。
epoll_event结构体
1 | struct epoll_event { |
epoll事件列表EPOLL_EVENTS:
1 | enum EPOLL_EVENTS |
3.epoll_wait
1 | int epoll_wait(int epfd, struct epoll_event *event, |
功能:监听epoll事件
参数:
epfd:epoll文件描述符events:epoll事件数组maxevents:epoll事件数组长度timeout:超时时间- 小于0:一直等待
- 等于0:立即返回
- 大于0:等待超时时间返回,单位毫秒
返回值:
- 成功:返回就绪事件个数,返回0表示超时还没有事件就绪
- 失败:返回-1,并设置
errno
epoll实现原理
相关数据结构
- socket等待队列(
sk_wq):epoll_ctl添加socket后,其socket的sk_wq中会存储一个epoll等待项,并注册回调函数,当数据到达时,中断程序会调用回调函数。- 每一个socket都有一个
sk_wq队列,这个队列中会存储epoll、poll、select等队列等待项。
- 每一个socket都有一个
- eventpoll等待队列(
wq):用于阻塞当前进程,用于epoll_wait未检测到就绪epoll事件节点的情况。- 阻塞进程:调用
epoll_wait时,rdllist没有数据,创建一个等待项并将其添加到wq中并阻塞当前进程。 - 唤醒进程:软中断数据就绪的时候会通过
wq来找到阻塞在epoll对象上的用户进程,并将其唤醒。
- 阻塞进程:调用
- 就绪队列(
rdllist):就绪队列用于存储就绪epoll事件节点,用户通过epoll_wait函数获取就绪epoll事件节点。当有的连接就绪的时候,内核会把就绪的连接放到rdllist链表里。- 优点:应用进程只需要判断链表就能找出就绪连接,而不用去遍历整颗红黑树树。
- 红黑树(
rbr) :eventpoll内部使用了一棵红黑树用于存储通过epoll_ctl函数注册的epoll事件节点(epitem),可以处理海量连接的高效修改、插入和删除。- 注意:红黑树只用于管理节点,它并不参与数据接收的过程,相对与
select、poll轮询所有的描述符集合更高效。
- 注意:红黑树只用于管理节点,它并不参与数据接收的过程,相对与
epoll_create创建eventpoll对象
当调用epoll_create时候,内核会创建一个 struct eventpoll 的内核对象。并同样把它关联到当前进程的已打开文件列表中。
1 | // file:fs/eventpoll.c |
epoll_ctl 添加 socket
只考虑 EPOLL_CTL_ADD 添加 socket,不考虑删除与修改的情况。
假设我们现在和客户端们的多个连接的 socket 都创建好了,也创建好了 epoll 内核对象。在使用 epoll_ctl 注册每一个 socket 的时候,内核会做如下三件事情:
分配一个红黑树节点对象
epitem添加等待事件到 socket等待队列中,注册回调函数为
ep_poll_callback- 将
epitem插入到epoll对象的红黑树里
epoll_wait等待接收
epoll_wait 被调⽤时它观察 eventpoll->rdllist 链表⾥有没有数据即可。有数据就返回,没有数据就创建⼀个等待队列项,将其添加到 eventpoll 的等待队列上,然后把⾃⼰阻塞掉就完事。流程如下:
- 判断就绪队列上有没有事件就绪,有则返回epoll事件,反之执行步骤2。
- 定义等待事件并关联当前进程(
current),添加到等待队列(eventpoll->wq) - 让出CPU 主动进⼊睡眠状态
数据到达过程
- 查找就绪回调函数,当 socket 上数据就绪时候,找到
epoll_ctl添加 socket 时在其上设置的回调函数ep_poll_callback。 - 执⾏ socket 就绪回调函数,软中断调用回调函数,
ep_poll_callback根据等待任务队列项上的额外的base指针可以找到epitem, 进⽽也可以找到eventpoll对象。- 把⾃⼰的
epitem添加到 epoll 的就绪队列中。 - 查看
eventpoll对象上的等待队列⾥是否有等待项,如果有等待项,那就查找到等待项⾥设置的回调函数,并调用。
- 把⾃⼰的
- 执⾏ epoll 就绪通知,在
default_wake_function中找到等待队列项⾥的进程描述符,然后唤醒。当进程醒来后,继续从epoll_wait时暂停的代码继续执⾏。把rdllist中就绪的事件返回给⽤户进程。
总结
- ⽤户进程内核态。进⾏调⽤
epoll_wait等函数时会将进程陷⼊内核态来执⾏。这部分 代码负责查看接收队列,以及负责把当前进程阻塞掉,让出 CPU。 - 硬软中断上下⽂。在这些组件中,将包从⽹卡接收过来进⾏处理,然后放到 socket 的 接收队列。对于 epoll 来说,再找到 socket 关联的
epitem,并把它添加到 epoll 对象 的就绪链表中。 这个时候再捎带检查⼀下 epoll 上是否有被阻塞的进程,如果有唤醒 之
epoll常见问题
1.LT模式和ET模式区别
epoll对文件描述符的操作有两种模式:LT(Level Trigger,水平触发)模式和ET(Edge Trigger,边缘触发)模式。
LT模式是默认的工作模式,如果用户在监听epoll事件,当内核有事件的时候,会拷贝给用户态事件,但是如果用户只处理了一次,那么剩下没有处理的会在下一次epoll_wait再次返回该事件。
ET模式,当内核有事件到达, 只会通知用户一次,至于用户处理还是不处理,以后将不会再通知。这样减少了拷贝过程,增加了性能。则只会在缓冲区满足特定情况下才会触发epoll_wait获取epoll事件
LT模式只不过比ET模式多执行了一个步骤,就是当epoll_wait获取完就绪队列epoll事件后,LT模式会再次将epoll事件添加到就绪队列。
LT与ET模式的优缺点:
| 模式 | 优点 | 缺点 |
|---|---|---|
| LT | 通过频繁触发保证数据的完整性 | 频繁的用户态和内核态切换消耗大量的系统资源 |
| ET | 只触发一次,减少用户态与内核态切换,减少资源消耗 | 一次触发无法保证读取的完整的数据 |
2.epoll为何高效
- eventpoll等待队列机制,当就绪队列没有epoll事件时主动让出CPU,阻塞进程,提高CPU利用率。
- socket等待队列机制,只有接收到数据时才会将epoll事件插入就绪队列,唤醒进程获取epoll事件。
- 红黑树提高epoll事件增加,删除,修改效率。
- 任务越多,进程出让CPU概率越小,进程工作效率越高,所以epoll非常适合高并发场景。
3.epoll是阻塞的吗?
当没有 IO 事件的时候,也就是当epoll_wait未检测到epoll事件时, epoll 也是会阻塞掉当前进程。这个是合理的,因为没有事情可做了占着 CPU 也没啥意义。epoll 本身是阻塞的,但⼀般会把 socket 设置成⾮阻塞。
4.socket为什么要设置为非阻塞?
epoll机制属于IO多路复用机制,这种机制的特点是一个进程处理多路IO请求,如果socket设置成阻塞模式会存在以下几个问题:
- 一个进程同一时间只能处理一个socket数据,如果socket被阻塞,那么该进程无法处理其他的socket数据,严重影响了性能。
- 阻塞的本质是进程状态和上下文的切换,频繁的阻塞会把让CPU一直处于上下文切换的状态,导致CPU瞎忙。
实例
1 |
|
总结
三种IO复用函数的比较
| 系统调用 | 最大支持文件描述符数 | 工作模式 | 内核实现与工作效率 |
|---|---|---|---|
| select | 1024 | LT | 采用轮询方式来检测就事件,算法时间复杂度为0(n) |
| poll | cat /proc/sys/fs/file-max | LT | 采用轮询方式来检测就绪,算法时间复杂度为绪事件,为O(n) |
| epoll | cat /proc/sys/fs/file-max | 支持ET | 采用回调方式来检测就绪事件,算法时间复杂度0(1) |
三个函数的事件集合区别
select:用户通过3个参数分别传入感兴趣的可读、可写及异常等事件,内核通过对这些参数的在线修改来反馈其中的就绪事件。这使得用户每次调用select都要重置这3个参数。poll:统一处理所有事件类型,因此只需一个事件集参数。
用户通过pollfd.events传入感兴趣的事件,内核通过修改pollfd.revents反馈其中就绪的事件。epoll:内核通过一个事件表直接管理用户感兴趣的所有事件。因此每次调用epoll_wait时,无须反复传入用户感兴趣的事件。
epoll_wait系统调用的参数events仅用来反馈就绪的事件
参考
- unix网络编程
- linux高性能服务器编程
- 13. Linux poll 详解
- Libevent 深入浅出
- 一篇文章让你真正搞懂epoll机制 (qq.com)
- 图解 | 深入揭秘 epoll 是如何实现 IO 多路复用的






