这样,我们就看下最终唤醒epoll_wait的ep_poll_callback函数:
static int ep_poll_callback(wait_queue_t *wait, unsigned mode, int sync, void *key) { // 获取wait对应的epitem struct epitem *epi = ep_item_from_wait(wait); // epitem对应的eventpoll结构体 struct eventpoll *ep = epi->ep; // 获取自旋锁,保护ready_list等结构 spin_lock_irqsave(&ep->lock, flags); // 如果当前epi没有被链入ep的ready list,则链入 // 这样,就把当前的可用事件加入到epoll的可用列表了 if (!ep_is_linked(&epi->rdllink)) list_add_tail(&epi->rdllink, &ep->rdllist); // 如果有epoll_wait在等待的话,则唤醒这个epoll_wait进程 // 对应的&ep->wq是在epoll_wait调用的时候通过init_waitqueue_entry(&wait, current)而生成的 // 其中的current即是对应调用epoll_wait的进程信息task_struct if (waitqueue_active(&ep->wq)) wake_up_locked(&ep->wq); }上述过程如下图所示:
最后wake_up_locked调用__wake_up_common,然后调用了在init_waitqueue_entry注册的default_wake_function,调用路径为: wake_up_locked |->__wake_up_common |->default_wake_function |->try_wake_up (wake up a thread) |->activate_task |->enqueue_task running
将epoll_wait进程推入可运行队列,等待内核重新调度进程,然后epoll_wait对应的这个进程重新运行后,就从schedule恢复,继续下面的ep_send_events(向用户空间拷贝事件并返回)。
wake_up过程如下图所示:
可写事件的运行过程和可读事件大同小异:
首先,在epoll_ctl_add的时候预先会调用一次对应文件描述符的poll,如果返回事件里有可写掩码的时候直接调用wake_up_locked以唤醒对应的epoll_wait进程。
然后,在tcp在底层驱动有数据到来的时候可能携带了ack从而可以释放部分已经被对端接收的数据,于是触发可写事件,这一部分的调用链为:
最后在此函数里面sk_stream_write_space唤醒对应的epoll_wait进程
void sk_stream_write_space(struct sock *sk) { // 即有1/3可写空间的时候才触发可写事件 if (sk_stream_wspace(sk) >= sk_stream_min_wspace(sk) && sock) { clear_bit(SOCK_NOSPACE, &sock->flags); if (sk->sk_sleep && waitqueue_active(sk->sk_sleep)) wake_up_interruptible_poll(sk->sk_sleep, POLLOUT | POLLWRNORM | POLLWRBAND) ...... } } 关闭描述符(close fd)值得注意的是,我们在close对应的文件描述符的时候,会自动调用eventpoll_release将对应的file从其关联的epoll_fd中删除,kernel关键路径如下:
close fd |->filp_close |->fput |->__fput |->eventpoll_release |->ep_remove所以我们在关闭对应的文件描述符后,并不需要通过epoll_ctl_del来删掉对应epoll中相应的描述符。
总结epoll作为linux下非常优秀的事件触发机制得到了广泛的运用。其源码还是比较复杂的,本文只是阐述了epoll读写事件的触发机制,探究linux kernel源码的过程非常快乐_。
公众号关注笔者公众号,获取更多干货文章: