之前曾经使用 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 上没有这种情况,有清楚这个现象原因的同学,不吝赐教啊

[apue] epoll 的一些不为人所注意的特性的更多相关文章

  1. c++ 跨平台线程同步对象那些事儿——基于 ace

    前言 ACE (Adaptive Communication Environment) 是早年间很火的一个 c++ 开源通讯框架,当时 c++ 的库比较少,以至于谈 c++ 网络通讯就绕不开 ACE, ...

  2. ASP.NET 整理比较全的URL重写解决方案

    经常有人请我指导应该如何动态地“重写”URL,以在他们的ASP.NETweb应用中发布比较干净的URL端点.这个博客帖子概述了几个方法,你可以用来在ASP.NET中干净地映射或重写URL,以及按照你自 ...

  3. (十一) 一起学 Unix 环境高级编程 (APUE) 之 高级 IO

    . . . . . 目录 (一) 一起学 Unix 环境高级编程 (APUE) 之 标准IO (二) 一起学 Unix 环境高级编程 (APUE) 之 文件 IO (三) 一起学 Unix 环境高级编 ...

  4. epoll和select区别

    先说下本文框架,先是问题引出,然后概括两个机制的区别和联系,最后介绍每个接口的用法 一.问题引出 联系区别 问题的引出,当需要读两个以上的I/O的时候,如果使用阻塞式的I/O,那么可能长时间的阻塞在一 ...

  5. Select与Epoll比较

    一.问题引出 联系区别 问题的引出,当需要读两个以上的I/O的时候,如果使用阻塞式的I/O,那么可能长时间的阻塞在一个描述符上面,另外的描述符虽然有数据但是不能读出来,这样实时性不能满足要求,大概的解 ...

  6. Socket网络编程--epoll小结

    以前使用的用于I/O多路复用为了方便就使用select函数,但select这个函数是有缺陷的.因为它所支持的并发连接数是有限的(一般小于1024),因为用户处理的数组是使用硬编码的.这个最大值为FD_ ...

  7. I/O多路转接-epoll

    By francis_hao    Aug 5,2017   APUE讲多路转接的章节介绍了select.pselect和poll函数.而epoll是linux内核在2.5.44引入的.在glibc ...

  8. [APUE]进程控制(上)

    一.进程标识 进程ID 0是调度进程,常常被称为交换进程(swapper).该进程并不执行任何磁盘上的程序--它是内核的一部分,因此也被称为系统进程.进程ID 1是init进程,在自举(bootstr ...

  9. [APUE]UNIX进程的环境(下)

    一.共享库 共享库使得可执行文件中不再需要包含常用的库函数,而只需在所有进程都可存取的存储区中保存这种库例程的一个副本.程序第一次执行的时候或第一次调用某个库函数的时候,用动态链接方法将程序与共享库函 ...

随机推荐

  1. 我的linux学习日记day5

    一.vim 编辑器 有三种模式,命令模式,输入模式,末行模式 1.下面是命令模式常用的命令 2.末行模式常用命令 :w 保存 :q 退出 :q! 强制退出 :wq! 强制保存退出 :set nu 显示 ...

  2. 黑马程序员_毕向东_Java基础视频教程——switch语句练习(随笔)

    switch(练习) /* if和 switch 语句很像. 具体什么场景下使用什么语句呢? 如果判断的具体数值不多且符合byte.short.int.char.String类型,虽然两个语句都可以使 ...

  3. Vi 和 Vim 的使用

    Vi (Visual Interface)是 Linux下基于Shell 的文本编辑器,Vim (Visual Interface iMproved)是 Vi的增强版本,扩展了很多功能,比如对程序源文 ...

  4. Bash Shell之内建命令和保留字

    转载自:http://blog.chinaunix.net/uid-25880122-id-2941630.html 命令 含义 ! 保留字,逻辑非 : 不做任何事,只做参数展开 . 读取文件并在sh ...

  5. 「雕爷学编程」Arduino动手做(25)——MQ2气敏检测模块

    37款传感器与模块的提法,在网络上广泛流传,其实Arduino能够兼容的传感器模块肯定是不止37种的.鉴于本人手头积累了一些传感器和模块,依照实践出真知(一定要动手做)的理念,以学习和交流为目的,这里 ...

  6. Djano之ORM多表查询操作

    # 把 model 转化为 迭代器去循环 MODEL.objects.all().iterator() # 等同于 values, values_list, 但是 only 这种方式 获取字段属性依旧 ...

  7. VIOS挂载ISO文件

    6.VIOS挂载ISO文件 1.给vhost建立虚拟设备 mkvdev -vadapter vhostX -fbo -dev cdx 2.建立存放ISO的资料库 mkrep -sp rootvg -s ...

  8. Python之日志处理(logging模块二实战)

    实战篇 import logging import logging.handlers LOG_PATH = r'./' def logConfig_1(): ''' 配置 log 输出到文件 : fi ...

  9. Robot Framework(2)- 快速安装

    如果你还想从头学起Robot Framework,可以看看这个系列的文章哦! https://www.cnblogs.com/poloyy/category/1770899.html 安装RF cmd ...

  10. SpringCloud Alibaba 简介

    SpringCloud Aliababa简介 SpringCloud Alibaba是阿里巴巴集团开源的一套微服务架构解决方案. 微服务架构是为了更好的分布式系统开发,将一个应用拆分成多个子应用,每一 ...