kernel对于SO_REUSEADDR的处理——避免滥用引发Bug

简介:
本文的copyleft归gfree.wind@gmail.com所有,使用GPL发布,可以自由拷贝,转载。但转载请保持文档的完整性,注明原作者及原链接,严禁用于任何商业用途。
作者:gfree.wind@gmail.com
博客:linuxfocus.blog.chinaunix.net
  
   

今天有一个客户问题,问题的现象的大致情形如下:有两个不同的daemon服务进程,负责不同的服务。在某种情况下,进程A可以作为进程B的一个代理。某些client在通过进程A的认证后,通过进程A获得进程B分配的资源。客户的问题是在认证过后,无法获得进程B分配的资源。

我检查了log,在问题发生的时候,进程B有分配资源的记录,而且从时间上看,分配并没有耗费太多的时间。那么是否是进程A的超时机制处理有问题,导致进程A错误的判断超时了呢。比如没有考虑时间溢出的问题。因为进程A的代码不是我写的,所以我重新review了一些代码。发现虽然代码还是有些问题,不仅没有考虑到时间溢出的问题,而且在保存时间和比较时,使用的是有符号数。这样即使没有到时间溢出的时候,有符号的时间值就可能会被当成负数了,从而引发问题。不过按照当前时间,可以排除这个情况。还有其它的一些小问题,也不会有大的影响。

这样,排除了超时的错误后,我检查了进程A用于向进程B发送请求的代码,果然发现了问题。进程A是一个多线程的进程。估计是当时为了简单处理和并发性,在向进程B发送请求时,使用的是局部变量socket来发送UDP请求给B,而不是有一个独立的线程来做这件事情。且在使用socket时,又bind了本地地址和知名的本地端口。而为了多个socket可以同时bind本地地址和相同端口,特意在bind前,设置了SO_REUSEADDR。结果,恰恰是这种行为导致了这个bug的发生!之所以要使用固定端口,因为进程B这个协议,在发送response时,不会根据client发过来的port回复,而只是回复到client的知名端口,所以不能进程A不能使用随机端口。

在揭开谜底之前,让我们先温习一下究竟在什么情况下需要使用SO_REUSEADDR呢。已故的W.Richard Stevens大师,在他的UNP1中列举了四种情况。
  1. 当有一个有相同本地地址和端口的socket1处于TIME_WAIT状态时,而你启
    动的程序的socket2要占用该地址和端口,你的程序就要用到该选项。另外作为服务daemon一般都会使用SO_REUSEADDR,避免服务意外崩溃而原有socket还未被kernel释放时,重启的daemon仍然可以bind成功。
  2. SO_REUSEADDR允许同一port上启动同一服务器的多个实例。但
    每个实例绑定的IP地址是不能相同的。在有多块网卡或用IP Alias技术的机器可
    以测试这种情况。
  3. SO_REUSEADDR允许单个进程绑定相同的端口到多个socket上,但每个soc
    ket绑定的ip地址不同。这和2很相似,区别请看UNPv1。
  4. SO_REUSEADDR允许完全相同的地址和端口的重复绑定。但这只用于UDP的
    多播,不用于TCP。
而当前的情况并不属于这4种的任何一种。那么这时,假设进程A同时打开了5个socket,并都成功bind了本地地址和相同的端口,且发送了请求给进程B。那么当进程B回应时,是什么样的情形呢?

按照第一感觉,似乎kernel应该把数据包发给每一个socket,也就是说如果这5个socket没有关闭,每个socket都应该收到5个数据包。——至少以前的我,会这么理解。

可现实往往与人们的第一感觉向左的。linux kernel在收到数据包,除了raw socket,只会选择一个最匹配的socket来接收数据包。以UDP为例,执行这一个工作的是__udp4_lib_lookup_skb函数。通过比对源地址,源端口,目的地址,目的端口以及bind的网卡,来找到最匹配的socket。使用compute_score这个函数来计算每个socket的分数,具体行为请参考kernel代码。当没有完全匹配的时候,才会匹配具有通配地址或端口的socket。

正因为linux kernel的这一行为,导致了这5个socket中,只有1个socket收到了所有的5个数据包。而这个socket只关心其中的一个并成功处理而后关闭socket。而其他4个socket则根本收不到任何数据包。这时这4个socket重发请求,而4个回复仍然只会被其中的1个socket收到,而导致另外3个socket失败。这里之所以每次都是1个socket收到所有的数据包,是因为每个socket都bind相同的地址和端口,那么对于kernel来说,这些socket的匹配得分肯定都相等。那么每次数据包到来,kernel都只会选择第一个socket。

通过上面的讲解,就可以确定了正是对SO_REUSEADDR的滥用,才导致了这个bug。那么,究竟怎样解决这个bug呢。我想出了2个方案:
1. 在进程A中,创建一个新的线程,专门用于向进程B发送请求和接受回复,来代替当前的机制。不过这样的改动会比较大些。
2. 利用linux中raw socket的特性——linux会复制所有符合raw socket过滤条件(使用bind和connect)的数据包到每一个raw socket,替换现有的UDP socket。这样每个socket都可以收到所有的回复,当然只会处理对应该socket发送出的请求的回复。这样的改动最简单,不过在效率上稍微差了一点。


根据我们的情况,因为通过进程A来申请进程B资源的客户请求并不经常使用。所有,最终,我选择使用raw socket这个方案来解决这个Bug。

这次我能够很快的发现这个根本原因。主要是因为这段时间我正在学些linux的TCP/IP的源代码。所以在看到进程A使用SO_REUSEADDR的情况时,直接想起了kernel挑选socket的最佳匹配策略。因此,直接找到了根本原因。可见,即使我们并不是kernel 开发人员,也要尽量的多了解kernel的内部原理和机制,对于应用层的开发有着极大的益处——如果我不知道这个匹
配策略,估计这个bug需要我研究好几天,而非今天的迅速解决。


另外从这个bug中,我们也应该得到教训。切莫滥用SO_REUSEADDR,只能在正确的情况下使用!不然的话,等着bug吧 4.gif
目录
相关文章
|
存储 算法 C++
C/C++工程师面试题(STL篇)
C/C++工程师面试题(STL篇)
410 6
|
安全 编译器 Go
Go语言中的int和int32:同一个概念吗?
【2月更文挑战第24天】
1303 3
|
8月前
|
JavaScript Linux Python
在Linux服务器中遇到的立即重启后的绑定错误:地址已被使用问题解决
总的来说,解决"地址已被使用"的问题需要理解Linux的网络资源管理机制,选择合适的套接字选项,以及合适的时间点进行服务重启。以上就是对“立即重启后的绑定错误:地址已被使用问题”的全面解答。希望可以帮你解决问题。
433 20
|
4月前
|
运维 网络协议 安全
为什么经过IPSec隧道后HTTPS会访问不通?一次隧道环境下的实战分析
本文介绍了一个典型的 HTTPS 无法访问问题的排查过程。问题表现为 HTTP 正常而 HTTPS 无法打开,最终发现是由于 MTU 设置不当导致报文被丢弃。HTTPS 因禁止分片,对 MTU 更敏感。解决方案包括调整 MSS 或中间设备干预。
|
机器学习/深度学习 自然语言处理 语音技术
使用Python实现深度学习模型:智能产品设计与开发
【10月更文挑战第2天】 使用Python实现深度学习模型:智能产品设计与开发
248 4
|
人工智能 自然语言处理 搜索推荐
博物馆地图导览系统:GIS与蓝牙定位技术实现地图导览与语音解说功能
维小帮博物馆地图导览系统结合GIS地图、蓝牙定位及智能语音解说,为访客提供沉浸式导览。系统采用自研地图引擎,精准构建三维模型,支持路径规划与个性化定制。蓝牙技术实现高精度室内定位及自动触发语音解说功能,无需手动操作。系统还支持多语言解说与AI语音生成,提升参观体验。目前已在多个博物馆应用并获好评。期待与您共同推进文化科技的融合发展!
535 3
|
机器学习/深度学习 人工智能 供应链
智能制造:AI驱动的生产革命——探索生产线优化、质量控制与供应链管理的新纪元
【7月更文第19天】随着第四次工业革命的浪潮席卷全球,人工智能(AI)正逐步成为推动制造业转型升级的核心力量。从生产线的智能化改造到质量控制的精密化管理,再到供应链的全局优化,AI技术以其强大的数据处理能力和深度学习算法,为企业开启了全新的生产效率和质量标准。本文将深入探讨AI在智能制造中的三大关键领域——生产线优化、质量控制、供应链管理中的应用与影响,并通过具体案例和代码示例加以阐述。
1632 3
|
SQL 缓存 网络协议
C++实现MySQL数据库连接池
为了提升MySQL数据库(基于C/S设计(客户端-服务器))的访问瓶颈,除了在服务器端增加缓冲服务器缓存常用的数据之外
771 0
|
存储 索引
STM32实战项目—停车计费系统
本文详细介绍了一个停车计费系统的任务要求,实现思路。最后,给出了详细的程序设计和测试结果。
504 2
STM32实战项目—停车计费系统