IO端口复用之epoll的底层实现

介绍

    解说中存在一些函数和数据结构,具体出处可以参照上一篇关于select的分析 《IO端口复用之select的底层实现》,里面提及了一些储备知识点,本篇不再赘述。

    由于tcp过于复杂,取个巧,全篇以udp连接来说明一下,内核版本依旧对应2.6.32。本篇在加上之前的《IO端口复用之poll的底层实现》,算是集齐了linux系统常用的端口复用技术三贱客。

    从上述两篇可以看出,select和poll在底层轮训技术上没有本质的区别,而epoll则有了很大的改进,使用了事件触发机制。

epoll系统调用

接口说明

    epoll在使用上与select和poll有本质的区别,select和poll是典型的一撸到底,一个api里面进行轮训判断、搞定所有,出了api就拿到了相应的结果。而epoll则需要多个部分相互合作来完成事件的监听触发工作。

    对epoll系统调用的接口分析可分epoll_create、epoll_ctl、epoll_wait以及就绪事件触发等几个部分。

源码追踪

epoll_create

   
    epoll_create主要创建了一个fd文件描述符,通过该fd可以找到对应的file结构体和eventpoll结构体 。

sys_epoll_create, int, size)
{
	......
	return sys_epoll_create1(0);
}

    epoll_create主要调用了内核系统调用函数sys_epoll_create1,而sys_epoll_create1的函数定义原型为sys_epoll_create1(int flags)。
 

sys_epoll_create1(int flags)
{
	/* 传递进来的flags为0, 为epoll_create传递的size大小没有起作用 */
	int error;
	struct eventpoll *ep = NULL;
	......

	error = ep_alloc(&ep);
	/* eventpoll结构体主要在ep_alloc中开辟的. */

	error = anon_inode_getfd("[eventpoll]", &eventpoll_fops, ep,
				 flags & O_CLOEXEC);

	......
}

    在anon_inode_getfd中构造epoll_create返回的套接字的file和dentry资源,此处给该fd的f_op参数赋值为event_poll_fops,具体结构如下:

        static const struct file_operations eventpoll_fops = {
            .release= ep_eventpoll_release,
            .poll    = ep_eventpoll_poll
        };    

    接上文中的函数调用,接下来是ep_alloc函数。

static int ep_alloc(struct eventpoll **pep)
{
	......
	user = get_current_user();
	/*get_current_user引用struct user_struct结构体(用户结构体), 此处不做讨论*/

	ep = kzalloc(sizeof(*ep), GFP_KERNEL);
	/* ep 为开辟的struct eventpoll结构体, 失败之后将调用free_uid*/

	/* 锁的初始化 */
	spin_lock_init(&ep->lock);
	mutex_init(&ep->mtx);

	/*初始化等待队列(基础结构为struct list_head), 一般sys_epoll_wait()函数用来使用*/
	init_waitqueue_head(&ep->wq);
	/*初始化等待队列(基础结构为struct list_head), 一般file->poll()函数用来使用*/
	init_waitqueue_head(&ep->poll_wait);

	/*初始化就绪队列*/
	INIT_LIST_HEAD(&ep->rdllist);

	/* epoll的底层数据结构红黑树, 红黑树的主要作用是进行ep_find操作,
	 * 用户快速查找事件是否已监听, 或快速的修改监听事件的状态集 */
	ep->rbr = RB_ROOT;

	/* ovflist初始化, ovflist将在内核触发通知和epoll_wait中说明*/
	ep->ovflist = EP_UNACTIVE_PTR;

	/* 创建epoll的用户结构体*/
	ep->user = user;

	*pep = ep;
	......

free_uid:
	/*释放引用计数,当引用为0时, 释放该结构, 此处不讨论*/
	free_uid(user);
	......
}
    
 

    接下来在anon_inode_getfd函数中调用了anon_inode_getfile函数,申请了对应的fd值,并开辟了与之关联的file(struct file结构)描述符,将file结构中的成员f_op设置为全局的eventpoll_fops,将file的private_data成员赋值为ep指针(上述开辟的struct eventpoll结构指针)。

    epoll_create系统调用就是创建一个eventpoll全局结构,一个文件描述符fd,一个file描述符结构,并通过fd查找对应的file结构,通过file->private_data找到对应的ep,从而在epoll_ctl和epoll_wait中使用。

epoll_ctl

    epoll_ctl是一个辅助接口,主要用于增加、删除和修改监听集。第一个参数epfd(int类型,epoll_create所创建的文件描述符),第二个参数fd(int类型,被操作的监听套接字文件描述符)。

  • 在sys_epoll_ctl函数中,通过ep_op_has_event判断操作类型,当操作类型不等于EPOLL_CTL_DEL时,说明有监听类型变化或者新的监听套接字加入,将应用层空间参数event拷贝给epds(struct epoll_event结构)。
  • 根据第二个参数fd,找到文件描述符file结构,并提取对应文件描述符的f_op,从select和poll剖析的socket及bind系统调用可知,此处的f_op指向 socket_file_ops。而f_op->poll函数,既socket_file_ops中的poll函数为sock_poll。而在sock_poll函数中,可通过file->private_data提取出来sock指针(struct socket结构指针)。而sock中的ops指向的是inet_dgram_op(以UDP为例),sock->ops->poll实际上调用了inet_dgram_ops中的poll函数udp_poll。当监听套接字对应的f_op结构或者f_op->poll函数不存在时,是不可以进行epoll_ctl操作的。
  • 从epfd提取对应的file结构,从file->private_data中提取出来ep指针。而后续所操作的红黑树的头节点就是ep的rbr成员。
  • 在ep_find函数中,遍历红黑树,看对应的文件文件描述符是否已经存在,返回epi(struct epitem结构)。epi是红黑树的节点结构是struct epitem指针,epitem结构中的rbn(struct rb_node结构)则为红黑树挂载点。其中ep_find的具体比较函数是ep_cmp_ffd,该函数中主要是比较epi中的ffd和当前的ffd,其中ffd是struct epoll_filefd结构,里面包括fd(int)和file(struct file指针)两个成员。
  • 如果操作类型是EPOLL_CTL_DEL,调用ep_remove从红黑树中移除监听的文件描述符对应的epi(struct epitem)相关信息。

    先详细看一下epitem相关信息。

    struct epitem {

        struct rb_node rbn;

        struct list_head rdllink;

        struct epitem *next;

        struct epoll_filefd ffd;

        int nwait;

       struct list_head pwqlist;

       struct eventpoll *ep;

       struct list_head fllink;

       struct epoll_event event;

    }

    执行ep_remove除了将epi从ep的红黑树中移除,还会执行ep_unregister_pollwait函数,剥离与之相关的挂载点。具体挂载点在ep_insert中介绍。

  • 如果操作类型是EPOLL_CTL_ADD,调用ep_insert。

    在ep_insert中创建epi,并对epi进行初始化操作,并定义epq变量(struct ep_pqueue结构)。

    struct ep_pqueue {

        poll_table pt;

       struct epitem *epi;

    }

    epi->ep = ep

    epi->nwait = 0

    epi->next = EP_UNACTIVE_PTR

    调用init_poll_funcptr对epq中的pt进行初始化,其中pt的qproc成员初始化为函数ep_ptable_queue_proc,pt的key成员初始化~0UL。epq中的epi既为当前创建的epi。

    调用对应f_op中的poll函数,返回就绪结果集revents。主要是针对当前要添加的文件描述符进行就绪查询,从UDP的角度来看,本质执行的是udp_poll函数,在udp_poll中调用的datagram_poll函数。在datagram_poll函数中会调用sock_poll_wait函数,sock_poll_wait会调用epq中的pt中的qproc,既ep_ptable_queue_proc函数。

    在函数ep_ptable_tqueue_proc函数中,用whead(wait_queue_head_t指针)代表sk->sk_sleep,从poll_table结构中提取出epi。创建pwq(struct eppoll_entry结构指针),调用init_waitqueue_func_entry对pwq的wait(wait_queue_t结构)进行初始化,wait中的func赋值成ep_poll_callback。pwq的whead赋值成whead(sk->sk_sleep),pwq的base赋值成epi,将whead(sk->sk_sleep)挂载到pwq的wait上,将pwd的llink挂载到epi的pwqlist上,epi的nwait进行累加。

    随后将epi的epi_fllink挂载到对应file文件的f_ep_links节点中,将epi插入到ep的红黑树中,并根据poll函数的结果集进行判断,当前文件描述符已经有就绪事件且epi的rdllink是还没有挂载,将epi->rdllink挂载到ep的rdllist就绪链表节点上,供epoll_wait使用。

    然后用waitqueue_active(&ep->wq) 判断ep的wq队列是否为空, 不为空说明此时已经有epoll_wait在监听该eventpoll, 并处于沉睡状态。则调用ep->wq上注册的default_wake_function函数进行epoll_wait唤醒,让epoll_wait继续执行for(;;)循环,关于default_wake_function的说明在内核触发上也有提到。

  • 如果操作类型是EPOLL_CTL_MOD,调用ep_modify。

    ep_modify函数的操作跟ep_insert有一定相似。

ep_find主要用来查找当前插入的fd套接字是否已经在epoll监听的红黑树中。
static struct epitem *ep_find(struct eventpoll *ep, struct file *file, int fd)
{
	int kcmp;
	struct rb_node *rbp;
	struct epitem *epi, *epir = NULL;
	struct epoll_filefd ffd;

	ep_set_ffd(&ffd, file, fd);
	/*遍历红黑树*/
	for (rbp = ep->rbr.rb_node; rbp; ) {
		epi = rb_entry(rbp, struct epitem, rbn);
		/*红黑树中的比较函数, 以file结构体指针和fd大小进行比较*/
		kcmp = ep_cmp_ffd(&ffd, &epi->ffd);
		if (kcmp > 0)
			rbp = rbp->rb_right;
		else if (kcmp < 0)
			rbp = rbp->rb_left;
		else {
			/*在红黑树中找到了要插入的fd套接字*/
			epir = epi;
			break;
		}
	}

	return epir;
}


ep_insert是插入操作, 插入操作主要是将事件epi插入到红黑树一份, 用于快速查找。
同时插入到每个套接字的sk_sleep中一份, 用于套接字数据到来时候, 进行通知。
static int ep_insert(struct eventpoll *ep, struct epoll_event *event,
		     struct file *tfile, int fd)
{
	......

	将函数地址ep_ptable_queue_proc赋值给epq(struct ep_pqueue)中的函数指针, 
    该函数指针主要在下面的f_op->poll中调用。
	init_poll_funcptr(&epq.pt, ep_ptable_queue_proc);

	运行tfile->f_op->poll函数, 返回当前待插入的事件对应的套接字的状态集.
	f_op->poll主要调用了sock_poll函数.
	sock_poll函数主要调用了sock->ops->poll函数.
	sock->ops->poll函数根据不同的协议有对应的函数.
	以tcp_poll函数为例:
	-----------------------------------------------------------
		tcp_poll{

			调用了上文init_poll_funcptr中赋值进去的ep_ptable_queue_proc函数。
			ep_ptable_queue_proc函数是内核触发功能的核心函数, 
			该函数将加入epoll监听中的事件挂载到了对应的网络套接字的sk_sleep上。
			sock_poll_wait(file, sk->sk_sleep, wait);

				----------------------------------------------------------------
					ep_ptable_queue_proc{
						挂载到sk_sleep上的节点结构
						struct eppoll_entry *pwq;

						......

						给wait_queue_t结构中的func函数指针赋值, 
                        ep_poll_callback将会在内核触发的时候使用。
						ep_poll_callback函数的分析在内核触发那块进行。
						init_waitqueue_func_entry(&pwq->wait, ep_poll_callback);

						针对eppoll_entry各种初始化
						pwq->whead = whead;
						pwq->base = epi;

						此处的whead是通过ep_ptable_queue_proc参数传递进来的, 
                        实参既为sock结构中的sk_sleep元素。
						这里将套接字结构与监听事件联系在了一起。
                        在sock->sk_sleep上(whead)挂载了与之有关的监听事件集合。
						add_wait_queue(whead, &pwq->wait);

						......

						将poll使用的eppoll_entry结构挂在到epi的pwqlist元素上
						list_add_tail(&pwq->llink, &epi->pwqlist);
						epi->nwait++;
	
						......
					}
				----------------------------------------------------------------

			.......
		}
	-----------------------------------------------------------
	revents = tfile->f_op->poll(tfile, &epq.pt);

	将当前epi事件插入到红黑树中.
	ep_rbtree_insert(ep, epi);

	......

	如果刚刚通过epoll_ctl插入的事件已经满足条件(就绪)
	if ((revents & event->events) && !ep_is_linked(&epi->rdllink)) {

		将该事件挂载到eventpoll的就绪事件链表中去, 供epoll_wait使用。
		list_add_tail(&epi->rdllink, &ep->rdllist);

		如果此时已经有epoll_wait在监听该eventpoll, 并处于沉睡状态.
		则调用ep->wq上注册的default_wake_function函数进行epoll_wait唤醒,
		让epoll_wait继续执行for(;;)循环, 关于default_wake_function的说明在内核触发上也有提到。
		if (waitqueue_active(&ep->wq))
			wake_up_locked(&ep->wq);

		......
	}

	......

	要是当前epi事件已经挂在到了eventpoll的rdllist上, 则进行剥离, 避免重复挂载.
	if (ep_is_linked(&epi->rdllink))
		list_del_init(&epi->rdllink);	

}

ep_remove和ep_modify操作和插入操作大同小异, 此处不只做具体分析。
其中ep_modify更改了事件状态之后, 会即刻唤醒epoll_wait函数。

epoll_wait

********************************************************************
*****	 		epoll_wait			       *****
********************************************************************
epoll_wait系统调用的入口函数既为YSCALL_DEFINE4(epoll_wait, ......

SYSCALL_DEFINE4(epoll_wait, int, epfd, struct epoll_event __user *, events,
		int, maxevents, int, timeout)
{
	......	
	判断传递进来的epfd是否是epoll_create函数创建的, epoll_wait只处理epoll_create创建的.
	if (!is_file_epoll(file))
		goto error_fput;
	
	从fd得到file结构,从file结构中拿到ep结构(epoll_create创建的)
    ep = file->private_data;

	接下来调用ep_poll函数.
	ep_poll(ep, events, maxevents, timeout)

	......
}


static int ep_poll(struct eventpoll *ep, struct epoll_event __user *events,
		   int maxevents, long timeout)
{

    对ep的自旋锁进行了加锁,并关闭软中断。在ep_poll入口处加了锁,
    就不会存在多个任务实体同时调用ep_poll的可能性。当其他任务实体准备调用ep_poll的时候,
    如果发现了其他任务已经拿到自旋锁,则进行睡眠等待,等待其释放自旋锁。
    在后续的代码中可以发现,如果未发现网络事件,会进行schedule调度,此处会释放了自旋锁。
    
    spin_lock_irqsave(&ep->lock, flags);
	......

	针对每个epoll_wait初始化一个wait(wait_queue_t)结构,
    并将其flags设置为WQ_FLAG_EXCLUSIVE,将current赋值给了wait中的private。
	WQ_FLAG_EXCLUSIVE标记主要用于内核唤醒时只唤醒一个,避免惊群现象。
    此处的值需要跟网络数据包到来时进行的触发唤醒区分开,此处唤醒主要用于当处理一波数据之后,
    如果发现rdllist还有数据(未处理完或者LT模式下又重新将epi加入进来),则进行唤醒A。
	init_waitqueue_entry(&wait, current);
	wait.flags |= WQ_FLAG_EXCLUSIVE;

	将wait(wait_queue_t结构)加入到eventpoll的wq链表元素上, 主要在内核唤醒时使用
	__add_wait_queue(&ep->wq, &wait)

	for (;;) {

		设置当前睡眠的task_struct 可以被打断
		set_current_state(TASK_INTERRUPTIBLE);

		如果当前eventpoll事件上的监听事件已经就绪或者监听已经超时,
		则跳出循环.
		ep->rdllist的就绪节点是内核在数据到来时,执行触发的时候加入的.
		if (!list_empty(&ep->rdllist) || !jtimeout)
			break;

		检查是否有信号需要处理
		if (signal_pending(current)) {
			res = -EINTR;
			break;
		}

		进入延时唤醒状态, 让出cpu, 同时会对超时计时jtimeout进行减法操作.
        这时候会释放锁,锁就有可能被别人拿到。
        spin_unlock_irqrestore(&ep->lock, flags);	
		jtimeout = schedule_timeout(jtimeout);
        spin_lock_irqsave(&ep->lock, flags);
	}	

	事件来临时, 跳出for(;;)循环后从链表上删除wait
	__remove_wait_queue(&ep->wq, &wait);

	......

	epoll_wait进程唤醒后, 设置task标记为running态
	set_current_state(TASK_RUNNING);

    判断ovflist是否未空或者rdllist是否为空,并以次为根据进行网络事件处理。
    !list_empty(&ep->rdllist) || ep->ovflist != EP_UNACTIVE_PTR;

    释放掉自旋锁,接下来开始处理rdllist和ovflist。
    如果有的话,其他任务实体可以紧着获取自旋锁进行事件等待。
    spin_unlock_irqrestore(&ep->lock, flags);

	调用ep_send_events函数,将就绪的event拷贝到用户态的events数组中来. 
	返回的时数据就绪的event事件数.
	res = ep_send_events(ep, events, maxevents)
	return res;
}


static int ep_send_events(struct eventpoll *ep,
			  struct epoll_event __user *events, int maxevents)
{
	struct ep_send_events_data esed;
	esed.maxevents = maxevents;
	esed.events = events;
	调用 ep_scan_ready_list函数
	return ep_scan_ready_list(ep, ep_send_events_proc, &esed);
}


static int ep_scan_ready_list(struct eventpoll *ep,
			      int (*sproc)(struct eventpoll *,
					   struct list_head *, void *),
			      void *priv)
{
    整个处理过程都会加上mutex锁。同时在处理ep相应的数据时候也会进行ep->lock自旋锁的加锁和解锁。
    mutex_lock(&ep->mtx);

	将就绪事件的链表转移给txlist.
	list_splice_init(&ep->rdllist, &txlist);

	......

	将ep的ovflist从初始化的EP_UNACTIVE_PTR变为NULL.
	因为在sproc函数调用中要对rdllist链表进行遍历, 
	并实现从内核态向用户态的拷贝操作,更改ovflist的值,
	并配合内核触发部分,实现类似锁的保护.
	ep->ovflist = NULL;

	执行sproc传入的回调函数, 既调用ep_send_events_proc函数, 在下面进行了该函数分析.
	error = (*sproc)(ep, &txlist, priv)

	当执行sproc进行内核态向用户态的拷贝时, ep的rdllist链表是不允许插入的.
	此时内核触发部分会将满足条件的epi事件插入到ovflist中.
	下面则是将ovflist链表上的部分加入到eventpoll的rdllist上,并做稍后处理.
	for (nepi = ep->ovflist; (epi = nepi) != NULL;
	     nepi = epi->next, epi->next = EP_UNACTIVE_PTR) {
		if (!ep_is_linked(&epi->rdllink))
			list_add_tail(&epi->rdllink, &ep->rdllist);
	}	

	拷贝操作结束, 将ep->ovflist的值还原为初始化值,
    此时触发部分通过ovflist初始值判断,即可将到来的时间插入到rdllist队列中。
	ep->ovflist = EP_UNACTIVE_PTR;

	将sproc中未完全剥离的剩余链表上的epi事件加入到rdllist上.
	list_splice(&txlist, &ep->rdllist);

	如果ep->rdllist上还有节点数据, 有可能是ovflist上拷贝过来的,
	有可能是txlist上剩余的, 也有可能是使用LT模式重新挂载过来的等,
	则进行进程唤醒操作, 唤醒之后睡眠的任务实体则会继续将epoll_wait重新执行起来,
    将rdllist上的数据进行及时处理。此处的唤醒就是上面提到的唤醒A。
	if (!list_empty(&ep->rdllist)) {
        //此处主要是通过!list_empty(&q->task_list)来检测ep->wq的任务队列是否为空。
		if (waitqueue_active(&ep->wq))
			wake_up_locked(&ep->wq);
		......
	}	

	......
}

tatic int ep_send_events_proc(struct eventpoll *ep, struct list_head *head,
			       void *priv)
{
	其中priv指向了传递进来的struct ep_send_events_data结构.
	......

	遍历head链表, 将就绪的事件以此拷贝到esed结构中.
	for (eventcnt = 0, uevent = esed->events;
	     !list_empty(head) && eventcnt < esed->maxevents;) {

		从链表上依次取下每个节点结构
		epi = list_first_entry(head, struct epitem, rdllink);

		将该节点从链表上进行剥离
		list_del_init(&epi->rdllink);

		返回当前套接字的event状态, 此处不进行具体分析.
		在epoll_ctl函数中也调用了poll函数.
		revents = epi->ffd.file->f_op->poll(epi->ffd.file, NULL) &
			epi->event.events;

		if (revents) {
			如果从内核态向用户态拷贝失败, 则将剥离下来的epi事件重新加入到txlist中.
			if (__put_user(revents, &uevent->events) ||
			    __put_user(epi->event.data, &uevent->data)) {
				list_add(&epi->rdllink, head);
				return eventcnt ? eventcnt : -EFAULT;
			}
			eventcnt++;
			uevent++;
			
			当监听的event事件类型中加入了EPOLLONESHOT态, 
			既除去了EPOLLIN或者EPOLLOUT等基本类型, 内核就不会进行第二次触发.
			相见内核触发分析.
			if (epi->event.events & EPOLLONESHOT)
				epi->event.events &= EP_PRIVATE_BITS;

			当监听的event事件类型中没有加入EPOLLET类型,
			则将该事件继续添加到ep的rdllist中.也就是在水平触发模式下,
            如果通过poll函数得到了对应的网络事件的话,epi->rdllink会接着加入到txlist中。
            而在后续的操作中,剩余的txlist会继续加入到rdllist。
			else if (!(epi->event.events & EPOLLET)) {
				list_add_tail(&epi->rdllink, &ep->rdllist);
			}
		}	
	}

	......
}

就绪事件触发


********************************************************************
*****	 		内核触发入口			       *****
********************************************************************

epoll机制是通过内核唤醒来进行触发的, 其基本的操作函数主要是sock_def_readable和sock_def_write_space.
其中sock_def_readable是EPOLLIN的触发入口,sock_def_write_space是EPOLLOUT的触发入口.

一般当有数据到来时候会执行sock_def_readable函数,
缓冲区有空间可写入时候会执行sock_def_write_space函数, 
此处不对函数调用上下文进行分析.

sock_def_readable
{
	sk_has_sleeper(sk);
	其中的sk_has_sleeper函数主要用来判断当前sock套接字结构中的sk_sleep元素上是否有具体的挂载节点。
	挂载点结构为wait_queue_head_t结构, 其中的task_list(struct list_head)为真正的挂载点。
	挂载chain上的节点结构为eppoll_entry结构,
	其钩子为内部元素wait(wait_queue_t结构),eppoll_entry中存储有元素base(struct epitem结构)。
	每个挂载点都可以查找到对应的已加入的监听事件, 具体挂载操作在epoll_ctl函数中。
	......
	wake_up_interruptible_sync_poll --> 宏定义 __wake_up_sync_key
	......
}

__wake_up_sync_key
{
	调用了__wake_up_common函数
}

__wake_up_common
{
	遍历sock结构中的sk_sleep上的task_list循环链表, 当当前的套接字缓冲区有数据时,
    需要遍历注册到该sk_sleep上的所有的监听事件。

	list_for_each_entry_safe(curr, next, &q->task_list, task_list) {

	执行每一个wait_queue_t结构中的func函数, func函数也是在epoll_ctl中进行初始化的。
	此处的func函数对应的函数为ep_poll_callback, 当func函数为default_wake_function时,
	WQ_FLAG_EXCLUSIVE可用于防止部分唤醒惊群使用,见上述唤醒A。
    此处的func函数对应ep_poll_callback。
	做惊群使用时, 每个epoll_wait的时候, 会生成一个wait结构, 挂载到ep->wq链表上。
	既多个epoll_wait共同进行时,ep->wq会挂在多个节点,
	但是wake唤醒的时候遇到WQ_FLAG_EXCLUSIVE标记退出, 则只唤醒了第一个。
	    if (curr->func(curr, mode, wake_flags, key) &&
				(flags & WQ_FLAG_EXCLUSIVE) &&
				!--nr_exclusive)
	}	
}

curr->func是在ep_ptable_queue_proc函数中进行赋值的,
此处针对ep_ptable_queue_proc函数的调用发生在epoll_ctl中, 具体分析在epoll_ctl函数中进行。

ep_poll_callback
{
	从sk_sleep(也有可能是从ep->wq)上得到每一个wait_queue_t结构,
	从wait_queue_t结构可以得到包含该结构的eppoll_entry结构。
	eppoll_entry是由对应的wait_queue_t和epitem结构组成的。

	struct epitem *epi = ep_item_from_wait(wait);

	如果注册过的epi事件中,
	除了EPOLLONESHOT和EPOLLET等特殊标识之外,没有其他的基础标识如EPOLLOUT和EPOLLIN,
	则直接跳出不对此epi事件进行处理。
	if (!(epi->event.events & ~EP_PRIVATE_BITS))
		goto out_unlock;

	匹配chain上的每一个epitem里面的events类型(EPOLLIN, EPOLLOUT, EPOLLET,EPOLLLT)是否跟key匹配。
	key里的类型在此处为POLLIN类型, 当然在sock_def_write_space函数里传递过来的为POLLOUT类型。
	if (key && !((unsigned long) key & epi->event.events))
		goto out_unlock;

	如果此时ep->ovflist的值不是EP_UNACTIVE_PTR, 
    说明此刻epoll_wait正在调用ep_scan_ready_list,
	而在ep_scan_ready_list中正在进行内核态向用户态的拷贝操作。
	此时不能直接将epi挂在到ep的rdllist上, 暂时性的挂载到ovflist上。
	if (unlikely(ep->ovflist != EP_UNACTIVE_PTR)) {
		if (epi->next == EP_UNACTIVE_PTR) {
			epi->next = ep->ovflist;
			ep->ovflist = epi;
		}
		goto out_unlock;
	}

	判断该epi事件的rdllink链表元素是否未挂载, 未挂载则将其挂载到eventpoll主监听集上, 供epoll_wait使用。
	要是该事件的rdllink为非空(已经在rdllist上了), 则不进行挂载。
	if (!ep_is_linked(&epi->rdllink))
		list_add_tail(&epi->rdllink, &ep->rdllist);

	当有epoll_wait监听的时候, 会在ep链表上挂载一个wait进程事件,
	当数据到来时候好唤醒沉睡的epoll_wait。
	挂载操作在epoll_wait函数的for(;;)循环之前进行的,
	具体操作在epoll_wait中进行分析。
	if (waitqueue_active(&ep->wq))
		wake_up_locked(&ep->wq);

	其中wake_up_locked也会调用上文中提到的__wake_up_common。
	但此处__wake_up_common函数中调用的curr->func则与上文不同。
	ep->wq链表上的节点挂载是在epoll_wait中执行的,在挂载之前对元素func进行了赋值,
	此处调用的是default_wake_function, 该函数主要用于唤醒沉睡的epoll_wait进程。
	
	......
}


sock_def_write_space函数分析基本同sock_def_readable一致, 此处不再进行具体分析.
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值