[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 上没有这种情况,有清楚这个现象原因的同学,不吝赐教啊
[apue] epoll 的一些不为人所注意的特性的更多相关文章
- c++ 跨平台线程同步对象那些事儿——基于 ace
前言 ACE (Adaptive Communication Environment) 是早年间很火的一个 c++ 开源通讯框架,当时 c++ 的库比较少,以至于谈 c++ 网络通讯就绕不开 ACE, ...
- ASP.NET 整理比较全的URL重写解决方案
经常有人请我指导应该如何动态地“重写”URL,以在他们的ASP.NETweb应用中发布比较干净的URL端点.这个博客帖子概述了几个方法,你可以用来在ASP.NET中干净地映射或重写URL,以及按照你自 ...
- (十一) 一起学 Unix 环境高级编程 (APUE) 之 高级 IO
. . . . . 目录 (一) 一起学 Unix 环境高级编程 (APUE) 之 标准IO (二) 一起学 Unix 环境高级编程 (APUE) 之 文件 IO (三) 一起学 Unix 环境高级编 ...
- epoll和select区别
先说下本文框架,先是问题引出,然后概括两个机制的区别和联系,最后介绍每个接口的用法 一.问题引出 联系区别 问题的引出,当需要读两个以上的I/O的时候,如果使用阻塞式的I/O,那么可能长时间的阻塞在一 ...
- Select与Epoll比较
一.问题引出 联系区别 问题的引出,当需要读两个以上的I/O的时候,如果使用阻塞式的I/O,那么可能长时间的阻塞在一个描述符上面,另外的描述符虽然有数据但是不能读出来,这样实时性不能满足要求,大概的解 ...
- Socket网络编程--epoll小结
以前使用的用于I/O多路复用为了方便就使用select函数,但select这个函数是有缺陷的.因为它所支持的并发连接数是有限的(一般小于1024),因为用户处理的数组是使用硬编码的.这个最大值为FD_ ...
- I/O多路转接-epoll
By francis_hao Aug 5,2017 APUE讲多路转接的章节介绍了select.pselect和poll函数.而epoll是linux内核在2.5.44引入的.在glibc ...
- [APUE]进程控制(上)
一.进程标识 进程ID 0是调度进程,常常被称为交换进程(swapper).该进程并不执行任何磁盘上的程序--它是内核的一部分,因此也被称为系统进程.进程ID 1是init进程,在自举(bootstr ...
- [APUE]UNIX进程的环境(下)
一.共享库 共享库使得可执行文件中不再需要包含常用的库函数,而只需在所有进程都可存取的存储区中保存这种库例程的一个副本.程序第一次执行的时候或第一次调用某个库函数的时候,用动态链接方法将程序与共享库函 ...
随机推荐
- 我的linux学习日记day5
一.vim 编辑器 有三种模式,命令模式,输入模式,末行模式 1.下面是命令模式常用的命令 2.末行模式常用命令 :w 保存 :q 退出 :q! 强制退出 :wq! 强制保存退出 :set nu 显示 ...
- 黑马程序员_毕向东_Java基础视频教程——switch语句练习(随笔)
switch(练习) /* if和 switch 语句很像. 具体什么场景下使用什么语句呢? 如果判断的具体数值不多且符合byte.short.int.char.String类型,虽然两个语句都可以使 ...
- Vi 和 Vim 的使用
Vi (Visual Interface)是 Linux下基于Shell 的文本编辑器,Vim (Visual Interface iMproved)是 Vi的增强版本,扩展了很多功能,比如对程序源文 ...
- Bash Shell之内建命令和保留字
转载自:http://blog.chinaunix.net/uid-25880122-id-2941630.html 命令 含义 ! 保留字,逻辑非 : 不做任何事,只做参数展开 . 读取文件并在sh ...
- 「雕爷学编程」Arduino动手做(25)——MQ2气敏检测模块
37款传感器与模块的提法,在网络上广泛流传,其实Arduino能够兼容的传感器模块肯定是不止37种的.鉴于本人手头积累了一些传感器和模块,依照实践出真知(一定要动手做)的理念,以学习和交流为目的,这里 ...
- Djano之ORM多表查询操作
# 把 model 转化为 迭代器去循环 MODEL.objects.all().iterator() # 等同于 values, values_list, 但是 only 这种方式 获取字段属性依旧 ...
- VIOS挂载ISO文件
6.VIOS挂载ISO文件 1.给vhost建立虚拟设备 mkvdev -vadapter vhostX -fbo -dev cdx 2.建立存放ISO的资料库 mkrep -sp rootvg -s ...
- Python之日志处理(logging模块二实战)
实战篇 import logging import logging.handlers LOG_PATH = r'./' def logConfig_1(): ''' 配置 log 输出到文件 : fi ...
- Robot Framework(2)- 快速安装
如果你还想从头学起Robot Framework,可以看看这个系列的文章哦! https://www.cnblogs.com/poloyy/category/1770899.html 安装RF cmd ...
- SpringCloud Alibaba 简介
SpringCloud Aliababa简介 SpringCloud Alibaba是阿里巴巴集团开源的一套微服务架构解决方案. 微服务架构是为了更好的分布式系统开发,将一个应用拆分成多个子应用,每一 ...