struct ep_pqueue
{
poll_table pt;
struct epitem *epi;
};
copy_from_user
将数据从用户空间拷贝到内核空间fget()
获得获得epoll_create()创建的匿名文件的文件指针.op
方法的判断.SYSCALL_DEFINE4(epoll_create, int, epfd, int, op, int, fd, struct epoll_event __user*, event)
{
struct epoll_event epds;
...
// 从用户态拷贝到内核态
if(ep_op_has_event(op) && copy_from_user(&epds, event, sizeof(struct epoll_event)))
goto error_return;
// 获取调用 epoll_create()函数返回的文件描述符后, 获得创建匿名文件的文件指针.
file = fget(epfd);
if (!file)
goto error_return;
tfile = fget(fd);
if (!tfile)
goto error_fput;
...
epi = ep_find(ep, tfile, fd); // 查找的原因在于ep是否已经存在了
/*
ep_find : 二叉查找文件
1. 通过将ep_set_ffd()将文件描述符和文件指针加入到一个结构体中
2. 调用ep_cmp_ffd()进行节点与要查找的文件进行比较, 二叉搜索
*/
error = -EINVAL;
// 调用 epoll_ctl()函数的方法
switch (op)
{
case EPOLL_CTL_ADD: // 添加
if (!epi)
{
epds.events |= POLLERR | POLLHUP;
// 调用ep_insert()函数, 设置好回调函数
error = ep_insert(ep, &epds, tfile, fd);
}
else
error = -EEXIST;
break;
case EPOLL_CTL_DEL: // 删除
if (epi)
error = ep_remove(ep, epi);
else
error = -ENOENT;
break;
case EPOLL_CTL_MOD: //修改
if (epi)
{
epds.events |= POLLERR | POLLHUP;
error = ep_modify(ep, epi, &epds);
}
else
error = -ENOENT;
break;
}
...
}
这里说道了ep_insert
设置回调函数. 那么现在也就讲解回调函数的实现.
ep_insert
kmem_cache_alloc
申请缓存空间ep_set_ffd
将文件描述符和文件指针加入ffd
结构体中ep_ptable_queue_proc
ep_rbtree_insert()
将ep, 加入到ep1中struct epitem epi
插入到红黑树中对目标文件的监听是由一个epitem结构的监听项变量维护的,所以在ep_insert函数里面,首先调用kmem_cache_alloc函数,从slab分配器里面分配一个epitem结构监听项,然后对该结构进行初始化.
static int ep_insert(struct eventpoll *ep, struct epoll_event *event, struct file *file, int fd)
{
...
struct epitem *epi;
struct ep_pqueue epq;
// 从slab里面分配缓存空间
if(!(epi = kmem_cache_alloc(epi_cache, GFP_KERNEL)))
return -ENOMEM;
// 初始化
INIT_LIST_HEAD(&epi->rdllink);
INIT_LIST_HEAD(&epi->fllink);
INIT_LIST_HEAD(&epi->pwqlist);
...
// 保存 epi 以便回调时使用
epq.epi = epi;
// 设置好 poll 回调函数为ep_ptable_queue_proc
init_poll_funcptr(&epq.pt, ep_ptable_queue_proc);
// 调用事件 poll 函数来获取当前事件位, 利用它来调用注册函数 ep_ptable_queue_proc
revents = tfile->f_op->poll(tfile, &epq.pt);
...
// 将其加入到红黑树中
ep_rbtree_insert(ep, epi);
/*
ep_rbtree_insert : 插入二叉树中
1. 通过ep_cmp_ffd进行二叉搜索
2. 调用rb_link_node rb_insert_color 将 ep结构加入到epi中
*/
// 返回的事件位(revent)与最初设置的事件位(events)相与进行判断, 判断是否有时间到来, 同时还要保证返回给epoll_wait()的就绪队列不为空
if ((revents & event->events) && !ep_is_linked(&epi->rdllink))
{
list_add_tail(&epi->rdllink, &ep->rdllist);
// 检查等待队列是否为空
if (waitqueue_active(&ep->wq))
wake_up_locked(&ep->wq);
// 等待队列不为空, 则增加唤醒次数
if (waitqueue_active(&ep->poll_wait))
pwake++;
}
...
}
同样ep_insert
函数又去调用init_poll_funcptr
将函数 init_ptable_queue_proc()
来设置为回调函数. 那么init_poll_funcptr
和 init_ptable_queue_proc
具体我们也来看一下吧
// 将qproc注册到 poll_table_struct 中
// 因为执行 f_op->poll() 时. XXX_poll() 函数会执行 poll_wait() 回调函数, 而 poll_wait()又会调用 poll_table_struct 中的 qproc
static inline void init_poll_funcptr(poll_table *pt, poll_queue_proc qproc)
{
pt->qproc = qproc;
}
ep_ptable_queue_proc函数
首先将eppoll_entry
的whead
指向fd的设备等待队列, 再初始化eppoll_entry
的base
变量指向epitem,最后通过add_wait_queue
将epoll_entry挂载到fd的设备等待队列上。完成这个动作后,epoll_entry已经被挂载到fd的设备等待队列.
// 当 poll 函数唤醒时就调用该函数
static void ep_ptable_queue_proc(struct file *file, wait_queue_head_t *whead, poll_table *pt)
{
// 从注册的结构中struct ep_pqueue中获取项epi
struct epitem *epi = ep_item_from_epqueue(pt);
// eppoll_entry主要完成epitem和epitem事件发生时的callback(ep_poll_callback)函数之间的关联
struct eppoll_entry *pwq;
// 申请eppoll_entry 缓存, 加入到等待队列中, 和链表中
if (epi->nwait >= 0 && (pwq = kmem_cache_alloc(pwq_cache,GFP_KERNEL)))
{
// 初始化等待队列函数的入口. 也就是 poll 醒来时要调用的回调函数
init_waitqueue_func_entry(&pwq->wait, ep_poll_callback);
pwq->whead = whead;
pwq->base = epi;
// 加入到等待队列中
add_wait_queue(whead, &pwq->wait);
// 将等待队列 llink 的链表挂载到 eptiem等待链表中
list_add_tail(&pwq->llink, &epi->pwqlist);
epi->nwait++;
}
...
}
最后一次的调用, ep_poll_callback
主要就只是将要回调事件的文件描述符(fd)加入到 epoll的监听队列中.
ep_poll_callback :
goto out_unlink
goto is_list
list_add_tail
加入链表is_list段 :
调用waitqueue_active
判断就绪队列是否为空, 不为空计数值增加,out_unlock段 :
解除所有的锁ep_poll_callback函数主要的功能是将被监视文件的等待事件就绪时,将文件对应的epitem实例添加到就绪队列中,当用户调用epoll_wait()时,内核会将就绪队列中的事件报告给用户.
// poll 到来时, 调用的回调函数. 判断poll 事件是否到来, 是否加入到就绪队列中了
static int ep_poll_callback(wait_queue_t *wait, unsigned mode, int sync, void *key)
{
struct epitem *epi = ep_item_from_wait(wait);
...
// 事件epi在是否在准备列表中
if (ep_is_linked(&epi->rdllink))
goto is_linked;
// 重要 : 将 fd 加入到 epoll 监听的就绪队列中
list_add_tail(&epi->rdllink, &ep->rdllist);
...
}
关于回调epoll 的回调函数设置的源码已经说的比较清楚了, 一层调用一层函数, 只需要在触发的时侯将其调用就行. 而epoll_ctl
函数根据监听的事件,为目标文件申请一个监听项,并将该监听项挂到eventpoll
结构的红黑树里面。
从SYSCALL_DEFINE4(epoll_ctl, …)开始, 函数首先就分配空间, 将结构从用户空间复制到内核空间中, 在进行方法(op)判断之前, 先采用ep_find
函数进行查找, 以确保该数据已经设置好回调函数了, 然后使用fget
函数获取该epoll的匿名文件的文件描述符, 最后进行方法(op)判断, 确定是EPOLL_CTL_ADD, EPOLL_CTL_MOD还是 EPOLL_CTL_DEL.
这里主要讲的是EPOLL_CTL_ADD
, 所以当是选择加入时, 就调用ep_insert
函数, 将回调函数设置为ep_ptable_queue_proc
函数, 也就是将消息到达后, 需要自动启动ep_ptable_proc函数, 进而调用ep_poll_callback
函数, 该函数就是把来的消息所对应的结构和文件信息加入到就绪链表中, 以便之后调用 epoll_wait 可以直接从就绪队列链表中夺得就绪的文件. 也正是这样, epoll的回调函数使epoll不用每次都轮询遍历数据, 而是自动唤醒回调, 更加的高效. 并且回调函数也只是在进程加入的时侯才设置, 而且只设置一次.