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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值