通俗易懂说多路复用(2-2)epoll源码解析

1. epoll_create 功能及源码实现

该接口是在内核区创建一个epoll相关的一些列结构(eventpoll),并且将一个句柄fd返回给用户态,后续的操作都是基于此fd的.
之后应用程序在用户态使用epoll的时候都将依靠这个文件描述符,而在epoll内部也是通过该文件描述符进一步获取到eventpoll类型对象,
再进行对应的操作,完成了用户态和内核态的贯穿。

long sys_epoll_create(int size) 
{
	struct eventpoll *ep;
	ep_alloc(&ep); // 为ep分配内存并进行初始化
	fd = anon_inode_getfd("[eventpoll]", &eventpoll_fops, ep, O_RDWR | (flags & O_CLOEXEC));
	return fd;
}

2. epoll_ctl 功能及源码实现

2.1 epoll_ctl 功能及源码实现

该接口是将fd添加/删除于epoll_create返回的epfd中,其中 epoll_event 是用户态和内核态交互的结构,定义了用户态关心的事件类型和触发时数据的载体epoll_data
epoll_ctl 函数的功能是对文件描述符进行增删改查。

asmlinkage long sys_epoll_ctl(int epfd, int op, int fd, struct epoll_event __user *event) 
{
	int error;
	struct file *file,*tfile;
	struct eventpoll *ep;
	struct epoll_event epds;
	error = -FAULT;

	// 判断参数的合法性,将 __user *event 复制给 epds。
	if(ep_op_has_event(op) && copy_from_user(&epds,event,sizeof(struct epoll_event)))
		goto error_return; //省略跳转到的代码
	file  = fget (epfd); // epoll fd 对应的文件对象
	tfile = fget(fd);    // fd 对应的文件对象

	// 在create时存入进去的(anon_inode_getfd),现在取用。
	ep = file->private->data;
	mutex_lock(&ep->mtx);

	// 防止重复添加(在ep的红黑树中查找是否已经存在这个fd)
	epi = epi_find(ep, tfile, fd);

	switch(op)
	{
		...
		case EPOLL_CTL_ADD:  // 增加监听一个fd
			if (!epi)
			{
				epds.events |= EPOLLERR | POLLHUP;     // 默认包含POLLERR和POLLHUP事件。
				error = ep_insert(ep, &epds, tfile, fd);  // 在ep的红黑树中插入这个fd对应的epitm结构体。
			} else  // 重复添加(在ep的红黑树中查找已经存在这个fd)。
				error = -EEXIST;
			break;
		...
	}
	return error;
}

2.2 ep_insert 源码实现

epoll_ctl 最后调用了 ep_insert
ep_insert 函数的功能是插入一个文件描述符到红黑树上。

2.2.1 ep_insert 功能及源码实现

创建并初始化一个strut epitem 类型的对象,完成该对象和被监控事件以及epoll对象eventpoll的关联;
将struct epitem类型的对象加入到epoll对象eventpoll的红黑树中管理起来;
将struct epitem类型的对象加入到被监控事件对应的目标文件的等待列表中,并注册事件就绪时会调用的回调函数,在epoll中该回调函数就是 ep_poll_callback();
ovflist主要是暂态处理,调用ep_poll_callback()回调函数的时候发现eventpoll的ovflist成员不等于EP_UNACTIVE_PTR,说明正在扫描rdllist链表,这时将就绪事件对应的epitem加入到ovflist链表暂存起来,等rdllist链表扫描完再将ovflist链表中的元素移动到rdllist链表;

static int ep_insert(struct eventpoll *ep, struct epoll_event *event, struct file *tfile, int fd)
{
	/* 初始化 epitem */

	// 设置监听注册函数为ep_ptable_queue_proc, &epq.pt->qproc = ep_ptable_queue_proc
	init_poll_funcptr(&epq.pt, ep_ptable_queue_proc);

	/*
	* 执行f_op->poll(tfile, &epq.pt)时,XXX_poll(tfile, &epq.pt)函数会执行poll_wait(),
	* poll_wait()会调用epq.pt.qproc函数,即ep_ptable_queue_proc。
	*/
	revents = tfile->f_op->poll(tfile, &epq.pt);

	spin_lock(&tfile->f_ep_lock);

	// 将 epitem 与它需要监听的文件链接起来, list_add_tail 是内核函数,表示添加一个结点到链表尾部
	list_add_tail(&epi->fllink, &tfile->f_ep_lilnks);
	spin_unlock(&tfile->f_ep_lock);

	// 将epitem插入红黑树
	ep_rbtree_insert(ep, epi);

	/*
	* 如果要监视的fd状态改变,并且还没有加入到就绪链表中,则将当前的
	* epitem加入到就绪链表中。如果有进程正在等待该文件的状态就绪,则
	* 唤醒一个等待的进程。
	*/
	if ((revents & event->events) && !ep_is_linked(&epi->rdllink)) 
	{
		list_add_tail(&epi->rdllink, &ep->rdllist);

		/* 唤醒epoll_wait()当前epoll实例的用户 */
		if (waitqueue_active(&ep->wq))
			wake_up_locked(&ep->wq);

		/* 当前epoll文件已就绪 */
		if (waitqueue_active(&ep->poll_wait))
			pwake++;
	}
}

2.2.2 红黑树、双链表、epitem之间的关系:

在这里插入图片描述

3. epoll_wait 功能及源码实现

该接口是阻塞等待内核返回的可读写事件, events 是个结构体数组指针存储 epoll_event,也就是将内核返回的待处理epoll_event结构都存储下来,
maxevents 告诉内核本次返回的最大fd数量,这个和events指向的数组是相关的;

3.1 epoll_wait 实现步骤

  1. epoll_wait 调用 ep_poll ,当 rdlist 为空(无就绪fd)时挂起当前进程,直到rdlist不空时进程才被唤醒。
  2. 然后就将就绪的events和data发送到用户空间(ep_send_events()),
    如果 ep_send_events()返回的事件数为0,并且还有超时时间剩余(jtimeout),那么我们retry,期待不要空手而归。

3.2 epoll_wait 源码实现

static int ep_poll(struct eventpoll *ep, struct epoll_event __user *events, int maxevents, long timeout)
{
	int res, eavail;
	unsigned long flags;
	long jtimeout;
	wait_queue_t wait;

	/* 处理睡眠时间:将毫秒数转化为HZ */
	jtimeout = (timeout < 0 || timeout >= EP_MAX_MSTIMEO) ?
		MAX_SCHEDULE_TIMEOUT : (timeout * HZ + 999) / 1000;

retry:
	/* spinlock 加锁:保护eventpoll的访问 */
	spin_lock_irqsave(&ep->lock, flags);

	res = 0;

	/* 如果就绪链表为空,说明还没有任何events就绪 */
	if (list_empty(&ep->rdllist)) 
	{
		/* 初始化等待队列节点,设置等待状态为互斥等待 */
		init_waitqueue_entry(&wait, current);
		wait.flags |= WQ_FLAG_EXCLUSIVE;

		/* 将刚刚初始化的等待队列节点挂载到 eventpoll 中的等待队列 */
		__add_wait_queue(&ep->wq, &wait);

		for (;;) 
		{
			/* 
			* 设置程序运行状态为可中断阻塞,因为我们希望能够接收到
			* ep_insert()、ep_modify()、ep_poll_callback()的唤醒
			*/
			set_current_state(TASK_INTERRUPTIBLE);

			/* events 就绪或者超时,跳出循环 */
			if (!list_empty(&ep->rdllist) || !jtimeout)
				break;

			/* 出现未决信号,设置返回值为-EINTR并跳出循环 */
			if (signal_pending(current)) 
			{
				res = -EINTR;
				break;
			}

			spin_unlock_irqrestore(&ep->lock, flags);
			/* 休眠...等待超时或者被就绪资源唤醒 */
			jtimeout = schedule_timeout(jtimeout);
			spin_lock_irqsave(&ep->lock, flags);
		}
		/* 从等待队列中卸载 */
		__remove_wait_queue(&ep->wq, &wait);

		/* 恢复程序运行状态 */
		set_current_state(TASK_RUNNING);
	}

	/* 判断是否有资源就绪 */
	eavail = !list_empty(&ep->rdllist) || ep->ovflist != EP_UNACTIVE_PTR;

	spin_unlock_irqrestore(&ep->lock, flags);

	if (!res && eavail && !(res = ep_send_events(ep, events, maxevents)) && jtimeout)
		goto retry;

	return res;
}

3.2.1 ep_send_events

  1. ep_send_events 函数,它扫描 txlist 中的每个epitem,调用其关联的fd对应的的poll方法,取得fd上较新的events(防止之前events被更新)即revents,

  2. 之后将 revents 和相应的data拷贝(__put_user())到用户空间。

  3. 如果这个epitem对应的fd是LT模式监听且取得的events是用户所关心的,则将其重新加入回rdlist,如果是ET模式则不再加入rdlist。

    // 将就绪的events传递到用户空间
    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;
    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)
    {
    int error, pwake = 0;
    unsigned long flags;
    struct epitem *epi, nepi;
    /
    初始化一个链表 */
    LIST_HEAD(txlist);

     /* mutex 加锁 */
     mutex_lock(&ep->mtx);
    
     /* spinlock 加锁:保护eventpoll的访问 */
     spin_lock_irqsave(&ep->lock, flags);
    
     /*
     * 将eventpoll就绪链表中的所有节点全部链接到 txlist 上,
     * 之后eventpoll就绪队列为空
     */
     list_splice_init(&ep->rdllist, &txlist);
    
     /* 
     * 设置eventpoll.ovflist,使得接下来新就绪的events被挂载到
     * eventpoll.ovflist而不是就绪队列 
     */
     ep->ovflist = NULL;
     spin_unlock_irqrestore(&ep->lock, flags);
     error = (*sproc)(ep, &txlist, priv);
    
     /* spinlock加锁:保护eventpoll的访问 */
     spin_lock_irqsave(&ep->lock, flags);
    
     /* 
     * 我们在调用ep_send_events_proc()将就绪队列中的事件交付
     * 给用户的期间,新就绪的events被挂载到eventpoll.ovflist
     * 所以我们需要遍历eventpoll.ovflist将所有已就绪的epitem
     * 重新挂载到就绪链表中,等待下一次epoll_wait()进行交付
     */
     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 = EP_UNACTIVE_PTR;
    
     /* 
     * 将调用ep_send_events_proc()之后剩余的未交付的epitem重新splice到
     * eventpoll的就绪链表上 
     */
     list_splice(&txlist, &ep->rdllist);
    
     /* 
     * 注意到epoll_wait()中,将wait_queue_t的等待状态设置为互斥等待,因此
     * 每次被唤醒的只有一个节点。现在我们已经将eventpoll中就绪队列里的事件
     * 尽量向用户交付了,但是在交付时,可能没有交付完全(1.交付过程中出现了
     * 错误 2.使用了LT模式),也有可能在过程中又发生了新的事件。也就是这次
     * epoll_wait()调用后,还剩下一些就绪资源,那么我们再次唤醒一个等待节点
     * 让别的用户也享用一下资源
     *
     * 从这里已经可以看出内核对于epoll惊群的解决方案:ET模式:
     * 1. 每次只唤醒一个节点
     * 2. 事件交付后不再将事件重新挂载到就绪队列
     */
     if (!list_empty(&ep->rdllist)) 
     {
     	/* 唤醒epoll_wait()当前epoll实例的用户 */
     	if (waitqueue_active(&ep->wq))
     		wake_up_locked(&ep->wq);
    
     	/* 当前epoll文件已就绪 */
     	if (waitqueue_active(&ep->poll_wait))
     		pwake++;
     }
     spin_unlock_irqrestore(&ep->lock, flags);
     mutex_unlock(&ep->mtx);
     if (pwake)
     	ep_poll_safewake(&ep->poll_wait);
     return error;
    

    }

    // 将 events 和 data 发送到用户空间
    static int ep_send_events_proc(struct eventpoll *ep, struct list_head *head, void *priv)
    {
    struct ep_send_events_data *esed = priv;
    int eventcnt;
    unsigned int revents;
    struct epitem *epi;
    struct epoll_event __user *uevent;

     /* 
     * 遍历就绪链表,eventcnt 记录已交付的events的数量
     * uevent指向esed中封装的events数组,这个数组用于将已就绪events返回给用户
     */
     for (eventcnt = 0, uevent = esed->events;!list_empty(head) && eventcnt < esed->maxevents;) 
     {
     	epi = list_first_entry(head, struct epitem, rdllink);
    
     	/* 将epitem从head就绪队列中卸载 */
     	list_del_init(&epi->rdllink);
    
     	/* 从资源文件当前状态中提取出我们所关心的events,拿到最新的数据 */
     	revents = epi->ffd.file->f_op->poll(epi->ffd.file, NULL) & epi->event.events;
    
     	/* 如果有我们所关心的events发生 */
     	if (revents) 
     	{
     		/* 
     		* 使用__put_user将数据在用户空间和内核空间互相拷贝
     		*
     		* 将events拷贝到用户空间
     		*
     		* 若拷贝失败,那么就将该epitem重新添加到就绪链表上,
     		* 然后返回已交付的events的数量
     		*/
     		if (__put_user(revents, &uevent->events) || __put_user(epi->event.data, &uevent->data)) 
     		{
     			/* 复制失败了 */
     			list_add(&epi->rdllink, head);
     			return eventcnt ? eventcnt : -EFAULT;
     		}
    
     		/* 更新已交付的event的数量 */
     		eventcnt++;
    
     		/* 指向events数组中的下一元素 */
     		uevent++;
    
     		/* 
     		* 如果是ET模式, epitem是不会再进入到就绪链表,
     		* 除非fd再次发生了状态改变, ep_poll_callback 被调用。
     		* 如果是LT模式,不但会将对应的数据返回给用户,并且会将当前的epitem再次加入到rdllist中。
     		* 这样如果下次再次被唤醒就会给用户空间再次返回事件。当然如果这个
     		* 被监听的fd确实没事件也没数据了, epoll_wait会返回一个0。
     		*/
     		if (epi->event.events & EPOLLONESHOT)
     			epi->event.events &= EP_PRIVATE_BITS;
     		else if (!(epi->event.events & EPOLLET)) 
     		{
     			list_add_tail(&epi->rdllink, &ep->rdllist);
     		}
     	}
     }
     return eventcnt;
    

    }

4. epoll 中涉及的 epitem 和 eventpoll

4.1 epitem 和 eventpoll 关系

可以简单的认为 epitem 是和每个用户态监控IO的fd对应的,epitem 是一个红黑树结构。
eventpoll 是用户态创建的管理所有被监控fd的结构,用于管理所有的 epitem。

4.2 epitem 和 eventpoll 源码

struct epitem 
{
	struct rb_node  rbn;        // 用于主结构管理的红黑树
	struct list_head  rdllink;  // 事件就绪链表
	struct epitem  *next;       // 用于主结构体中的链表
	struct epoll_filefd  ffd;   // 这个结构体对应的被监听的文件描述符信息
	int  nwait;                 // poll操作中事件的个数
	struct list_head  pwqlist;  // 双向链表,保存着被监视文件的等待队列
	struct eventpoll  *ep;      // 该项属于哪个主结构体(多个epitm从属于一个eventpoll)
	struct list_head  fllink;   // 双向链表,用来链接被监视的文件描述符对应的struct
	struct epoll_event  event;  // 注册的感兴趣的事件,也就是用户空间的epoll_event
}

struct eventpoll
{
	spin_lock_t       lock;         // 对本数据结构的访问
	struct mutex      mtx;          // 防止使用时被删除
	/*
	* 等待队列可以看作保存进程的容器,在阻塞进程时,将进程放入等待队列;
	* 当唤醒进程时,从等待队列中取出进程
	*/ 
	wait_queue_head_t   wq;         // sys_epoll_wait() 使用的等待队列
	wait_queue_head_t   poll_wait;  // file->poll()使用的等待队列
	struct list_head    rdllist;    // 就绪链表 --- 可读写的 fd
	struct rb_root      rbr;        // 用于管理所有fd的红黑树(树根)  ---挂载 epitem
	struct epitem      *ovflist;    // 将事件到达的fd进行链接起来发送至用户空间
}

5. epoll官方demo

man epoll

#define MAX_EVENTS 10
struct epoll_event ev, events[MAX_EVENTS];
int listen_sock, conn_sock, nfds, epollfd;

/* Set up listening socket, 'listen_sock' (socket(),
bind(), listen()) */

epollfd = epoll_create(10);
if(epollfd == -1) {
perror("epoll_create");
exit(EXIT_FAILURE);
}

ev.events = EPOLLIN;
ev.data.fd = listen_sock;
if(epoll_ctl(epollfd, EPOLL_CTL_ADD, listen_sock, &ev) == -1) {
perror("epoll_ctl: listen_sock");
exit(EXIT_FAILURE);
}

for(;;) {
nfds = epoll_wait(epollfd, events, MAX_EVENTS, -1);
if (nfds == -1) {
	perror("epoll_pwait");
	exit(EXIT_FAILURE);
}

for (n = 0; n < nfds; ++n) {
	if (events[n].data.fd == listen_sock) {
		//主监听socket有新连接
		conn_sock = accept(listen_sock,
						(struct sockaddr *) &local, &addrlen);
		if (conn_sock == -1) {
			perror("accept");
			exit(EXIT_FAILURE);
		}
		setnonblocking(conn_sock);
		ev.events = EPOLLIN | EPOLLET;
		ev.data.fd = conn_sock;
		if (epoll_ctl(epollfd, EPOLL_CTL_ADD, conn_sock,
					&ev) == -1) {
			perror("epoll_ctl: conn_sock");
			exit(EXIT_FAILURE);
		}
	} else {
		//已建立连接的可读写句柄
		do_use_fd(events[n].data.fd);
	}
}
}

参考:
https://blog.csdn.net/daaikuaichuan/article/details/88770427
https://blog.csdn.net/chinesehuazhou2/article/details/108353712

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值