[apue] epoll 的一些不为人所注意的特性

以前曾经使用 epoll 构建过一个轻量级的 tcp 服务框架:html

一个工业级、跨平台、轻量级的 tcp 网络服务框架:gevent linux

 

在调试的过程当中,发现一些 epoll 以前没怎么注意到的特性。缓存

a)  iocp 是彻底线程安全的,即同时能够有多个线程等待在 iocp 的完成队列上;安全

  而 epoll 不行,同时只能有一个线程执行 epoll_wait 操做,所以这里须要作一点处理,bash

  网上有人使用 condition_variable + mutex 实现 leader-follower 线程模型,但我只用了一个 mutex 就实现了,网络

  当有事件发生了,leader 线程在执行事件处理器以前 unlock  这个 mutex,数据结构

  就能够容许等待在这个 mutex 上的其它线程中的一个进入 epoll_wait 从而担任新的 leader。多线程

  (不知道多加一个 cv 有什么用,有明白原理的提示一下哈)app

 

b)  epoll 在加入、删除句柄时是能够跨线程的,并且这一操做是线程安全的。框架

  以前一直觉得 epoll 会像 select 一像,添加或删除一个句柄须要先通知 leader 从 epoll_wait 中醒来,

  在从新 wait 以前经过  epoll_ctl 添加或删除对应的句柄。可是如今看彻底能够在另外一个线程中执行 epoll_ctl 操做

  而不用担忧多线程问题。这个在 man 手册页也有描述(man epoll_wait):

NOTES
       While one thread is blocked in a call to epoll_pwait(), it is possible for  another  thread  to
       add  a  file  descriptor to the waited-upon epoll instance.  If the new file descriptor becomes
       ready, it will cause the epoll_wait() call to unblock.

       For a discussion of what may happen if a file descriptor in an epoll instance  being  monitored
       by epoll_wait() is closed in another thread, see select(2).

 

 c)  epoll 有两种事件触发方式,一种是默认的水平触发(LT)模式,即只要有可读的数据,就一直触发读事件;

  还有一种是边缘触发(ET)模式,即只在没有数据到有数据之间触发一次,若是一次没有读彻底部数据,

  则也不会再次触发,除非全部数据被读完,且又有新的数据到来,才触发。使用 ET 模式的好处是,

  不用在每次执行处理器前将句柄从 epoll 移除、在执行完以后再加入 epoll 中,

  (若是不这样作的话,下一个进来的 leader 线程还会认为这个句柄可读,从而致使一个链接的数据被多个线程同时处理)

  从而致使频繁的移除、添加句柄。好多网上的 epoll 例子也推荐这种方式。可是我在亲自验证后,发现使用 ET 模式有两个问题:

 

  1)若是链接上来了大量数据,而每次只能读取部分(缓存区限制),则第 N 次读取的数据与第 N+1 次读取的数据,

    有多是两个线程中执行的,在读取时它们的顺序是能够保证的,可是当它们通知给用户时,第 N+1 次读取的数据

    有可能在第 N 次读取的数据以前送达给应用层。这是由于线程的调度致使的,虽然第 N+1 次数据只有在第 N 次数据

    读取完以后才可能产生,可是当第 N+1 次数据所在的线程可能先于第 N 次数据所在的线程被调度,上述场景就会产生。

    这须要细心的设计读数据到给用户之间的流程,防止线程抢占(须要加一些保证顺序的锁);

  2)当大量数据发送结束时,链接中断的通知(on_error)可能早于某些数据(on_read)到达,其实这个原理与上面相似,

    就是客户端在全部数据发送完成后主动断开链接,而获取链接中断的线程可能先于末尾几个数据所在的线程被调度,

    从而在应用层形成混乱(on_error 通常会删除事件处理器,可是 on_read 又须要它去作回调,好的状况会形成一些

    数据丢失,很差的状况下直接崩溃)

 

  鉴于以上两点,最后我仍是使用了默认的 LT 触发模式,幸亏有 b) 特性,我仅仅是增长了一些移除、添加的代码,

  并且我不用在应用层加锁来保证数据的顺序性了。

 

d)  必定要捕捉 SIGPIPE 事件,由于当某些链接已经被客户端断开时,而服务端还在该链接上 send 应答包时:

  第一次 send 会返回 ECONNRESET(104),再 send 会直接致使进程退出。若是捕捉该信号后,则第二次 send 会返回 EPIPE(32)。

  这样能够避免一些莫名其妙的退出问题(我也是经过 gdb 挂上进程才发现是这个信号致使的)。

 

e)  当管理多个链接时,一般使用一种 map 结构来管理 socket 与其对应的数据结构(特别是回调对象:handler)。

  可是不要使用 socket 句柄做为这个映射的 key,由于当一个链接中断而又有一个新的链接到来时,linux 上倾向于用最小的

  fd 值为新的 socket 分配句柄,大部分状况下,它就是你刚刚 close 或客户端中断的句柄。这样一来很容易致使一些混乱的状况。

  例如新的句柄插入失败(由于旧的虽然已经关闭可是还将来得及从 map  中移除)、旧句柄的清理工做无心间关闭了刚刚分配的

  新链接(清理时 close 一样的 fd 致使新分配的链接中断)……而在 win32 上不存在这样的状况,这并非由于 winsock 比 bsdsock 作的更好,

  相同的, winsock 也存在新分配的句柄与以前刚关闭的句柄同样的场景(当大量客户端不停中断重连时);而是由于 iocp 基于提早

  分配的内存块做为某个 IO 事件或链接的依据,而 map 的 key 大多也依据这些内存地址构建,因此通常不存在重复的状况(只要还在 map 中就不释放对应内存)。

 

  通过观察,我发如今 linux 上,即便新的链接占据了旧的句柄值,它的端口每每也是不一样的,因此这里使用了一个三元组做为 map 的 key:

  { fd, local_port, remote_port }

  当 fd 相同时,local_port 与 remote_port 中至少有一个是不一样的,从而能够区分新旧链接。

 

f)  若是链接中断或被对端主动关闭链接时,本端的 epoll 是能够检测到链接断开的,可是若是是本身 close 掉了 socket 句柄,则 epoll 检测不到链接已断开。

  这个会致使客户端在不停断开重连过程当中积累大量的未释放对象,时间长了有可能致使资源不足从而崩溃。

  目前尚未找到产生这种现象的缘由,Windows 上没有这种状况,有清楚这个现象缘由的同窗,不吝赐教啊

 

最后,再乱入一波 iocp 的特性:

iocp 在异步事件完成后,会经过完成端口完成通知,但在某些状况下,异步操做能够“当即完成”,

就是说虽然只是提交异步事件,可是也有可能这个操做直接完成了。这种状况下,能够直接处理获得的数据,至关因而同步调用。

可是我要说的是,千万不要直接处理数据,由于当你处理完以后,完成端口依旧会在以后进行通知,致使同一个数据被处理屡次的状况。

因此最好的实践就是,不管是否当即完成,都交给完成端口去处理,保证数据的一次性。

相关文章
相关标签/搜索