IO 多路复用:select、poll、epoll
select,poll,epoll都是IO多路复用的机制。**I/O多路复用就是通过一种机制,可以监视多个描述符**,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。但
select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。
IO 多路复用之 select
使用 select 可以实现同时处理多个网络连接的 IO 请求。其基本原理是程序调用 select 后,程序进入阻塞状态,此时 kernel(内核)会轮询检查所有 select 负责的文件描述符 fd。当找到其中某个文件描述符的数据已准备好时,select 返回该文件描述符并通知系统调用,将数据从内核复制到进程的缓存区。
缺点:
- 进程可以打开的 
fd有限制(32位机1024个,64位2048个),因为fd存储在一个固定大小的数组中。 - 对 
socket进行扫描是线性扫描,即采用轮询方法,效率较低。 - 用户空间和内核空间之间复制数据非常消耗资源。
 
IO 多路复用之 poll
poll 的基本原理与 select 非常类似,但 poll 使用链表来存储文件描述符(fd),并且不会修改文件描述符。相比于 select,poll 提供了更多的事件类型,对文件描述符的重复利用率也更高。与 select 不同,poll 没有最大文件描述符数量的限制。
虽然 poll 和 select 的机制类似,都是通过轮询来管理多个描述符,根据描述符的状态进行处理,但它们都存在一个缺点:包含大量文件描述符的数组会整体复制于用户态和内核的地址空间之间,无论这些文件描述符是否就绪,这样的开销会随着文件描述符数量的增加而线性增大。
IO 多路复用之 epoll
epoll 提供的三个主要函数
int epoll_create1(int flags):- 用于创建一个 
epoll实例,并返回一个文件描述符。 flags参数是一个位掩码,可以是0或者EPOLL_CLOEXEC。如果使用EPOLL_CLOEXEC标志,则epoll实例会在exec调用时自动关闭。- 如果成功,返回一个非负整数作为 
epoll实例的文件描述符;失败时返回-1,并设置errno。 
- 用于创建一个 
 int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event):- 用于向 
epoll实例中添加、修改或删除事件。 epfd是epoll实例的文件描述符;op是操作类型,可以是EPOLL_CTL_ADD(添加事件)、EPOLL_CTL_MOD(修改事件)或EPOLL_CTL_DEL(删除事件);fd是要操作的文件描述符;event是指向epoll_event结构体的指针,包含要关注的事件类型和相关数据。- 如果成功,返回 
0;失败时返回-1,并设置errno。 
- 用于向 
 int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout):- 用于等待事件的发生。
 epfd是epoll实例的文件描述符;events是一个数组,用于存储发生的事件;maxevents是events数组的大小,表示最多可以存储多少个事件;timeout是超时时间,单位为毫秒,传入-1表示阻塞直到有事件发生。- 函数会阻塞,直到有事件发生或超时。如果有事件发生,则将事件存储在 
events数组中,并返回发生事件的数量;如果超时,则返回0;如果出错,则返回-1,并设置errno。 
epoll 的优势
文件描述符数量没有限制
epoll对文件描述符的数量没有限制,因此最大数量与系统能够打开的文件描述符数量有关。
不需要每次调用都从用户空间复制到内核
epoll不再需要每次调用都将fd_set复制到内核,这减少了内核与用户空间之间的数据传输开销。
被动触发机制
- 与 
select和poll的主动轮询不同,epoll采用被动触发的方式。为每个文件描述符注册相应的事件和回调函数,当数据准备好后,就会将就绪的文件描述符加入到就绪队列中。epoll_wait的工作方式是查看这个就绪队列中是否有就绪的文件描述符,如果有,就唤醒等待者并调用回调函数。 
- 与 
 精准的文件描述符就绪通知
select和poll只能通知有文件描述符已经就绪,但无法知道具体哪个文件描述符就绪,因此需要主动轮询找到就绪的文件描述符。而epoll可以直接知道就绪的文件描述符编号,避免了不必要的轮询,提高了效率。
select、poll 和 epoll 的比较
select 和 poll 的缺点:
- 单个进程能够监视的文件描述符的数量存在最大限制,通常是 
1024,当然可以更改数量,但由于select采用轮询的方式扫描文件描述符,文件描述符数量越多,性能越差;在 linux 内核头文件中,有这样的定义:#define __FD_SETSIZE 1024 内核/用户空间内存拷贝问题,select需要复制大量的句柄数据结构,产生巨大的开销select返回的是含有整个句柄的数组,应用程序需要遍历整个数组才能发现哪些句柄发生了事件select的触发方式是水平触发,应用程序如果没有完成对一个已经就绪的文件描述符进行IO操作,那么之后每次select调用还是会将这些文件描述符通知进程
相比 select 模型,poll 使用链表保存文件描述符,因此没有了监视文件数量的限制,但其他三个缺点依然存在。
以 select 模型为例,假设服务器需要支持 100 万的并发连接,则在 __FD_SETSIZE 为 1024 的情况 下,至少需要开辟 1k 个进程才能实现 100 万的并发连接。除了进程间上下文切换的时间消耗外,从内核/用户空间大量的句柄结构内存拷贝、数组轮询等,是系统难以承受的。因此,基于 select 模型的服务器程序,要达到 100 万级别的并发访问,是一个很难完成的任务。
epoll 原理以及优势
epoll 的实现机制与 select/poll 机制完全不同,它们的缺点在 epoll 上不复存在。
设想一下如下场景:有 100 万个客户端同时与一个服务器进程保持着 TCP 连接。而每一时刻,通常只有几百上千个 TCP 连接是活跃的(事实上大部分场景都是这种情况)。如何实现这样的高并发?
在 select/poll 时代,服务器进程每次都把这 100 万个连接告诉操作系统(从用户态复制句柄数据结构到内核态),让操作系统内核去查询这些套接字上是否有事件发生,轮询完成后,再将句柄数据复制到用户态,让服务器应用程序轮询处理已发生的网络事件,这一过程资源消耗较大,因此,select/poll 一般只能处理几千的并发连接。
epoll 的设计和实现与 select 完全不同。epoll 通过在 Linux 内核中申请一个简易的文件系统(文件系统一般用 B+ 树实现,磁盘 IO 消耗低,效率很高)。把原先的 select/poll 调用分成以下 3 个部分:
- 调用 
epoll_create()建立一个epoll对象(在epoll文件系统中为这个句柄对象分配资源) - 调用 
epoll_ctl向epoll对象中添加这 100 万个连接的套接字 - 调用 
epoll_wait收集发生的事件的fd资源 
如此一来,要实现上面说是的场景,只需要在进程启动时建立一个 epoll 对象,然后在需要的时候向这个 epoll 对象中添加或者删除事件。同时,epoll_wait 的效率也非常高,因为调用 epoll_wait 时,并没有向操作系统复制这 100 万个连接的句柄数据,内核也不需要去遍历全部的连接。
epoll_create 在内核上创建的 eventpoll 结构如下:
1  | struct eventpoll {  | 
-------------本文结束感谢您的阅读-------------
本文链接: http://corner430.github.io/2024/07/21/I-O-%E5%A4%9A%E8%B7%AF%E5%A4%8D%E7%94%A8/
版权声明: 本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。转载请注明出处!