通俗易懂说多路复用(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 实现步骤
- epoll_wait 调用 ep_poll ,当 rdlist 为空(无就绪fd)时挂起当前进程,直到rdlist不空时进程才被唤醒。
- 然后就将就绪的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
-
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;
}
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