逐步实现TCP服务端Step04-8:epoll

epoll是对selectpoll的强化,是Linux(2.6内核开始支持)特有的机制。它有这么几个特点:1. 内核维护了一个事件列表用于记录各描述符上注册的事件,某个文件描述符上的事件只需向内核注册一次,不必像selectpoll那样每次调用都要向内核注册一遍;2. 监测结果集中只会给出就绪了的文件描述符;3. 支持ET工作模式。

这是在目前的程序(netio)结构下,使用selectpollepoll在处理结果集时的区别:

右边图中,之所以能直接取到文件描述符对应的那个SocketInfo结构,还有赖于epoll提供的epoll_event结构。它是在注册事件时,向内核告知事件类型的结构,里面提供一个data字段,可以附带用户自己定义的数据。这给使用者带来了很大的“自由度”。我们把每一个文件描述符对应的SocketInfo结构的地址交由data.ptr来存放。这样,通过结果集就可直接访问到对应的SocketInfo结构了。

epoll在内核中维护的这个事件列表,其本身对应了一个文件描述符,由epoll_create调用返回。除非显式地将其close掉,否则它会一直存在。

epoll有关的系统调用有epoll_create、epoll_ctl和epoll_wait三个。

  • size:告诉内核需要关注的文件描述符的个数。内核会根据这个数值去准备内核事件列表,调用成功的话,会返回对应于内核事件列表的文件描述符。这个描述符很重要,在其它调用中都会用到。

基于该调用可对内核事件列表实施管控。

  • epfd:该参数就是epoll_create返回的那个对应于内核事件列表的文件描述符。
  • op:即operation,指明到底要做什么。
    • EPOLL_CTL_ADD:向内核事件列表中注册文件描述符fd上的事件
    • EPOLL_CTL_DEL:卸载那些已注册过的,对应于文件描述符fd上的事件
    • EPOLL_CTL_MOD:修改已注册了的,对应于文件描述符fd上的事件
      我们这里用到的是EPOLL_CTL_ADD和EPOLL_CTL_DEL操作。
  • fd:希望被检测的那个文件描述符
  • event:该参数用于指定要注册的具体事件,开头已经说过,这个epoll_event结构中的data字段可用于承载用户数据。我们用ptr指针记录了fd对应的那个SocketInfo结构的指针。事件的类型与poll类似,事件名为poll事件名加”E”前缀。不过,epoll有两个独有的事件类型,EPOLLET及EPOLLONESHOT,这个后面再讨论。

该调用类似于select,是实际发起查询请求的调用。

  • epfd:同上面的参数。
  • events:这个地方需要传入一块足够大的空间,用来存放查询结果。定义一个epoll_event类型的数组,长度为当时传给epoll_create调用的那个值即可。
  • maxevents:该参数指明了events数组的大小,这里设置与epoll_create的参数相同的值。
  • timeout:与poll中的timeout意义相同。
  • return:返回就绪的事件数,返回0表示超时。

epoll的高级模式

epoll默认工作在所谓的LT(Level-Trigger)模式,该模式下的epollselectpoll在对待那些未被立即处理,或被处理了,但未处理干净的事件的态度是一致的。即:仍将它们视为就绪状态,直到其被处理干净。

明确一下,“未被立即处理“指的是,通过调用某I/O复用接口,得到就绪结果集后,不对某个或某些结果做处理。“未处理干净“指的是,对某个就绪文件描述符处理时,未将此次就绪的那些数据都处理掉。例如,在进行recv操作时,若传入缓冲区的尺寸小于内核中就绪数据的大小,此时,只进行一次recv操作的话,就不能将事情处理干净。

LT模式提供了无限次的处理机会,如果你没将事情处理完的话。

ET(Edge-Trigger)是epoll特有的模式,启动该模式后,每个事件将只会有一次处理机会。若不立即处理且处理干净的话,以后就没机会了。

ET模式这种粗暴的做法,迫使使用者必须在事件到来时立即处理完毕,这无疑会减少LT模式下事情被处理干净之前的那些循环操作。

ET模式的开启是基于每个文件描述符的,在为某个文件描述符注册事件的时候,带上EPOLLET事件即可。

epoll的另外一个特殊事件是EPOLLONESHOT,从名字大概可以看出它的用处。若注册了该事件,程序在处理当前事件的时候,其它事件不会被触发,直到EPOLLONESHOT被重置(使用epoll_ctl进行EPOLL_CTL_MOD操作),这类似于一种锁定。

这两种高级操作,均未在netio的当前版本中使用,当需要更高性能或其它特殊需求时再做讨论。

启用epoll后,netio存在的问题

若在某次发送消息的时候,有部分数据未发完,这部分滞留数据的发送时机是何时?

在使用selectpoll的时候,因为必须要在外层遍历socket_info_list_,所以就有机会遍访每个SocketInfo,就顺便把滞留数据给发了。

而这次,我们若通过注册EPOLLOUT事件,来获得发送滞留数据的一个时机的话,将会导致结果集中的元素数量同文件描述符数几乎相同。因为,绝大多数情况下,内核发送缓冲区都是就绪的,即,每一个文件描述符都会不断地产生EPOLLOUT事件(LT模式下)。实际情况就是要遍历一个几乎跟socket_info_list_等长的结果集,这貌似跟selectpoll就没什么区别了。

其实,我们只关注那些滞留数据对应的文件描述符是否准备就绪,其它的无用过问。我们可以只对产生滞留数据的文件描述符注册EPOLLOUT事件,待其发送完毕,就将EPOLLOUT事件清除。

此次除增加了epoll相关的内容外,还为TCPSocket类增加了几个方法,主要是一些状态查询接口,例如查询残留数据的多少等。相关文件如下:


<==  index  ==>