I/O复用之epoll的实现

epoll系列系统调用

     epoll是Linux下特有的I/O复用函数,它在实现和使用上与select、poll有很大的差异。首先epoll使用一组函数来完成任务,而不是单个函数,其次,epoll吧用户关心的文件描述符上的事件放在内核中的一个事件表中,无需向select和poll那样每次调用都要重复传入文件描述符集或者事件集,但是epoll需要一个额外的文件描述符来标识内核中的这个事件表,使用epoll_create函数创建:
#include <sys/epoll.h>
int epoll_create(int size)
此函数用来创建内核事件表。

内核事件表:在系统中创建一个用于记录用户关注的文件描述符上的事件的一个表

当调用epoll_create函数时,内核会创建一个eventpoll结构,其中有两个重要的成员 rb_root(红黑树) rdlist(双向链表),每个epoll对象都有一个独立的eventpoll结构,epoll_ctl()添加的事件会挂载到以rb_root为根节点的红黑树上面。
在这里插入图片描述
那么用户如何将需要关注的事件传递给内核事件表呢? 这时需要使用操作内核事件表的函数:
#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
fd参数是要操作的文件描述符,op参数则制定操作类型,操作类型有如下三种宏定义:

EPOLL_CTL_ADD,往事件表中注册的fd上的事件
EPOLL_CTL_MOD,修改fd上的注册事件
EPOLL_CTL_DEL,删除fd上的注册事件

event参数指定事件,它是epoll_event结构的指针类型,epoll_event的定义如下:

struct epoll_event
{
	__uint32_t events;/* epoll事件 */
	epoll_data__t data; /* 用户数据 */
};

     其中events成员描述事件类型。epoll的事件类型和poll基本相同,表示epoll事件的宏是在poll的对应的宏加上"E",但是epoll有两个额外的事件类型——EPOLLET和EPOLLONESHOT。它们对于epoll的高效运行非常关键。data的成员用于存储用户数据,其类型epoll_data_t的定义如下:

typedef union epoll_data
{
	void *ptr;
	int fd;
	uint32_t u32;
	uint64_t u64;
}epoll_data_t;

epoll_data是一个联合体,其4个成员中使用最多的是fd,他指定事件所丛书的目标文件描述符。ptr成员可用来指定fd相关的用户数据,但是因为union原因,我们不可以同时使用ptr和fd成员,因此,若要将文件描述符和用户数据关联起来,欲实现快速的数据访问,只能使用其他手段,例如放弃使用epoll_data_t的fd成员,而在ptr指向的用户数据包含fd。
epoll_ctl成功返回0,失败返回-1,并设置errno。

epoll_ctl()具体做法是怎样的呢?

 asmlinkage long
 sys_epoll_ctl(int epfd, int op, int fd, struct epoll_event __user *event)
 {
	 struct file *file, *tfile;
	 struct eventpoll *ep;
	 struct epitem *epi;
	 struct epoll_event epds;
	....
	 epi = ep_find(ep, tfile, fd);//tfile存放要监听的fd对应在rb-tree中的epitem
	 switch (op) {//省略了判空处理
	 case EPOLL_CTL_ADD: epds.events |= POLLERR | POLLHUP;
	 error = ep_insert(ep, &epds, tfile, fd); break;
	 case EPOLL_CTL_DEL: error = ep_remove(ep, epi); break;
	 case EPOLL_CTL_MOD: epds.events |= POLLERR | POLLHUP;
	 error = ep_modify(ep, epi, &epds); break;
 }

使用ep_find()函数去查找 struct epitem,若找到的话将根据 ADD、DEL、MOD操作调用不同的函数对应红黑树的插入,删除,修改

     上面的信息告诉我们,一个新创建的epoll文件带有一个struct eventpoll结构,这个结构上再挂一个红黑树,而这个红黑树就是每次epoll_ctl时fd存放的地方!
在这里插入图片描述

如何获取文件描述符上面的事件呢?这就需要使用epoll_wait函数了
#include <sys/epoll.h>
int epoll_wait(int epfd, struct_event* events, int maxevents, int timeout)
events:执行一个用户数组,返回所有就绪文件描述符
event:指定数组元素的个数–此数组用于保存epoll_wait返回时,内核填充的就绪文件描述符的信息
epoll_wait成功返回就绪文件描述符的个数,超时返回0,失败返回-1
epoll_wait函数如果检测到事件,那将所有就绪的事件从内核事件表中复制到events指向的数组中。这个数组只用于输出epoll_wait检测到的就绪事件,这样提高了应用程序索引就绪文件描述符的效率。
epoll_wait()的具体做法
epoll_wait()函数内部调用Sys_epoll_wait()从file->private_data 取到 struct eventpoll,然后调用ep_poll()

static int ep_poll(struct eventpoll *ep, struct epoll_event __user *events, int maxevents, long timeout)
 {
	 int res;
	 wait_queue_t wait;//等待队列项
	 if (list_empty(&ep->rdllist))
	  {
		 //ep->rdllist存放的是已就绪(read)的fd,为空时说明当前没有就绪的fd,所以需要将当前进程放入等待队列
		 init_waitqueue_entry(&wait, current);//创建一个等待队列项,并使用当前进程(current)初始化
		 add_wait_queue(&ep->wq, &wait);//将刚创建的等待队列项加入到ep中的等待队列(即将当前进程添加到等待队列)
		 for (;;) 
		 {
			 /*将进程状态设置为TASK_INTERRUPTIBLE,因为我们不希望这期间ep_poll_callback()发信号唤醒进程的时候,进程还在sleep */
			 set_current_state(TASK_INTERRUPTIBLE);
			 if (!list_empty(&ep->rdllist) || !jtimeout)//如果ep->rdllist非空(即有就绪的fd)或时间到则跳出循环

			 break;
			 if (signal_pending(current)) {
			 res = -EINTR;
			 break;
		 }
	 }
	 remove_wait_queue(&ep->wq, &wait);//将等待队列项移出等待队列(将当前进程移出)
	 set_current_state(TASK_RUNNING);
 }

      ep_poll()函数获取ep->rdlist中就绪的fd,若为空就一直持续循环直到超时
      ep->rdlist 如何不为空呢,这时就是需要当时 ep_insert设置的回调函数了

static int ep_insert(struct eventpoll *ep, struct epoll_event *event, struct file *tfile, int fd)
{

	struct epitem *epi;
	 struct ep_pqueue epq;// 创建ep_pqueue对象
	epi = EPI_MEM_ALLOC();//分配一个epitem
	/* 初始化这个epitem ... */
	 epi->ep = ep;//将创建的epitem添加到传进来的struct eventpoll

	/*后几行是设置epitem的相应字段*/
	 EP_SET_FFD(&epi->ffd, tfile, fd);//将要监听的fd加入到刚创建的epitem
	 epi->event = *event;
	 epi->nwait = 0;

	/* Initialize the poll table using the queue callback */
	epq.epi = epi;  //将一个epq和新插入的epitem(epi)关联

	//下面一句等价于&(epq.pt)->qproc = ep_ptable_queue_proc;设置回调函数

	init_poll_funcptr(&epq.pt, ep_ptable_queue_proc);

	revents = tfile->f_op->poll(tfile, &epq.pt);  //tfile代表target file,即被监听的文件,poll()返回就绪事件的掩码,赋给revents.

	list_add_tail(&epi->fllink, &tfile->f_ep_links);// 每个文件会将所有监听自己的epitem链起来

	ep_rbtree_insert(ep, epi);// 都搞定后, 将epitem插入到对应的eventpoll中去

	……

}

       接着 tfile->f_op->poll(tfile, &epq.pt)其实就是调用被监控文件(epoll里叫“target file”)的poll方法,而这个poll其实就是调用poll_wait(还记得poll_wait吗?每个支持poll的设备驱动程序都要调用的),最后就是调用ep_ptable_queue_proc。(注:f_op->poll()一般来说只是个wrapper, 它会调用真正的poll实现, 拿UDP的socket来举例, 这里就是这样的调用流程: f_op->poll(), sock_poll(), udp_poll(), datagram_poll(), sock_poll_wait()。)这是比较难解的一个调用关系,因为不是语言级的直接调用。ep_insert还把struct epitem放到struct file里的f_ep_links连表里,以方便查找,struct epitem里的fllink就是担负这个使命的。
其中最重要的就属于这个回调函数(ep_ptable_queue_proc)了

static void ep_ptable_queue_proc(struct file *file, wait_queue_head_t *whead, poll_table *pt)
{
	 struct epitem *epi = EP_ITEM_FROM_EPQUEUE(pt);
	 struct eppoll_entry *pwq;
	if (epi->nwait >= 0 && (pwq = PWQ_MEM_ALLOC())) {
	 //设置回调函数
	 init_waitqueue_func_entry(&pwq->wait, ep_poll_callback);
	 //这个whead是设备的等待队列
	 pwq->whead = whead;
	 pwq->base = epi;
	 //将ep_poll_callback 加入waitqueue 中
	 add_wait_queue(whead, &pwq->wait);
	 list_add_tail(&pwq->llink, &epi->pwqlist);
	 epi->nwait++;
	 } else {
	 /* We have to signal that an error occurred */
	 epi->nwait = -1;
 	}
 }

ep_wait中做的最重要的事情:创建 struct epoll_entry 唤醒回调函数 ep_poll_callback并加入到设备的等待队列
ep_poll_callback()的作用就是将有响应的epitem插入到ep->rdlist中,这时rdlist中全部都是已经就绪的fd了
在这里插入图片描述
LT和ET模式

LT:电平触发,如果epoll_wait()将事件返回给应用程序,应用程序可以不立即处理该事件,则下一次epoll_wait()还会将事件通知给应用程序
这个操作在ep_event_transfer()函数完成

static int ep_events_transfer(struct eventpoll *ep, struct epoll_event __user *events, 

int maxevents)
{
int eventcnt = 0;
struct list_head txlist;
INIT_LIST_HEAD(&txlist);
/* Collect/extract ready items */
if (ep_collect_ready_items(ep, &txlist, maxevents) > 0) {
/* Build result set in userspace */
eventcnt = ep_send_events(ep, &txlist, events);
/* Reinject ready items into the ready list */
ep_reinject_items(ep, &txlist);
}
up_read(&ep->sem);
return eventcnt;
}

其中ep_collect_ready_items()把rdlist的fd移动到txlist中(rdlist就为空了),随后ep_send_events把txlist的fd拷贝到用户空间,然后ep_reinject_items将一部分fd从txlist中"归还"给rdlist以便于下次还能从rdlist发现它,下次调用epoll_wait()就会继续告知应用程序。
ET:边沿触发,如果epoll_wait将事件返回给应用程序,应用程序不处理时间则下一次就不会降该事件通知给应用程序
这一点在ep_reinject_items函数中体现

static void ep_reinject_items(struct eventpoll *ep, struct list_head *txlist)
{
     int ricnt = 0, pwake = 0;
     unsigned long flags;
     struct epitem *epi;
     while (!list_empty(txlist)) {//遍历txlist(此时txlist存放的是已就绪的epitem)
     epi = list_entry(txlist->next, struct epitem, txlink);
     EP_LIST_DEL(&epi->txlink);//将当前的epitem从txlist中删除
     if (EP_RB_LINKED(&epi->rbn) && !(epi->event.events & EPOLLET) &&
     (epi->revents & epi->event.events) && !EP_IS_LINKED(&epi->rdllink)) {

     list_add_tail(&epi->rdllink, &ep->rdllist);//将当前epitem重新加入ep->rdllist
     ricnt++;// ep->rdllist中epitem的个数(即从新加入就绪的epitem的个数)
      }
    }
 if (ricnt) {//如果ep->rdllist不空,重新唤醒等、等待队列的进程(current)
    if (waitqueue_active(&ep->wq))
    wake_up(&ep->wq);
    if (waitqueue_active(&ep->poll_wait))
    pwake++;
    }
   ……

}

!(epi->event.events & EPOLLET) 这个判断尤为重要,是将没有满足EPOLLET并且事件被关注的fd重新放进了rdlist,当更改为ET模式后就不会满足条件也就不会将"剩余"的fd拷贝回rdlist了,随后调用epoll_wait也不会通知应用程序。在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值