epoll源码分析(三)
epoll_wait函数
SYSCALL_DEFINE4(epoll_wait,…)
- 判断最大值合法性
- 获取匿名文件的文件指针, 取得文件信息
- 调用
ep_poll
阻塞自己, 等待有消息的到来
// epoll_wait()函数的调用
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;
// 获取epoll_create()函数创建的匿名文件的文件描述符
file = fget(epfd);
if (!file)
goto error_return;
...
error = -EINVAL;
// 判断类型是不是epoll类型
if (!is_file_epoll(file))
goto error_fput;
// 获取文件中保存的信息
ep = file->private_data;
// 等待就绪队列不为空, 也就是等待有要接受的消息到来
error = ep_poll(ep, events, maxevents, timeout);
...
}
epoll_wait()中最重要的函数其实是ep_poll
, 它会完成等待消息到来, 执行, 没有消息到来就阻塞自己. 可以看出来这是epoll_wait的核心.
ep_poll
- 先设置好时间长度, 如果传过来的已经设置好的时间, 就设置为传入 的时间, 没有的话就默认一个初始时间.
- 初始化就绪的等待队列
- 对有没有进程准备好, 有消息传来或者对时间是否到达进行判断, 如果满足就退出循环, 执行进程.
- 如果没有满足就执行进程调度, 让出cpu, 让其他的进程执行, 等待一定的时间后返回该进程继续进行判断, 有准备好的执行, 没有继续让出cpu.
- 调用
ep_send_evnets
将就绪链表中的数据返回给用户空间.
// 设置进程的为可中断, 一直等待有事件的到来. 没哟到来就被调度
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;
...
// 设置运行的时间节拍
jtimeout = (timeout < 0 || timeout >= EP_MAX_MSTIMEO) ?
MAX_SCHEDULE_TIMEOUT : (timeout * HZ + 999) / 1000;
if (list_empty(&ep->rdllist))
{
// 初始化 wait 等待队列的入口
init_waitqueue_entry(&wait, current);
wait.flags |= WQ_FLAG_EXCLUSIVE;
__add_wait_queue(&ep->wq, &wait);
// 等带回调函数, 进程设置为是可中断的
for (;;)
{
// 设置进程此时的状态是可中断的
set_current_state(TASK_INTERRUPTIBLE);
// 链表不为空, 或者时间到了
if (!list_empty(&ep->rdllist) || !jtimeout)
break;
...
// 在时间节拍到达之前, 让出 cpu 调用其他进程, 时间节拍到达时, 重新通过中断回调该进程
jtimeout = schedule_timeout(jtimeout);
/*
// 可用于在阻塞态的进程让出cpu, 一定时间后再回调查看
extern inline void schedule_timeout(int timeout)
{
current->timeout = jiffies + timeout;
current->state = TASK_INTERRUPTIBLE;
schedule();
current->timeout = 0;
}
*/
spin_lock_irqsave(&ep->lock, flags);
}
__remove_wait_queue(&ep->wq, &wait);
// 将进程状态设置为运行态
set_current_state(TASK_RUNNING);
}
eavail = !list_empty(&ep->rdllist);
...
// 没有被中断, 有就绪队列, 并向用户空间发送成功, 并调用ep_send_event()返回给用户空间就绪的epoll事件
if (!res && eavail &&
!(res = ep_send_events(ep, events, maxevents)) && jtimeout)
goto retry;
return res;
}
在ep_poll
等待消息到来, 当消息来到后执行ep_send_events
函数, 实现将链表从内核态复制到用户态.
ep_send_events
- 将就绪队列的链表复制到另一个新的链表中, 并且清空就绪队列.
- 将新保存的链表调用
__put_uer
, 将就绪队列从内核空间复制到内核空间. - 判断是否是下降沿有效
- 如果在将队列返回给用户空间的时侯又有消息到来的时侯, 需要重新将就绪消息放入
ovflist
中, 同时增加唤醒的次数.
// 向用户空间发送就绪事件队列
static int ep_send_events(struct eventpoll *ep, struct epoll_event __user *events,
int maxevents)
{
int eventcnt, error = -EFAULT, pwake = 0;
unsigned int revents;
unsigned long flags;
struct epitem *epi, *nepi;
struct list_head txlist;
INIT_LIST_HEAD(&txlist);
...
// 将 rdllist 就绪队列的链表放入到 txlist 链表中, 此时 rdllist 就为空链表了
list_splice(&ep->rdllist, &txlist);
INIT_LIST_HEAD(&ep->rdllist);
// 清空ovflist, 原因是后面执行将链表复制到用户空间的时侯还有消息到来, 就保存在ovflist中
ep->ovflist = NULL;
spin_unlock_irqrestore(&ep->lock, flags);
// 将就绪的队列放入传回到用户空间
for (eventcnt = 0; !list_empty(&txlist) && eventcnt < maxevents;)
{
// 取出第一个消息数据到来的结构, 然后移除这个数据结构
epi = list_first_entry(&txlist, struct epitem, rdllink);
list_del_init(&epi->rdllink);
// 确保有需要文件的存在, 并且也有操作的掩码存在
revents = epi->ffd.file->f_op->poll(epi->ffd.file, NULL);
revents &= epi->event.events;
// 就绪队列发送至用户空间
if (revents) {
if (__put_user(revents, &events[eventcnt].events) ||__put_user(epi->event.data, &events[eventcnt].data))
goto errxit;
if (epi->event.events & EPOLLONESHOT)
epi->event.events &= EP_PRIVATE_BITS;
// 唤醒消息的个数
eventcnt++;
}
// 是否是下降沿有效
if (!(epi->event.events & EPOLLET) && (revents & epi->event.events))
list_add_tail(&epi->rdllink, &ep->rdllist);
}
error = 0;
errxit:
spin_lock_irqsave(&ep->lock, flags);
// 在发送给用户空间的时侯又有数据传来时, 将事件放入到 ovflist 队列中. 在将数据加入到就绪队列事件中
for (nepi = ep->ovflist; (epi = nepi) != NULL; nepi = epi->next, epi->next = EP_UNACTIVE_PTR)
{
if (!ep_is_linked(&epi->rdllink) && (epi->event.events & ~EP_PRIVATE_BITS))
list_add_tail(&epi->rdllink, &ep->rdllist);
}
ep->ovflist = EP_UNACTIVE_PTR;
...
if (waitqueue_active(&ep->wq))
wake_up_locked(&ep->wq);
// 如果 poll 队列不为空, 则唤醒的次数++
if (waitqueue_active(&ep->poll_wait))
pwake++;
}
...
// 返回累计的值
return eventcnt == 0 ? error: eventcnt;
}
这就是关于epoll_wait函数的具体实现, 自己描述的可能不是特别的清楚, 但是整体的思路还是写的比较的清楚了. 最后, 再来小小的总结一下epoll_wait
的思路.
总结
调用SYSCALL_DEFINE(epoll_wait, …)函数, 获得匿名文件的文件指针, 在通过调用ep_poll
函数, 进行时间片的设置, 在时间片结束后就绪队列为空, 就退出等待; 如果时间设置的是负数, ep_poll
函数会调用schedule_timeout, 执行进程调度, 当设置的时间片结束后又继续回到ep_poll进程
查看就绪队列是否为空, 为空的话就继续cinching调度, 此时wait又变成阻塞态; 当就绪队列准备好后, 就退出进程调度, 执行ep_send_events
函数, 主要是为了将就绪队列的链表从内核空间发送给用户空间.
ep_send_events
函数, 先将就绪链表rdllist
复制到另一个新的链表, 重新将就绪链表清零, 然后程序调用__put_user
将新链表的数据发送给用户空间, 同时, 发送的个数吉拉路下来, 以便函数的返回 , 但是, 如果在发送的时候又有就绪信号到来, 就将来的就绪信号保存在ovflist
链表中, 最后又重新数据拷贝给rdllist
中, 再重复执行ep_send_events
函数.