目录
前言
select系统调用
poll系统调用
epoll系统调用
epoll_create
epoll_ctl
epoll_wait
LT和ET模式
EPOLLONESHOT事件
epoll和select/poll的区别
事件集处理方式
实现原理和效率
其他区别
前言
在第三章中我们大概地讲解了什么是I/O复用,即:I/O复用技术即使用select,poll,epoll等系统调用,让主线程能同时监听多个文件描述符,提高服务器运行的效率。
PS: 虽然I/O复用能同时监听多个文件描述符,但是其本身是阻塞的,当主线程已经监听到多个文件描述符时,如果不采用并发技术,那么程序也只能按顺序一个一个去处理这些文件描述符。
在本章中,我们将依次介绍select,poll,epoll系统调用,基于简单且重要的原则,本章不会详细介绍select和poll,但他们依然是面试高频考点,希望读者能自行去了解一下。本章将重点介绍epoll,同时介绍epoll中两种重要的触发模式LT水平触发模式和ET边沿触发模式,这些内容是第三章中提到的是I/O处理单元中的核心内容,需要读者理解且记忆。
select系统调用
#include<sys/select.h>
int select(int nfds,fd_set* readfds, fd_set* writefds,
fd_set* exceptfds,struct timeval* timeout);
- 作用:在指定的一段时间内,轮询监听文件描述符上的可读,可写和异常等事件
- 参数:
-
- nfds:指定被监听文件描述符的总数,通常却文件描述符中的最大值+1
- readfds:可读事件对应的文件描述符的集合
- writefds:可写事件对应的文件描述符的集合
- exceptfds:异常事件对应的文件描述符的集合
- timeout:函数运行的时间
- 返回值:
-
- 成功:就绪文件描述符的总数
- 失败:-1
其中fd_set结构指针类型和struct timeval指针类型感兴趣可以自行搜索。
select系统调用中的文件描述符只有在可读,可写或异常的条件下才被计入就绪文件描述符,而这些具体条件感兴趣的读者自行搜索。
poll系统调用
#include<poll.h>
int poll(struct pollfd* fds,nfds_t nfds,int timeout);
struct pollfd{
int fd;/*文件描述符*/
short events;/*注册的事件*/
short revents;/*实际发生的事件,由内核填充*/
- 作用:在一定时间内,轮询一定数量的描述符,检测其中是否有就绪事件
- 参数:
-
- fds:pollfd结构类型的数组
-
-
- fd:文件描述符
- events:fd上有哪些事件(见下图),这些事件通过按位或传入events
- revents:内核进行修改,通知程序fd上那些事件确实发生了
-
-
- nfds:被监听的事件集合大小
-
-
- nfds_t定义:typedef unsigned long int nfds_t;
-
-
- timeout:poll运行的时间,可以理解为一个倒计时,当时间到达函数返回
-
-
- -1:永远阻塞直到某个事件发生
-
- 返回值:
-
- 成功:就绪文件描述符的总数
- 失败:-1
epoll系统调用
epoll是非常高效的I/O复用函数,它不是一个函数,而是一组函数,即
- epoll_create
- epoll_ctl
- epoll_wait
epoll把文件描述符上的事件放入一个内核事件表里,而不是像select和poll那样每次调用重复传入文件描述符,但epll需要一个额外的文件描述符来标识这个内核事件表。
epoll_create
#include<sys/epoll.h>
int epoll_create(int size);
- 作用:创建文件描述符来标识内核事件表
- 参数:size不起作用,仅仅给内核一个提示,事件表应该有多大
- 返回值:作为其他epoll函数的第一个参数
epoll_ctl
创建好了标识内核事件表的文件描述符,即可使用epoll_ctr对内核事件表进行设置
#include<sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event);
struct epoll_event
{
_uint32_t events;
epoll_data_t data;
}
- 作用:对内核事件表进行操作
- 参数:
-
- epfd:标识内核事件表
- op:操作类型
-
-
- EPOLL_CTL_ADD:往时间表中注册fd上的事件
- EPOLL_CTL_MOD:修改fd上的注册事件
- EPOLL_CTL_DEL:删除fd上的注册事件
-
-
- fd:要操作的文件描述符
- event:指定事件(结构体内如下)
-
-
- _uint32_t events : 事件类型(与poll的事件类型相同,但要前加“E")
- epoll_data_t data : 存储用户数据(联合体内如下)
-
-
-
-
- void* ptr : 指定与fd相关的用户数据
- int fd : 事件所从属的目标文件描述符(常用)
- uint32_t: u32:
- uint64_t:u64:
- (PS:联合体union即多选一使用,不能同时使用)
-
-
- 返回值:
-
- 成功:0
- 失败:-1
事件类型(poll类型的,epoll在poll类型的事件类型前面 + ”E";
epoll_wait
创建并设置好内核事件表后,即可使用epoll_wait()开始等待事件
#include<sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event* events,int maxevents,int timeout);
- 作用:在一定时间内接收就绪事件,并加入一个事件数组中等待程序处理
- 参数:
-
- epfd:内核事件表的标识符(epoll_create创建的)
- events:事件数组,将epfd中的就绪事件取出放进去
- maxevents:最多监听多少个时间
- timeout:epoll_wait等待的时间,可以理解为倒计时,时间到了返回
- 返回值
-
- 成功:接收到的事件数量
- 失败:-1
epoll_wait函数检测到事件后,会将内核注册表中的就绪事件放入第二个数组中,当工作线程来数组中取事件处理时,就不需要判断事件是否是就绪事件,而poll则需要先判断是否就绪。
可以这样理解,文件描述符上的事件是一堆未处理的食物,而I/O复用函数作用是将这些食物放入一格一格的容器中。poll容器不管食物是否烹饪过,直接放进去。而epoll会有一个大厨(内核)来将烹饪过的食物放进去。在食客挑选食物食用时(工作线程逻辑处理),如果是poll容器,则需要一个格子一个格子打开判断食物是否可以食用,而epoll容器中都是可以直接食用的食物,食客直接打开食用即可。
LT和ET模式
epoll对文件描述符的操作又两种模式,LT模式ET模式。
LT模式(水平触发):LT是默认的工作模式,这种模式epoll相当于一个高效的poll
ET模式(边沿触发):当往epoll内核事件表中注册一个文件描述符上的EPOLLET事件时,epoll会切换成ET模式
LT模式:当epoll_wait检测到文件描述符上有事件发生并通知应用程序后,应用程序可以不立即处理事件,当下一次调用epoll_wait时,epoll_wait还会再次向应用程序通告此事件,直到这个事件被处理。
ET模式:当epoll_wait检测到文件描述符上有事件发生并通知应用程序后,应用程序必须立即处理事件,后续epoll_wait不再通知
LT模式和ET模式可以这样理解,还是上面那个例子,EPOLL容器安装了一个语音呼叫装置,当容器某一格内有食物(就绪事件)时,它就会呼叫食客来取餐。LT模式下,食客可以不来用餐,这样当主线程循环一遍后又一次调用EPOLL时,它又会再次呼叫食客来取走上次未取走的那个格子里的食物。而ET模式下,EPOLL容器一呼叫,食客就必须来取餐。
EPOLLONESHOT事件
在使用epoll时可能遇到这样的问题,在某个线程读取完某个socket上的数据开始处理时,这个socket上又来了新的数据,这时另一个线程被唤醒处理这些数据,那么就出现了两个线程处理同一个socket的情况。为了避免这种情况,可以使用EPOLLONESHOT事件。
一个注册了EPOLLONESHOT事件的文件描述符,操作系统最多可以触发其上的一个事件,且只能触发一次。除非用epoll_ctl函数重置该文件描述符上的EPOLLONESHOT事件。反过来,当一个线程处理完一个socket上的事件,就该立刻重置其上的EPOLLONESHOT事件,保证下次其他线程能处理这个socket。
epoll和select/poll的区别
事件集处理方式
select:使用fd_set。
fd_set并未将文件描述符和事件绑定,所以需要三个参数来分别传入可读,可写和异常事件。且由于内核对fd_set集合为在线修改,应用程序下次调用select前需要重置三个参数
poll:使用pollfd
pollfd内定义文件描述符和事件,所有事件都统一处理;内核每次修改的时revent成员,而event成员不变,所以不用重置事件集参数。
epoll:
在内核中维护事件表,用独立系统调用epoll_ctr来控制事件表,epoll_wait调用直接从内核时间表中取得注册的事件,无需反复从用户空间读入这些事件。
实现原理和效率
select/poll:
采用轮询的方式,每次调用都要扫描整个注册文件描述符集合,并将其中就绪的文件描述符返回给用户程序,时间复杂度O(n)
epoll:
采用回调的方式,内核检测到就绪的文件描述符时触发回调函数,回调函数将文件描述符上的对应事件加入内核就绪队列,内核在合适的时机将就绪事件队列中的拷贝到用户空间。时间复杂度是O(1)
其他区别
工作模式:
select/poll只有LT模式
epoll可以使用ET模式
最大文件描述符数:
select:有最大值,且比poll和epoll少
poll/epoll:65353