linux系统I/O复用技术之三:epoll()
epoll
epoll是一种高效的管理socket的模型,相对于select和poll来说具有更高的效率和易用性。传统的select以及poll的效率会随socket数量的线形递增而呈二次乃至三次方的下降,而epoll的性能不会随socket数量增加而下降。标准的linux-2.4.20内核不支持epoll,需要打patch。本文主要从linux-2.4.32和linux-2.6.10两个内核版本介绍epoll。
1. 头文件
#include <sys/epoll.h>
2. 参数说明
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event event);
int epoll_wait(int epfd,struct epoll_event events,int maxevents,int timeout);
typedef union epoll_data
{
void ptr;
int fd;
__uint32_t u32;
__uint64_t u64;
} epoll_data_t;
epoll_data是一个联合体,借助于它应用程序可以保存很多类型的信息:fd、指针等等。有了它,应用程序就可以直接定位目标了。
struct epoll_event
{
__uint32_t events; / epoll events /
epoll_data_t data; / User data variable /
};
epoll_event 结构体被用于注册所感兴趣的事件和回传所发生待处理的事件,其中
epoll_data_t 联合体用来保存触发事件的某个文件描述符相关的数据,例如一个client连接到服务器,服务器通过调用accept函数可以得到于这个client对应的socket文件描述符,可以把这文件描述符赋给epoll_data的fd字段以便后面的读写操作在这个文件描述符上进行。
events字段是表示感兴趣的事件和被触发的事件。可能的取值为:
EPOLLIN : 表示对应的文件描述符可以读;
EPOLLOUT:表示对应的文件描述符可以写;
EPOLLPRI: 表示对应的文件描述符有紧急的数据可读
EPOLLERR: 表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET: 表示对应的文件描述符设定为edge模式;
3. 所用到的函数:
epoll不再是一个单独的系统调用,而是由epoll_create/epoll_ctl/epoll_wait三个系统调用组成
3.1. epoll_create函数
函数声明:int epoll_create(int size)
该函数生成一个epoll专用的文件描述符,其中的参数是指定生成描述符的最大范围。在linux-2.4.32内核中根据size大小初始化哈希表的大小,在linux2.6.10内核中该参数无用,使用红黑树管理所有的文件描述符,而不是hash。其实是申请一个内核空间,用来存放你想关注的socket fd上是否发生以及发生了什么事件。
在用完之后,记得用close()来关闭这个创建出来的epoll句柄。
3.2. epoll_ctl函数
函数声明:int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
该函数用于控制某个文件描述符上的事件,可以注册事件,修改事件,删除事件。
相对于select模型中的FD_SET和FD_CLR宏。
参数:
epfd:由 epoll_create 生成的epoll专用的文件描述符;
op:要进行的操作例如注册事件,可能的取值
EPOLL_CTL_ADD 注册、
EPOLL_CTL_MOD 修改、
EPOLL_CTL_DEL 删除
fd:关联的文件描述符;
event:指向epoll_event的指针;
返回值:如果调用成功返回0,不成功返回-1
3.3. epoll_wait函数
函数声明:int epoll_wait(int epfd,struct epoll_event events,int maxevents,int timeout)
该函数用于轮询I/O事件的发生,来查询所有的网络接口,看哪一个可以读,哪一个可以写了。相对于select模型中的select函数。
一般如果网络主循环是单独的线程的话,可以用-1来等,这样可以保证一些效率,如果是和主逻辑在同一个线程的话,则可以用0来保证主循环的效率。epoll_wait范围之后应该是一个循环,遍利所有的事件
参数:
epfd:由epoll_create 生成的epoll专用的文件描述符;
epoll_event:用于回传代处理事件的数组;
maxevents:每次能处理的事件数;
timeout:等待I/O事件发生的超时值(ms);
-1永不超时,直到有事件产生才触发,
0立即返回。
返回值:返回发生事件数。-1有错误。
4. epoll的ET模式与LT模式
ET(Edge Triggered)与LT(Level Triggered)的主要区别可以从下面的例子看出
eg:
1). 标示管道读者的文件句柄注册到epoll中;
2). 管道写者向管道中写入2KB的数据;
3). 调用epoll_wait可以获得管道读者为已就绪的文件句柄;
4). 管道读者读取1KB的数据
5). 一次epoll_wait调用完成
如果是ET模式,管道中剩余的1KB被挂起,再次调用epoll_wait,得不到管道读者的文件句柄,除非有新的数据写入管道。如果是LT模式,只要管道中有数据可读,每次调用epoll_wait都会触发。
另一点区别就是设为ET模式的文件句柄必须是非阻塞的。
5. epoll的实现
epoll的源文件在/usr/src/linux/fs/eventpoll.c,在module_init时注册一个文件系统 eventpoll_fs_type,对该文件系统提供两种操作poll和release,所以epoll_create返回的文件句柄可以被poll、 select或者被其它epoll epoll_wait。对epoll的操作主要通过三个系统调用实现:
1). sys_epoll_create
2). sys_epoll_ctl
3). sys_epoll_wait
下面结合源码讲述这三个系统调用。
1). long sys_epoll_create (int size)
sys_epoll_create(epoll_create对应的内核函数),这个函数主要是做一些准备工作,比如创建数据结构,初始化数据并最终返回一个文件描述符(表示新创建的虚拟epoll文件),这个操作可以认为是一个固定时间的操作。该系统调用主要分配文件句柄、inode以及file结构。
在linux-2.4.32内核中,使用hash保存所有注册到该epoll的文件句柄,在该系统调用中根据size大小分配hash的大小。具体为不小于size,但小于2size的2的某次方。最小为2的9次方(512),最大为2的17次方 (128 x 1024)。
在linux-2.6.10内核中,使用红黑树保存所有注册到该epoll的文件句柄,size参数未使用,只要大于零就行。
epoll是做为一个虚拟文件系统来实现的,这样做至少有以下两个好处:
(1),可以在内核里维护一些信息,这些信息在多次epoll_wait间是保持的,比如所有受监控的文件描述符。
(2),epoll本身也可以被poll/epoll;
具体epoll的虚拟文件系统的实现和性能分析无关,不再赘述。
2). long sys_epoll_ctl(int epfd, int op, int fd, struct epoll_event event)
sys_epoll_ctl(epoll_ctl对应的内核函数),需要明确的是每次调用sys_epoll_ctl只处理一个文件描述符,这里主要描述当op为EPOLL_CTL_ADD时的执行过程,sys_epoll_ctl做一些安全性检查后进入ep_insert,ep_insert里将 ep_poll_callback做为回掉函数加入设备的等待队列(假定这时设备尚未就绪),由于每次poll_ctl只操作一个文件描述符,因此也可以认为这是一个O(1)操作。
ep_poll_callback函数很关键,它在所等待的设备就绪后被系统回掉,执行两个操作:
(1),将就绪设备加入就绪队列,这一步避免了像poll那样在设备就绪后再次轮询所有设备找就绪者,降低了时间复杂度,由O(n)到O(1);
(2),唤醒虚拟的epoll文件;
(1). 注册句柄 op = EPOLL_CTL_ADD
注册过程主要包括:
A.将fd插入到hash(或rbtree)中,如果原来已经存在返回-EEXIST,
B.给fd注册一个回调函数,该函数会在fd有事件时调用,在该函数中将fd加入到epoll的就绪队列中。
C.检查fd当前是否已经有期望的事件产生。如果有,将其加入到epoll的就绪队列中,唤醒epoll_wait。
(2). 修改事件 op = EPOLL_CTL_MOD
修改事件只是将新的事件替换旧的事件,然后检查fd是否有期望的事件。如果有,将其加入到epoll的就绪队列中,唤醒epoll_wait。
(3). 删除句柄 op = EPOLL_CTL_DEL
将fd从hash(rbtree)中清除。
3). long sys_epoll_wait(int epfd, struct epoll_event events, int maxevents,int timeout)
如果epoll的就绪队列为空,并且timeout非0,挂起当前进程,引起CPU调度。
如果epoll的就绪队列不空,遍历就绪队列。对队列中的每一个节点,获取该文件已触发的事件,判断其中是否有我们期待的事件,如果有,将其对应的epoll_event结构copy到用户events。
sys_epoll_wait,这里实际执行操作的是ep_poll函数。该函数等待将进程自身插入虚拟epoll文件的等待队列,直到被唤醒(见上面ep_poll_callback函数描述),最后执行ep_events_transfer将结果拷贝到用户空间。由于只拷贝就绪设备信息,所以这里的拷贝是一个O(1)操作。
需要注意的是,在LT模式下,把符合条件的事件copy到用户空间后,还会把对应的文件重新挂接到就绪队列。所以在LT模式下,如果一次epoll_wait某个socket没有read/write完所有数据,下次epoll_wait还会返回该socket句柄。
6. 使用epoll的注意事项
1. ET模式比LT模式高效,但比较难控制。
2. 如果某个句柄期待的事件不变,不需要EPOLL_CTL_MOD,但每次读写后将该句柄modify一次有助于提高稳定性,特别在ET模式。
3. socket关闭后最好将该句柄从epoll中delete(EPOLL_CTL_DEL),虽然epoll自身有处理,但会使epoll的hash的节点数增多,影响搜索hash的速度。
注:本文并非原创,乃是对多篇网络资料的整理!