一、背景
在某一时刻,大量的TCP长连接接入服务导致服务崩溃重启
二、问题分析
根据coredump文件以及抓包文件得知,是eXosip线程在执行_eXosip_keep_alive时,检查tcp连接出现了段错误。通过对函数 tcp_tl_check_connection分析,结合当时的大量未断链TCP接入,判断是执行 _tcp_tl_is_connected 的 FD_SET 操作时发生错误。通过查看源码可知,POSIX给出的FD_SIZESET为1024。
通过添加打印日志,确实看到了socket大于1023的情况(socket是从0开始计数的)并在此后发生了coredump。所以此问题无解(或重新编译源码)?事实真的是最大的socket_size只能是1024吗?
接下来具体认识下 fd_set 及其位图操作。
三、fd_set及其位图操作
如上图,可以看到,fd_set 是一个 long int 类型长度为16的数组,因此,在连续地址空间上,其占用 16*8bytes = 1024bit的地址空间。这个1024就是POSIX定义的最大文件描述符值,其可以表示的socket值范围为 0 - 1023。为什么取值范围与索引范围保持一致,不能socket=25555表示在任意的位置?原因在于fd_set实际上是一个位图数组,索引的头即为数组的首地址。
当执行 FD_SET(sock, rset),将sock加入到文件描述符集rset中时,实际上是将从索引地址开始的第sock+1个bit位置为1(sock从0计数),所以fd_set的每一个bit位都可以代表一个socket。所以在POSIX的建议中,socket最大值为1024,并且其值与其对应的bit位索引保持一致,这就是socket数量和其值的关系。
可能你会说,既然最大的socket为1023,那么为什么会出现大于1023的socket值呢?实际上,Linux内核对于socket的值是没有做限制的(在系统可使用的fd数量范围内),通过代码可以测试得出,socket的值是完全可以超过1023的。将socket值大于1023的socket加入到fd_set中会发生什么呢?Linux内核的FD_SET(sock, rset)操作不会去校验fd_set数组的长度,它只会拿到位图数组的地址,然后根据传入的sock作偏移操作,即将地址空间的rset+sock的bit位地址置为1。当sock大于1023时,偏移指针会越过rset的地址空间向高地址空间继续移动,直到找到寻找的bit位,将其置为1。此时,没人知道它到底修改了什么数据,可能是将栈底的其他数据进行了修改或覆盖,这就会产生段错误导致程序coredump。
四、解决方案
既然知道了根源在于POSIX定义的fd_set的数组空间太小所致,那如何解决呢?其实,将fd_set放在堆中即可,与此同时,使用一个全局变量记录当前TCP连接的最大socket。每一次基于该全局变量为其分配合理大小的地址空间。这样,即使出现socket大于1023的情况,bit置位地址也不会超过预先分配的fd_set地址空间,但要注意及时free。基于以上处理可解决socket连接数过大的问题
五、总结
- POSIX接口限制为1024个文件描述符。
- fd_set的位索引就是文件描述符索引。
- Linux 内核没有任何限制。
- POSIX的1024限制很可能会越界并造成数据覆盖。
- POSIX的1024限制需要用堆内存突破限制,想要多大有多大。