本篇文章所讲主要内容:
1、epoll系统调用介绍
2、 epoll系统调用的优缺点
一、epoll介绍
从面试的角度来说,对于现在的大厂面试来说,网络编程中的NIO模块一般是必须问的。目前java项目一般都是部署在Linux平台之上,linux内核提供的epoll系统调用无疑是多路复用最优的实现方式,对NIO的实现提供了基础。
epoll是Linux特有的I/O复用函数。他在实现上与select和poll有很大的差异。首先epoll使用一组函数来完成任务。其次,epoll把用户所关心的文件描述符上的事件放在内核的一个事件表中,从而无须像select和poll把用户关心的事件每次调用的时候重复传送。但是epoll需要一个额外的文件描述符来唯一标识内核中的这个事件表。
下面分为三个步骤来epoll的应用过程:
1、创建内核事件表
内核事件表是用来存储用户所关心的文件描述符。这个事件表的创建将使用epoll组合函数中的第一个epoll_create来创建,
函数如下:
int epoll_create(int size);
size:给内核一个提示,告诉它时间表需要多大
返回值:指向内核中的时间表,该函数返回的描述符将用作其他函数调用的第一个参数。
2、操作用户感兴趣的事件
主要用来操作内核事件表的事件,可以对用户感兴趣的事件进行注册、修改和删除。
函数如下:
int epoll_ctl(int epfd , int op , int fd , struct epoll_event *event);
epfd:为内核事件表的引用地址
fd:要操作的文件描述符
op:操作的类型。操作类型有三种,注册、修改和删除。
event:指定的事件,里面包含用户感兴趣的事件以及携带的用户数据。它的结构类型为epoll_event。
返回值:成功为0,失败为-1,
epoll_event的结构为:
struct epoll_event{ _uint32_t events; //epoll事件,epoll支持的事件基本上和poll事件基本相同。对事件集合操作的方法和poll类似,在这里就不多介绍了。 epoll_data_t data; //用户数据 }
data用于存储用户数据其数据结构如下:
typedef union epoll_data{ void *ptr; int fd; uint32_t u32 ; unint64_t u64 ; } epoll_data_t
epoll_data_t是一个联合体,当描述符fd稍后成为就绪态时,联合体的成员可以用来指定传回给调用进程的信息。
3、获取已经就绪的事件
可以实现阻塞、非阻塞和阻塞一段时间的方式等待内核事件表的就绪。获取就绪的事件之后拷贝到用户空间,进行业务逻辑的处理。
最后咱们来分析一下epoll_wait函数:主要用来在一段超时时间内等待一组文件描述符上的事件。
函数如下:
int epoll_ctl(int epfd,,struct epoll_event *events,int maxevents,int timeout);
epfd:为内核事件表的引用地址
events:已经就绪的事件列表,也就是将所有就绪的文件从内核事件表中复制到events中,这个数组只会返回已经就绪的事件。
maxevents :最多监听的事件数量
timeout:与poll和select函数作用相同,
判断描述符就绪的条件与select和poll相同。但是对于描述符就绪之后的通知方式,epoll支持水平触发模式和边缘触发两种模式,默认采用的是水平触发模式。对于两种模式的介绍在文章:I/O多路复用之select系统调用 中我们已经分析。
通过下面一个例子我们来分析一下两种模式之间的区别:
当通过两种模式监控一个套接字的输入,接下来会发生如下事件。
1、套接字有输入到来。
2、我们调用一次epoll_wait()。无论我们采用的是水平触发还是边缘触发,该调用都会告诉我们该套接字已经就绪。
3、再次调用epoll_wait()。
如果我们采用的是水平触发,第二次调用epoll_wait(),还会返回该套接字处于就绪状态。如果我们采用边缘触发,第二次调用将会阻塞,因为自从上次调用以来没有新的输入。
我们知道边缘触发模式要配合非阻塞I/O的模式进行使用,所以java NIO开发中经常会看到将Channel设置为非阻塞模式。通分析java nio中的Selector源码发现在注册和修改事件信息的api中没有提供关于选择采用水平触发模式和边缘触发模式的API,应该在JVM的实现中指定了触发模式(待确认)。
二、epoll系统调用的优缺点
优点:
1、每次调用select()和poll(),内核都会检查一遍所传递的所有文件描述符。而对于epoll(),通过epoll_ctl指定了需要监视的文件描述符,内核会在内核事件表中记录此感兴趣的事件,不需要每次都进行传递。
2、每次调用select()和poll()之后都会将结果设置到所传入的文件描述符集合中,并将集合再拷贝到用户空间。对于epoll()则只是将就绪的事件放入到就绪列表中,由epoll_wait()函数进行调用返回,极大的提升了性能。
3、每次调用select()都会需要初始化输入数据,并且对于入参的文件描述符的数量有限制,在Linux平台上不能超过1024 个。epoll()没有这种限制。
4、每次调用select()和poll()之后,返回值是一个集合,里面包含就绪和未就绪的文件描述符,需要用户程序自己来循环判断哪些文件描述符已经就绪。而epoll()则会直接返回所有已经就绪的文件描述符。
5、对于select()和poll(),随着所要监控的描述符数量的增多,性能会不断的下降。而epoll则会下降的少。
下面图片为三个函数监控文件描述符的对比:
图片来自《UNIX系统编程手册 下》p1121
缺点:
1、Linux专有,可移植性差
2、需要多个函数来共同支持,并且需要维护事件表来维护事件
参考书籍:《UNIX系统编程手册 下》、《UNIX网络编程 卷一》和《Linux高性能服务器》