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 国际许可协议 进行许可。转载请注明出处!
