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的速度。

 

 注:本文并非原创,乃是对多篇网络资料的整理!

posted @ 2013-04-01 17:18  alyssa.cui  阅读(1208)  评论(0编辑  收藏  举报