本篇文章是基于Linux Kernel 2.6.11的源码来展开的。
之前我的博客里也写到过,epoll是Linux特有的I/O复用函数。它在实现上与select、poll有很大差异。它的提出是为了弥补select和poll对于描述符过多处理时时间效率过高的问题。
可以参考https://mp.csdn.net/postedit/89601608
三种IO函数中:
IO方法返回后,通知应用程序哪些事件就绪时:
但是epoll也并不总优于select和poll,epoll适用于并发连接的客户端很多,但是就绪的文件描述符不多的情况下;而select则适用于当前并发连接的客户端不多,但是就绪描述符较多的情况下。
select、poll只能工作于LT模式,epoll既可以工作于LT模式,还可以工作于ET模式。当有事件就绪时,LT模式会反复提醒你,ET方式当有事件就绪后,无论用户此次是否将数据读取完毕,只提醒用户一次,这就使得用户空间程序有可能缓存IO状态,减少函数t的调用,提高应用程序的效率。
在源码中可以很清楚的将ET和LT分开,即你当前到底是工作于哪种方式下。如果是LT模式下,给应用程序返回之后,会将其再次加入rdllist即就绪队列中;而如果是ET模式,那就只提醒一次。
我们在使用epoll函数时,都知道它有一组函数来实现,这也是区别于select和poll的一点。它的一组函数包括:
#include
int epoll_create(int size);//创建内核事件表
int epoll_ctl();//向内核事件表中添加描述符和事件的增加,修改或删除
int epoll_wait();//获取有就绪事件的描述符
在源码中这三个函数也是重点,在每个函数中又调用了诸多的函数来实现整个epoll的功能,这三个函数密不可分,相互依存。
epoll_create()函数中定义了fd,用其来标志内核事件表,但是它只是一个整型数字怎么能找到相应的内核事件表,这就很重要了。还定义了一个文件节点*inode,和一个如果打开文件就一定会创建的的结构体struct file *file的结构体变量。
epoll_create()函数中主要调用了两个函数:ep_getfd()和ep_file_init()。ep_getfd()用来给fd、inode、file这三个变量获取到值, ep_file_init()则是通过传入的file参数来获取一个更重要的数据结构,并使得file与其建立关系。这些都是整个实现过程中非常重要的数据结构,因为他们之间的关系密不可分。
最终epoll_create()函数返回fd,即后续函数可以用fd来找到内核事件表并进行相应操作。
epoll_ctl()函数中就要用到epoll_create的返回值了,用户并且要传入要操作的文件描述符,操作类型和事件。同样,在epoll_ctl函数中也定义了很多的数据结构和变量,传入epfd的目的就是通过它来找到一系列的东西。
而它里面最重要的函数就是通过查找当前的文件描述符是否在内核事件表中创建而执行不同的操作。如果没有添加,则执行insert,否则执行修改或删除。也是在insert中注册了最重要的回调函数。
epoll_wait()在此函数内,将所有的就绪事件拷贝给用户,返回的是就绪文件描述符的个数。在epoll_wait函数中还有很重要的一步就是会判断是ET还LT模式,然后来执行不同的操作。
其实可以把epoll中的内核事件表看做是一个特殊的文件系统,在eventpoll_init()中有一步就是要来注册它。
首先当系统启动时,epoll进行初始化:
这也就是为什么epoll_create会返回一个新的fd,这是因为在eventpolls这个文件系统里创建了一个新文件。
在使用时,epoll_create()函数被用来创建内核事件表,它调用成功时返回的就是内核事件表的描述符。
epoll_create()函数源码的原型如下:
asmlinkage long sys_epoll_create(int size)
epoll_create函数执行过程:
1、strtuct inode结构体
源码中对其描述为: 内核用该结构在内部表示一个文件。它与file不同。file表示的是文件描述符。对单个文件,可能会有许多个表示打开的文件描述符的filep结构,但是它们都指向单个inode结构。
2、struct file结构体
struct file是epoll源码中非常重要的一个结构体,它与很多的结构体都会产生关联。它里面重要的成员有:
3、struct eventpoll 结构体
这个结构体中重要的成员就是rdllist和rbr:
4、ep_getfd()
ep_getfd()函数的主要功能是为fd,inode节点,file赋予值。函数原型如下:
error = ep_getfd(&fd, &inode, &file);//调用处
static int ep_getfd(int *efd, struct inode **einode, struct file **efile);//函数实现处
可以看见在调用该函数的时候参数全传的是地址,那么可以猜想在该函数内部肯定是要对这三个变量的值进行改变的。
5、 ep_file_init()
ep_file_init()函数的主要功能是将struct file结构体与另外一个很重要的结构体建立关系,并创建内核事件表。
error = ep_file_init(file);//函数调用处
static int ep_file_init(struct file *file);//函数实现
6、总结
上述步骤是sys_epoll_create()中比较重要的步骤,执行成功后返回fd,后续函数可以用fd来找到内核事件表并进行相应操作。
其各结构体之间的关系可以先表示为:
epoll_ctl用来操作epoll的内核事件表,可以对内核事件表进行添加、修改和删除。函数源码中的原型如下:
sys_epoll_ctl(int epfd, int op, int fd, struct epoll_event __user *event);
1、参数:
①EPOLL_CTL_ADD,往事件表中注册fd上的事件
②EPOLL_CTL_MOD,修改fd上的注册事件
③EPOLL_CTL_DEL,删除fd上的注册事件
2、epoll_ctl函数执行过程:
3、ep_insert()
ep_insert()函数原型如下:
error = ep_insert(ep, &epds, tfile, fd);//函数调用处
static int ep_insert(struct eventpoll *ep, struct epoll_event *event,
struct file *tfile, int fd);//实现
①可以看到在init_poll_funcptr函数中,将ep_ptable_queue_proc函数赋值给了epq中的函数指针,
即接下来调用ep_ptable_queue_proc()函数。
②在ep_ptable_queue_proc回调函数中,注册回调函数ep_poll_callback。ep_poll_callback表示当描述符fd上相应的事件发生时该 如何通知该进程。在ep_ptable_queue_proc回调函数中,检测文件描述符fd对应的设备的epoll_event事件是否发生,如果发生则把 fd 及其epoll_event加入上面提到的就绪队列rdlist中。
③在ep_poll_callback()中,当事件就绪后,执行该回调函数,把描述符添加到rdllist。
④在整个回调函数实现中,还有另外一个很重要的结构体struct eppoll_entry *pwq。eppoll_entry主要用于处理 epitem 和 epitem上 事 件发生时的回调函数之间的关联。
它的实现步骤如下:
a.首先将eppoll_entry的whead指向fd的设备等待队列。
b.然后初始化eppoll_entry的base变量指向epitem。
c.通过add_wait_queue()函数将epoll_entry挂载到目标文件fd的设备等待队列waitlist上。
d.最后通过list_add_tail()函数将eppoll_entry挂载到epitem的pwqlist上面。
调用ep_rbtree_insert(ep, epi),将描述符和事件添加到内核事件表中。
完成以上调用后,将还会判断刚刚添加的的event是否刚好发生,如果是,那么做一个ready动作,将epitem加入到就绪队列rdlist中,并唤醒epoll上的等待队列。
至此,添加完成。
epoll_wait() 用来获取就绪描述符,函数成功时返回就绪的文件描述符的个数。函数原型如下:
asmlinkage long sys_epoll_wait(int epfd, struct epoll_event __user *events,
int maxevents, int timeout)
1、参数:
2、epoll_wait函数执行过程:
3、ep_poll()
函数原型如下:
error = ep_poll(ep, events, maxevents, timeout);//调用处
static int ep_poll(struct eventpoll *ep, struct epoll_event __user *events,
int maxevents, long timeout);//实现
ep_events_transfer()函数原型:
static int ep_events_transfer(struct eventpoll *ep,
struct epoll_event __user *events, int maxevents);
ep_events_transfer函数将rdllist中的就绪事件拷贝到它定义的另外一个结构体变量txlist中,并将rdllist清空。
①首先调用 ep_collect_ready_items()函数从rdllist中收集最多maxevent个元素到txlist中。每向txlist拷贝一个元素,从rdllist中将其删除,最终rdllist被清空。如果ep_collect_ready_items()函数返回为真时,则调用ep_send_events()函数和ep_reinject_items()函数。
②调用 ep_send_events()函数,变量txlist,将有事件就绪的描述符拷贝到用户空间,实际返回的是实际拷贝的数目。
③调用 ep_reinject_items()函数,在该函数中就要判断当前事件到底是工作于ET还是LT模式。如果是LT模式,就将描述符返还给rdlist,以便于再下一次有事件就绪时可以继续提醒用户。