epoll可以说是编写高性能服务端程序必不可少的技术。在介绍epoll之前,我们先了解一下多路复用I/O。MultiplexedI/OMultiplexedI/O:表示内核负责监视多个I/O流,当任何一个I/O流就绪(可读或可写)时都会通知进程,让这个I/O上的数据O流可以被处理。如图1所示:如图1所示,内核负责监控多个I/O流。当某些I/O流变为就绪时,内核会将这些I/O流加入到就绪队列中,然后通知Processes处理就绪队列中的I/O流。与传统的阻塞I/O相比,多路复用I/O的优势在于可以同时监听多个I/O流,并通知进程准备好I/O流。介绍完epoll多路复用I/O的原理,再介绍一下我们的主角:epoll。在Linux系统中,多路复用I/O的实现有很多,比如select和poll。而epoll也是多路复用I/O的一种实现。与select和poll相比,epoll有更大的性能提升。红黑树epoll内部用一颗红黑树来保存所有监听的socket。红黑树是平衡二叉树。添加和查找元素的时间复杂度为O(logn),其结构如图2所示:epoll以socket句柄为key,将socket存储在红黑树中。如图2所示,每个节点中的数字代表套接字句柄。将监听套接字保存在红黑树中的目的是在修改监听套接字的读写事件时,通过套接字句柄快速找到对应的套接字对象。就绪队列另外epoll还维护了一个就绪队列。当epoll监听的socket状态发生变化(变为可读或可写)时,就绪的socket会被加入到就绪队列中。如图3:当socket从网络获取数据时,会通知epoll,epoll会将当前socket加入就绪队列,并唤醒等待进程(即调用epoll_wait的进程)。当socket状态改变时,会调用ep_poll_callback函数通知epoll。我们来看看这个函数的处理过程:staticintep_poll_callback(wait_queue_t*wait,unsignedmode,intsync,void*key){...structepitem*epi=ep_item_from_wait(wait);structeventpoll*ep=epi->ep;...//1)将socket添加到就绪队列list_add_tail(&epi->rdllink,&ep->rdllist);is_linked://2)唤醒调用epoll_wait()和阻塞进程if(waitqueue_active(&ep->wq))wake_up_locked(&ep->wq);...return1;}ep_poll_callback函数的意图很明确,主要完成两个任务:将就绪的socket加入就绪队列。通过调用epoll_wait函数唤醒阻塞的进程。当进程醒来时,它会将就绪队列中的就绪套接字复制到用户提供的数组中。如图4所示:如图4所示,在调用epoll_wait时需要提供一个events数组来存放就绪的sockets。当epoll_wait返回时,用户可以从events数组中获取就绪的socket,并可以对其进行读写。小结本文主要通过图的方式介绍epoll的原理,但是很多实现细节只有阅读源码才能理解。如果你对epoll的实现感兴趣,可以参考这篇文章《epoll 如何工作的》。
