源码分析使用版本为:linux2.6.9;分析文件为:eventpoll.c,所有的epoll实现代码都在此文件中。
1、 sys_epoll_create()
Epoll通过sys_epoll_create创建一个epoll文件,并返回该epoll文件的描述符,在该文件中也将同时创建该epoll文件的文件结构体,i节点等,需要注意的是,其中创建了一个eventpoll结构体,该结构体将存放于epoll文件结构体的private_data成员中,这里的文件结构体类型为struct file,就是linux系统的通用文件结构体类型。
因此:一次调用sys_epoll_create,便产生一个epoll文件,epoll便是使用此文件对整个epoll操作过程中的所有数据进行管理。
另需注意的是该函数中有一参数int size,该参数只要大于0即可,实际监控的设备数目不受此值约束。通过分析源码也可以看到如下代码行:
if (size <= 0)
goto eexit_1;
说明此值若小于等于0便创建失败。在此函数中只有这一处使用了size。
2、 结构体eventpoll
每创建一个epoll文件描述符,就对应创建了该epoll文件的文件结构体等内容,
eventpoll的定义形式如下:
struct eventpoll {
rwlock_t ;
struct rw_semaphore sem;
wait_queue_head_t wq;
wait_queue_head_t poll_wait;
struct list_head rdllist;
struct rb_root rbr;
};
其中:
(1)成员struct rb_root rbr是指向一颗红黑树,该红黑树中存储了所有待监控目标设备的struct epitem结构。
(2)成员struct list_head rdllist表示所有就绪的待监控目标设备。
(3)成员struct rw_semaphore sem是linux系统的旗标,主要用于控制同步访问红黑树中挂载的待监控设备。
3、 结构体epitem
数据结构epitem主要用于存储一个待监控目标设备的相关信息,每当要添加一个待监控目标设备到epoll里时,实际上就是为该设备创建了一个这样的结构体,并将它挂到了epoll文件的红黑树中(就是前文提到的epoll文件的文件结构体中的private_data成员所指的红黑树),例如我们要添加一个socket函数创建的socket文件描述符fd时,调用sys_epoll_ctl后,在此函数内部将为此fd创建一个epitem结构体,并将它挂到epoll的红黑树中。
它的定义形式如下:
struct epitem {
struct rb_node rbn;
struct list_headrdllink;
struct epoll_filefdffd;
int nwait;
struct list_headpwqlist;
struct eventpoll*ep;
atomic_t usecnt;
struct list_headfllink;
struct list_headtxlink;
unsigned intrevents;
};
其中:
(1)成员struct eventpoll *ep,指向存放该目标设备的epoll结构体(类型为struct eventpoll).
(2)成员
4、 sys_epoll_ctl()
该函数用于添加、修改、或者删除一个ep(定义为eventpoll*ep)中的待监控目标设备(后续将简称为目标设备);以添加一个设备结构为例,函数中将调用ep_insert(ep, &epds, tfile, fd)把目标设备的结构放入到ep的的红黑树中。该函数内部执行的流程大概如下:
(1)通过epoll文件描述符获取epoll文件结构体;
(2)通过epoll文件的结构体获取epoll的eventpoll结构体;
(3)在eventpoll结构体的红黑树中查找目标设备的epitem结构体是否存在,如果存在就返回此设备的epitem结构体。
(4)根据请求的类型完成目标设备在epoll文件中的添加、修改和删除操作。该函数源码如下:
对应源码大致为:
file = fget(epfd); //获取epoll的文件结构体,这里epfd就是epoll文件描述符
…
ep = file->private_data;//获取epoll文件结构体中eventpoll结构体
…
epi = ep_find(ep, tfile, fd);//从epoll的红黑树中查找目标设备epitem结构体是否存在
…
switch (op) {
case EPOLL_CTL_ADD:
if (!epi)ep_insert(ep, &epds, tfile, fd);
case EPOLL_CTL_DEL:
if (epi) ep_remove(ep,epi);
break;
case EPOLL_CTL_MOD:
if (epi)ep_modify(ep, epi, &epds);
break;
}
5、 ep_insert()
该函数主要完成将待监控的目标设备以及监听的事件类型设置进epoll文件的红黑树中,其源码中具体实现步骤为:
(1)为目标设备创建对应的epitem结构体,代码段及其如下:
//创建epitem结构体
if (!(epi = EPI_MEM_ALLOC()))
gotoeexit_1;
//初始化结构体
EP_RB_INITNODE(&epi->rbn);
INIT_LIST_HEAD(&epi->rdllink);
INIT_LIST_HEAD(&epi->fllink);
INIT_LIST_HEAD(&epi->txlink);
INIT_LIST_HEAD(&epi->pwqlist);
epi->ep = ep;//其ep结构体指向存放该结构体的epoll文件的eventpoll结构
EP_SET_FFD(&epi->ffd,tfile, fd);//设置目标设备的fd
epi->event =*event;//设置所关注的事件
(2)将回调函数ep_ptable_queue_proc设置进目标设备的结构体中(类型为struct epitem);当目标设备中所关注的事件发生后将会调用此回调函数。相关代码为:
epq.epi = epi;
init_poll_funcptr(&epq.pt, ep_ptable_queue_proc);
revents = tfile->f_op->poll(tfile, &epq.pt);
这里epq 的类型为struct ep_pqueue epq,而该类型的定义形式为:
struct ep_pqueue {
poll_table pt;
struct epitem *epi;
};
可见,epq中就是放了一个目标设备还有该设备对应的回调函数;代码init_poll_funcptr(&epq.pt,ep_ptable_queue_proc)将回调函数设置进epq.pt之后,又执行代码tfile->f_op->poll(tfile, &epq.pt)来完成将回调函数设置进目标设备。
(2)将目标设备的结构体放入ep的rb树中,使用函数ep_rbtree_insert(ep,epi)来完成此功能。
6、 sys_epoll_wait()
该函数将就绪的待检测设备放入数组(该数组通过参数传入),并返回就绪设备的数目。在该函数内部主要调用ep_poll函数来完成此功能。
7、 ep_poll()
该函数实际完成sys_epoll_wait函数要完成的功能,在该函数中首先判断是否有设备就绪,如果没有则进入睡眠状态,如果有设备已经就绪,则会将就绪队列中的就绪设备的fd拷贝到用户空间,并返回就绪设备的数目。具体分以下两个步骤
(1)当目标设备所关注的事件发生时将会调用自己的回调函数,将自己的设备fd放在epoll的成员队列ep->rdllist中,因此在该函数内也是判断此队列是否为空来判断是否有设备就绪。其部分代码如下:
if (list_empty(&ep->rdllist)) {
init_waitqueue_entry(&wait,current);
add_wait_queue(&ep->wq,&wait);
for (;;){
set_current_state(TASK_INTERRUPTIBLE);
if(!list_empty(&ep->rdllist) || !jtimeout)
break;
if(signal_pending(current)) {
res= -EINTR;
break;
}
write_unlock_irqrestore(&ep->lock,flags);
jtimeout= schedule_timeout(jtimeout);
write_lock_irqsave(&ep->lock,flags);
}
remove_wait_queue(&ep->wq,&wait);
set_current_state(TASK_RUNNING);
}
需要注意的是在该上述代码中,进程并不会一直在for中不停的死循环,函数中调用下面两句代码,将当前进程设置进epoll的等待队列中:
init_waitqueue_entry(&wait, current);
add_wait_queue(&ep->wq,&wait);
接下来调用set_current_state之后,本进程current将进入睡眠,直到被唤醒。语句set_current_state(TASK_INTERRUPTIBLE);表示在遇到TASK_INTERRUPTIBLE信号时进程将被唤醒。
在有事件发生后,当前进程被唤醒,然后通过下面的代码将当前进程从epoll的等待队列中删除:
remove_wait_queue(&ep->wq, &wait);
Epoll的ep->rdllist链表里放置的是当前epoll注册的待监控目标设备中所有的就绪设备,当目标设备就绪时,它就会调用回调函数ep_poll_callback将当前设备拷贝到就绪列表ep->rdllist中。
(2)函数执行语句ep_events_transfer将ep->rdllist链表里的就绪设备拷贝到用户的地址空间,具体如下:
if (!res && eavail &&
!(res = ep_events_transfer(ep, events,maxevents)) && jtimeout)
gotoretry;
8、 回调函数ep_ptable_queue_proc
回调函数在sys_epoll_ctl添加新的目标控设备时使用(直接调用之处在函数ep_insert内),在ep_ptable_queue_proc函数内部将为目标设备注册一个设备回调函数ep_poll_callback,其详细操作步骤如下:
(1)从epoll中查找目标设备的设备结构体structepitem,其代码为:
struct epitem *epi = EP_ITEM_FROM_EPQUEUE(pt);
(2)为目标设备申请一个结构体structeppoll_entry,并对此结构体各成员进行初始化,其代码及含义如下:
struct eppoll_entry*pwq;
if (epi->nwait>= 0 && (pwq = PWQ_MEM_ALLOC())) {
init_waitqueue_func_entry(&pwq->wait,ep_poll_callback);//将回调函数ep_poll_callback设置进pwq->wait
pwq->whead= whead;//将目标设备自带的等待队列放进structeppoll_entry
pwq->base= epi;//将目标设备结构体放进structeppoll_entry
add_wait_queue(whead,&pwq->wait);//把目标设备自带的等待队列设置到pwq->wait
list_add_tail(&pwq->llink,&epi->pwqlist);
epi->nwait++;
}
其中函数list_add_tail是linux的内核函数,完成将新节点插入链表的功能,如下所示:
void list_add_tail(struct list_head *new, struct list_head *head)
{
__list_add(new, head->prev, head);
}
9、 回调函数ep_poll_callback
该回调函数在设备就绪时会被调用,它完成的功能是,将当前设备挂到epoll的就绪链表rdllink中,如有就绪则要唤醒睡眠进程,相关代码如下:
list_add_tail(&epi->rdllink, &ep->rdllist);
…
if(waitqueue_active(&ep->wq))
wake_up(&ep->wq);
if(waitqueue_active(&ep->poll_wait))
pwake++;
10、 list_entry
list_entry的功能是已知一个结构体中的一个成员的地址与此结构体的类型来求得结构体变量的地址。其宏定义:
#define list_entry(ptr, type, member) / ((type*)((char *)(ptr)-(unsigned long)(&((type *)0)->member)))