select、poll、epoll之间的区别
前言
在之前的博客中介绍了五种I/O模型(阻塞,非阻塞,多路复用,信号,异步)。https://blog.youkuaiyun.com/lyn_00/article/details/84780288
select、poll和epoll是三种实现I/O的机制,本质上他们都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的。
下面我们来深入探讨他们。
select
select
函数监视的文件描述符分3类,分别是writefds、readfds、和exceptfds。
原理:
进程调用select
函数会被阻塞,然后内核会轮询检查select
负责的fd,直到有文件描述符就绪(或超时timeout),select
函数就会返回。若未超时,当select
函数返回后,可以通过调用FD_ISSET
,遍历fdset
,来找到就绪的描述符后进程被唤醒,处理结束后又继续轮询检查。
FD_ZERO(fd_set *fdset); //将set清零使集合中不含任何fd
FD_SET(int fd, fd_set *fdset); //将fd加入set集合
FD_CLR(int fd, fd_set *fdset); //将fd从set集合中清除
FD_ISSET(int fd, fd_set *fdset); //检测fd是否在set集合中,不在则返回0
//成功时返回fd的总数,错误时返回SOCKET_ERROR
int select(int nfds, fd_set* readset, fd_set* writeset, fe_set* exceptset, struct timeval* timeout);
参数:
- nfds 需要检查的文件描述字个数
- readset 用来检查可读性的一组文件描述字。
- writeset 用来检查可写性的一组文件描述字。
- exceptset 用来检查是否有异常条件出现的文件描述字。(注:错误不包括在异常条件之内)
- timeout 超时,填NULL为阻塞,填0为非阻塞,其他为一段超时时间
如下如所示:
select 的缺点
select本质上是通过设置或者轮询检查存放fd标志位的数据结构(fdset
)来进行下一步处理,这样做带来了一些缺点:
- 单个进程可监视的fd数量是有限的。
一般来说这个数目和系统内存关系很大,具体数目可以cat /proc/sys/fs/file-max
察看。
32位机默认是1024个。64位机默认是2048。 - 采用轮询的方法,效率较低。
每一次调用select
都需要将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态。
当监听的fd数量较多但活跃数(就绪)较少时,select
仍然需要遍历检查FD_SETSIZE
个fd,他的时间复杂度为O(n)。 - 需要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大
poll
poll
有一个“水平触发”的特点,如果报告了fd后,没有被处理,那么下次poll时会再次报告该fd。
poll
和select
在本质上没有什么区别,但他解决了select
对监听数量有限制这一缺点,在使用poll
时,可以传入一个自定义大小的结构体数组,不再受限制。但仍存在问题。
- 仍然采用轮询的方式进行fd检查,每次调用都会将数组传入内核空间,不论其是否有意义
epoll
在linux上,2.4内核前主要是select
和poll
,自Linux 2.6内核正式引入epoll
以来,epoll
已经成为了目前实现高性能网络服务器的必备技术。
与前两种方式相比,epoll
并不是采用轮询的方式监听事件,而是触发式。
我们先了解一下epoll
的函数再讲解它的内部机制。
epoll
的三个函数:
新建一个epoll对象
//返回一个fd
int epoll_create(int size);
添加、删除或修改所有待监控的连接
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
参数:
- epfd :epoll_create()的返回值
- op:表示动作,用三个宏来表示:
EPOLL_CTL_ADD:注册新的fd到epfd中;
EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
EPOLL_CTL_DEL:从epfd中删除一个fd; - fd:是需要监听的fd
- *event:是告诉内核需要监听什么事,struct epoll_event结构如下:
typedef union epoll_data {
void *ptr;
int fd;
__uint32_t u32;
__uint64_t u64;
} epoll_data_t;
struct epoll_event {
__uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
events可以是以下几个宏的集合:
EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
EPOLLOUT:表示对应的文件描述符可以写;
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
等待事件的产生
//该函数返回需要处理的事件数目,如返回0表示已超时。
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
类似于select()调用。
- events用来从内核得到事件的集合
- maxevents告之内核这个events有多大,这个 maxevents的值不能大于创建epoll_create()时的size
- timeout是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)。
epoll的内部机制
epoll
的实现重点有几个关键词红黑树
,回调函数
,rdlist
-
调用
epoll_create
会返回一个文件描述符fd,里面创建好了用于监听事件的红黑树和用于存放就绪事件的双向链表rdlist
。 -
在调用
epoll_ctl
时,会将监听事件存放到红黑树(红黑树本身插入和删除时间复杂度为LogN,效率是很高的)中,并且与网络适配器绑定一个回调函数。 -
当监听的事件发生时,将调用之前绑定的回调函数,这个回调函数的作用是将该事件存放到
rdlist
中去。 -
调用
epoll_wait
函数,去查找rdlist
中的就绪事件,存放在传入的参数events中,并返回就绪事件的数量。
epoll的两种模式
- 水平触发(LT level triggered)
LT模式是epoll
的缺省模式,在该模式下,如果某个事件就绪了,但是并没有被处理,下一次内核还是会继续通知你,poll
就是这种模式的一个代表。 - 边缘触发(ET edge triggered)
ET模式是一种非常高速的工作方式,它与LT的区别在于,如果第一次通知了用户事件就绪,不论该事件是否被处理,都不会再次通知了,也就是调用epoll_wait
无法再获取到该事件。
在使用边缘触发时,建议使用非阻塞I/O,如果使用阻塞I/O可能会造成永久阻塞。
总结
以上简单介绍了一下select
,poll
,epoll
,在介绍的过程中也简述了他们之间的差别
相对于epoll
的触发式,select、poll
的轮询方式效率低下,但是每种机制都有自己的适用场景,epoll
并不是在所有场景下都是最好的。
试想这样一个场景,select
和epoll
监听的事件数量一样,这些事件大都处于活跃状态,select
只要通过一次循环就可以找到他们,但是epoll
则会频繁地调用回调函数,这样做的效率必定是低于一次循环的效率的。
epoll
并不适用于活跃事件较多的情况。