zoukankan      html  css  js  c++  java
  • [apue] epoll 的一些不为人所注意的特性

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

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

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

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

      而 epoll 不行,同时只能有一个线程执行 epoll_wait 操作,因此这里需要做一点处理,

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

      当有事件发生了,leader 线程在执行事件处理器之前 unlock  这个 mutex,

      就可以允许等待在这个 mutex 上的其它线程中的一个进入 epoll_wait 从而担任新的 leader。

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

    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 在异步事件完成后,会通过完成端口完成通知,但在某些情况下,异步操作可以“立即完成”,

    就是说虽然只是提交异步事件,但是也有可能这个操作直接完成了。这种情况下,可以直接处理得到的数据,相当于是同步调用。

    但是我要说的是,千万不要直接处理数据,因为当你处理完之后,完成端口依旧会在之后进行通知,导致同一个数据被处理多次的情况。

    所以最好的实践就是,不论是否立即完成,都交给完成端口去处理,保证数据的一次性。

  • 相关阅读:
    Codeforces Round #609 (Div. 2)
    Educational Codeforces Round 78 (Rated for Div. 2)
    Codeforces
    crontab
    C6 C7的开机启动流程
    平均负载压力测试
    ps 和 top
    if判断
    使用3种协议搭建本地yum仓库
    linux rpm包
  • 原文地址:https://www.cnblogs.com/goodcitizen/p/13004694.html
Copyright © 2011-2022 走看看