一、epoll相关的数据结构
最重要的两个数据结构是红黑树和就绪链表,红黑树用于管理所有的文件描述符fd,就绪链表用于保存有事件发生的文件描述符。
当向系统中添加一个fd时,就创建一个epitem结构体。eventpoll用于管理所有的epitem。
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 mutexmtx;// 防止使用时被删除 /** 等待队列可以看作保存进程的容器,在阻塞进程时,将进程放入等待队列;* 当唤醒进程时,从等待队列中取出进程*/wait_queue_head_t wq; // sys_epoll_wait() 使用的等待队列 wait_queue_head_t poll_wait; // file->poll()使用的等待队列 struct list_head rdllist; // 就绪链表 struct rb_rootrbr; // 用于管理所有fd的红黑树(树根) struct epitem*ovflist; // 将事件到达的fd进行链接起来发送至用户空间}1234567891011121314151617181922232425262728
在这里给大家强烈推荐一个视频linux下的epoll实战揭秘——支撑亿级IO的底层基石
另外需要C/C++ Linux服务器架构师学习资料私信“资料”(资料包括C/C++,Linux,golang技术,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg等),免费分享
二、epoll_create函数
epoll_create函数的功能是创建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;}1234567
三、epoll_ctl函数
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;}12345678910111213141516171819222324252627282930313233343536
四、ep_insert函数
ep_insert函数的功能是插入一个文件描述符到红黑树上。
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_procinit_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++; }}1234567891011121314151617181922232425262728293031323334353637383940
五、ep_poll_callback函数
所以ep_poll_callback函数主要的功能是当被监视文件的等待事件就绪时,将文件描述符对应的epitem实例添加到就绪链表中,导致rdlist不空,进程被唤醒,epoll_wait()得以继续执行,之后内核会将就绪链表中的事件从内核空间拷贝到用户空间。
// 将epitem挂载到资源文件的监听队列上static void ep_ptable_queue_proc(struct file *file, wait_queue_head_t *whead, poll_table *pt){ /* 获取epitem */ struct epitem *epi = ep_item_from_epqueue(pt); struct eppoll_entry *pwq; /* 从slab分配一个eppoll_entry结构,然后进行相应的初始化 */ if (epi->nwait >= 0 && (pwq = kmem_cache_alloc(pwq_cache, GFP_KERNEL))){ /** 初始化一个等待队列节点,其中唤醒函数设置为ep_poll_callback * 唤醒回调函数为ep_poll_callback!!! */ init_waitqueue_func_entry(&pwq->wait, ep_poll_callback);/* 还要保存资源文件监听队列的队列头whead */ pwq->whead = whead; pwq->base = epi; /** 将eppoll_entry挂载到资源文件的监听队列中, * add_wait_queue表示将队列元素加入到等待队列头部,并设置非互斥等待 */ add_wait_queue(whead, &pwq->wait); list_add_tail(&pwq->llink, &epi->pwqlist); /* 增加等待计数 */ epi->nwait++; } else { epi->nwait = -1; }}static int ep_poll_callback(wait_queue_t *wait, unsigned mode, int sync, void *key){ int pwake = 0; unsigned long flags; /* 通过eppoll_entry中的wait获取对应的epitem */ struct epitem *epi = ep_item_from_wait(wait); /* 获取epitem所属的eventpoll */ struct eventpoll *ep = epi->ep; /* spinlock加锁(不允许休眠):保护eventpoll的访问 */ spin_lock_irqsave(&ep->lock, flags); /* 如果我们想要监听的事件events为空,那么资源文件就绪时,nothing to do */ if (!(epi->event.events & ~EP_PRIVATE_BITS)) goto out_unlock; /* 判断当前fd状态是否是我们关心的事件events */ if (key && !((unsigned long) key & epi->event.events)) goto out_unlock; if (unlikely(ep->ovflist != EP_UNACTIVE_PTR)){ if (epi->next == EP_UNACTIVE_PTR) { epi->next = ep->ovflist; ep->ovflist = epi; } goto out_unlock; } /* 如果epitem没有被挂载到所属eventpoll中的就绪链表,就将其添加到就绪链表尾 */ if (!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++;out_unlock: spin_unlock_irqrestore(&ep->lock, flags); if (pwake) ep_poll_safewake(&ep->poll_wait); return 1;}12345678910111213141516171819222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687
六、epoll_wait函数
epoll_wait调用ep_poll,当rdlist为空(无就绪fd)时挂起当前进程,直到rdlist不空时进程才被唤醒。然后就将就绪的events和data发送到用户空间(ep_send_events()),如果ep_send_events()返回的事件数为0,并且还有超时时间剩余(jtimeout),那么我们retry,期待不要空手而归。
SYSCALL_DEFINE4(epoll_wait, int, epfd, struct epoll_event __user *, events, int, maxevents, int, timeout){ int error; struct file *file; struct eventpoll *ep; /* 参数验证 */ if (maxevents <= 0 || maxevents > EP_MAX_EVENTS) return -EINVAL; /* 验证events数组区域,当前用户是否能够访问 */ if (!access_ok(VERIFY_WRITE, events, maxevents * sizeof(struct epoll_event))){ error = -EFAULT; goto error_return; } error = -EBADF; /* 获取eventpoll文件描述符对应的struct file结构 */ file = fget(epfd); if (!file) goto error_return; error = -EINVAL; /* 验证epfd指向的文件是否是epoll文件 */ if (!is_file_epoll(file)) goto error_fput; /* 取出挂载到epoll文件中的eventpoll */ ep = file->private_data; /* 调用ep_poll()等待事件的到来 */ error = ep_poll(ep, events, maxevents, timeout);error_fput: fput(file);error_return: return error;}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;}12345678910111213141516171819222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111
七、ep_send_events函数
ep_send_events函数,它扫描txlist中的每个epitem,调用其关联的fd对应的的poll方法,取得fd上较新的events(防止之前events被更新)即revents,之后将revents和相应的data拷贝(__put_user())到用户空间。如果这个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;}1234567891011121314151617181922232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811911122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
八、epoll的工作过程详解
通过上面的源码分析可以看出epoll的工作过程如下:
1.epoll_wait()调用ep_poll(),如果就绪链表rdlist为空,则挂起当前进程,直到rdlist不为空时被唤醒,这个时候会调用ep_send_events()将实际发生的事件revents和data从内核空间拷贝到用户空间(拷贝调用的是__put_user,并不存在什么共享内存之类的)。
2. 当文件描述符fd的状态改变时(buffer由不可读变为可读或由不可写变为可写),导致相应fd上的回调函数ep_poll_callback()被调用。
3.ep_poll_callback()将有事件发生的文件描述符(epitem)加入到就绪链表rdlist中,这时候就绪链表不为空,epoll_wait()进程被唤醒。
4.ep_send_events()会扫描就绪链表,调用每个文件描述符的poll函数返回revents,之后将revents和data从内核空间拷贝到用户空间。如果是ET模式, epitem是不会再进入到就绪链表,除非fd再次发生了状态改变,ep_poll_callback被调用。如果是LT模式,不但会将对应的数据返回给用户,并且会将当前的epitem再次加入到rdllist中。这样如果下次再次被唤醒就会给用户空间再次返回事件。