背景:以往存在的模型存在的问题
什么是epoll
epoll和select都采用IO多路复用(IO multiplexing)技术
epoll是为处理大批量句柄而进行改进改进的poll
epoll模型如何解决其他模型存在的问题
支持一个进程打开大量文件描述符
传统方案有两种方式可以解决这个问题:
修改决定可开启fd数量的FD_SETSIZE后重新编译内核,
缺点:fd数量的提升会带来网络性能的下降。
使用多进程的方案,缺点:进程的创建存在开销,同时,进程之间数据同步的代价较高。
epoll如何实现
IO效率不会因为文件描述符的增加而线性下降:
使用mmap加速内核与用户空间的信息传递
epoll中重要的数据结构
eventpoll
[/fs/eventpoll.c]
//epoll的核心实现对应于一个epoll描述符
struct eventpoll {
//保证文件被epoll使用时不被移动
struct mutex mtx;
/* Wait queue used by sys_epoll_wait() */
wait_queue_head_t wq;
/* Wait queue used by file->poll() */
wait_queue_head_t poll_wait;
/* List of ready file descriptors */
struct list_head rdllist;
/* RB tree root used to store monitored fd structs */
struct rb_root_cached rbr;
//链接struct epitem的链表
struct epitem *ovflist;
/* wakeup_source used when ep_scan_ready_list is running */
struct wakeup_source *ws;
/* The user that created the eventpoll descriptor */
struct user_struct *user;
struct file *file;
/* used to optimize loop detection check */
int visited;
struct list_head visited_list_link;
#ifdef CONFIG_NET_RX_BUSY_POLL
/* used to track busy poll napi_id */
unsigned int napi_id;
#endif
};
epitem
[/fs/eventpoll.c]
// 对应于一个加入到epoll的文件
struct epitem {
union {
// 挂载到eventpoll 的红黑树节点
struct rb_node rbn;
//用于释放epitem
struct rcu_head rcu;
};
// 挂载到eventpoll.rdllist 的节点
struct list_head rdllink;
// 连接到ovflist 的指针
struct epitem *next;
//该item涉及到的文件描述符信息
struct epoll_filefd ffd;
//与poll operation有关的active的waitqueue的数量
int nwait;
//包含poll wait queues的列表
struct list_head pwqlist;
// 当前epitem 的所有者
struct eventpoll *ep;
//用于将该item连接到struct file 列表的列表头
struct list_head fllink;
//当EPOLLWAKEUP被设置时 wakeup_source将会被使用
struct wakeup_source __rcu *ws;
//描述监视的event和source 的fd
struct epoll_event event;
};
epoll如何使用
epoll API包括:epoll_create, epoll_ctl和epoll_wait三个。
epoll_create
[/fs/eventpoll.c]
//判断size是否大于0,如果大于0就调用epoll_create1,否则就调用epoll_create
SYSCALL_DEFINE1(epoll_create1, int, flags)
{
return do_epoll_create(flags);
}
SYSCALL_DEFINE1(epoll_create, int, size)
{
if (size <= 0)
return -EINVAL;
return do_epoll_create(0);
}
P.S.:
SYSCALL_DEFINE1是一个宏,用来定义有一个参数的系统调用函数。
以下两个函数展开后为int epoll_create1(int flags)和int epoll_create(int size)。这两个就是我们常见的入口。
使用宏的原因在于:系统调用的参数个数,传参方式都有限制。
do_epoll_create函数
[/fs/eventpoll.c]
//创建一个epoll描述符
static int do_epoll_create(int flags)
{
int error, fd;
struct eventpoll *ep = NULL; //主描述符
struct file *file;
...
//分配一个struct eventpoll
error = ep_alloc(&ep);
//创建设置一个eventpoll文件所需要的各个items,如:file structure和free file descriptor
fd = get_unused_fd_flags(O_RDWR | (flags & O_CLOEXEC));
...
//创建匿名fd
file = anon_inode_getfile("[eventpoll]", &eventpoll_fops, ep, O_RDWR | (flags & O_CLOEXEC));
...
}
epoll_ctl
在得到epoll描述符后,可以调用epoll_ctl以注册要监听的事件类型。
[/fs/eventpoll.c]
//实现epoll描述符的控制接口,支持对在epoll文件描述符中监听的文件描述符的修改,如插入/删除/修改,由参数op决定,
//此外,fd是我们要监听的描述符,就是socket的,而event是我们感兴趣的事件
SYSCALL_DEFINE4(epoll_ctl, int, epfd, int, op, int, fd, struct epoll_event __user *, event)
{
int error;
int full_check = 0;
struct fd f, tf;
struct eventpoll *ep;
struct epitem *epi; //socket描述符 在 epoll 描述符中的映射
struct epoll_event epds;
struct eventpoll *tep = NULL;
...
//在红黑树中以tfile和fd为参数来查找文件对应的epitem
epi = ep_find(ep, tf.file, fd);
error = -EINVAL;
//对每种操作分别进行处理
switch (op) {
case EPOLL_CTL_ADD: //处理添加
if (!epi) {
epds.events |= EPOLLERR | EPOLLHUP;
//进行添加的函数
error = ep_insert(ep, &epds, tf.file, fd, full_check);
} else
error = -EEXIST;
if (full_check)
clear_tfile_check_list();
break;
case EPOLL_CTL_DEL: //处理删除
if (epi)
error = ep_remove(ep, epi);
else
error = -ENOENT;
break;
case EPOLL_CTL_MOD: //处理修改
if (epi) {
if (!(epi->event.events & EPOLLEXCLUSIVE)) {
epds.events |= EPOLLERR | EPOLLHUP;
error = ep_modify(ep, epi, &epds);
}
} else
error = -ENOENT;
break;
}
...
}
在epoll_ctl中处理EPOLL_CTL_ADD事件时,会调用ep_insert函数。
[/fs/eventpoll.c]
//向epollfd中添加一个监听fd
static int ep_insert(struct eventpoll *ep, const struct epoll_event *event,
struct file *tfile, int fd, int full_check)
{
struct epitem *epi;
...
//使用queue的callback函数初始化poll table
epq.epi = epi;
init_poll_funcptr(&epq.pt, ep_ptable_queue_proc);
//在文件对应的wait queue head上注册callback函数,并返回当前文件的状态
revents = ep_item_poll(epi, &epq.pt, 1);
//添加当前的epitem到文件的f_ep_links链表
spin_lock(&tfile->f_lock);
list_add_tail_rcu(&epi->fllink, &tfile->f_ep_links);
spin_unlock(&tfile->f_lock);
//将epi插入红黑树
ep_rbtree_insert(ep, epi);
...
//如果文件就绪就把它插入到就绪列表中
if (revents && !ep_is_linked(epi)) {
list_add_tail(&epi->rdllink, &ep->rdllist);
ep_pm_stay_awake(epi);
//提醒等待任务的事件已经就绪
if (waitqueue_active(&ep->wq))
//通知epoll_wait, 调用callback函数唤醒等待进程
wake_up_locked(&ep->wq);
if (waitqueue_active(&ep->poll_wait))
pwake++;
}
spin_unlock_irq(&ep->wq.lock);
atomic_long_inc(&ep->user->epoll_watches);
//在没有锁的时候再通知epoll进程
if (pwake)
ep_poll_safewake(&ep->poll_wait);
return 0;
...
}
等待队列中的文件描述符调用的callback函数
[/fs/eventpoll.c]
//这是等待队列唤醒进程时的callback函数。在等待队列中的文件描述符希望被唤醒时会调用该函数
static int ep_poll_callback(wait_queue_entry_t *wait, unsigned mode, int sync, void *key)
{
int pwake = 0;
unsigned long flags;
//获取epitem
struct epitem *epi = ep_item_from_wait(wait);
struct eventpoll *ep = epi->ep;
spin_lock_irqsave(&ep->wq.lock, flags);
//在callback函数调用时,如果epoll_wait函数返回了,此时进程可能在获取event
//此时,内核将发生event的epitem用单独的链表链接,在下次epoll_wait时交付
if (ep->ovflist != EP_UNACTIVE_PTR) {
if (epi->next == EP_UNACTIVE_PTR) {
epi->next = ep->ovflist;
ep->ovflist = epi;
if (epi->ws) {
__pm_stay_awake(ep->ws);
}
}
goto out_unlock;
}
...
//唤醒epoll wait list和the ->poll() wait list.
if (waitqueue_active(&ep->wq)) {
if ((epi->event.events & EPOLLEXCLUSIVE) &&
!(pollflags & POLLFREE)) {
switch (pollflags & EPOLLINOUT_BITS) {
case EPOLLIN:
if (epi->event.events & EPOLLIN)
ewake = 1;
break;
case EPOLLOUT:
if (epi->event.events & EPOLLOUT)
ewake = 1;
break;
case 0:
ewake = 1;
break;
}
}
wake_up_locked(&ep->wq);
}
//如果epollfd也在被poll,则唤醒队列中所有成员
if (waitqueue_active(&ep->poll_wait))
pwake++;
...
}
epoll_wait
[/fs/eventpoll.c]
SYSCALL_DEFINE4(epoll_wait, int, epfd, struct epoll_event __user *, events,
int, maxevents, int, timeout)
{
return do_epoll_wait(epfd, events, maxevents, timeout);
}
[/fs/eventpoll.c]
//为eventpoll描述符实现event wait接口。以下函数是用户空间的epoll_pwait(2)的kernel部分
SYSCALL_DEFINE6(epoll_pwait, int, epfd, struct epoll_event __user *, events,
int, maxevents, int, timeout, const sigset_t __user *, sigmask,
size_t, sigsetsize)//该函数主要是调用下面的do_epoll_wait函数
static int do_epoll_wait(int epfd, struct epoll_event __user *events,
int maxevents, int timeout)
{
int error;
struct fd f;
struct eventpoll *ep;
...
//得到epoll文件描述符
f = fdget(epfd);
if (!f.file)
return -EBADF;
//获取eventpoll结构
ep = f.file->private_data;
/* Time to fish for events ... */
//等待事件
error = ep_poll(ep, events, maxevents, timeout);
...
}
//该函数让执行epoll_wait的进程进入睡眠状态
static int ep_poll(struct eventpoll *ep, struct epoll_event __user *events,
int maxevents, long timeout)
{
int res = 0, eavail, timed_out = 0;
u64 slack = 0;
//等待队列
wait_queue_entry_t wait;
ktime_t expires, *to = NULL;
...
if (!ep_events_available(ep)) {
...
//目前没有足够的event可以返回给调用者,所以在此休眠。同时,将会在由event时,由ep_poll_callback()唤醒
init_waitqueue_entry(&wait, current);
__add_wait_queue_exclusive(&ep->wq, &wait);
for (;;) {
//在ep_poll_back函数发送唤醒时,我们不希望还处于被挂起状态。因此我们在进行check event钱设置进程状态为TASK_INTERRUPTIBLE。即设置为可唤醒状态。
set_current_state(TASK_INTERRUPTIBLE);
//允许进程在等待时间用完时退出。
if (fatal_signal_pending(current)) {
res = -EINTR;
break;
}
//如果此时就绪队列中已经有了成员,或者睡觉时间用尽,则退出,结束睡眠
if (ep_events_available(ep) || timed_out)
break;
//产生信号时,也退出睡眠
if (signal_pending(current)) {
res = -EINTR;
break;
}
//没有发生event,则解锁,进如睡眠
spin_unlock_irq(&ep->wq.lock);
if (!schedule_hrtimeout_range(to, slack, HRTIMER_MODE_ABS))
timed_out = 1;
//callback函数的调用时机是由被监听的fd具体实现的,例如在socket情况下,在等待队列头中决定,就是ep_insert中哪个。而epoll和当前进程做的就是等待。
spin_lock_irq(&ep->wq.lock);
}
__remove_wait_queue(&ep->wq, &wait);
//运行
__set_current_state(TASK_RUNNING);
}
check_events:
...
//尝试向用户空间发送事件
if (!res && eavail &&
!(res = ep_send_events(ep, events, maxevents)) && !timed_out)
goto fetch_events;
...
}
参考文献:
epoll分析过程
openEuler源代码