select引起的服务端程序崩溃问题

现象:

某个线上的服务最近频繁崩溃。该服务使用C++编写,是个网络服务端程序。作为TCP服务端,接收和转发客户端发来的消息,并给客户端发送消息。该服务跑在CentOS上,8G内存。线上环境中,与客户端建立的TCP连接大约在3~4万左右。

使用GDB查看每次崩溃产生的core文件,发现崩溃时的函数调用栈每次都各不相同,而且有时会发生在比较奇怪的地方,比如标准库std::string的析构函数中。

该线上服务崩溃之后,会有监控进程进行重启,因此暂时不会造成太大的影响。

复现:

先尝试在自己的虚拟机环境中复现,考虑到虚拟机环境资源有限,如果无法复现则尝试在测试环境中复现。

首先编写模拟的客户端程序,该客户端程序需要尽可能地模拟实际客户端的所有动作:能够发送实际客户端所有可能发送的消息,并且会在随机的时间内向服务端建链和断链,该客户端是一个死循环后台程序,不断的重复建链、发消息、断链这一过程。

客户端程序写好之后,为了模拟线上环境中大量TCP连接的情况,编写一个脚本,循环启动多个客户端程序。

因虚拟机资源有限,先启动1000个客户端,也就是建立1000个TCP连接。结果崩溃未能复现。考虑可能还是连接数太少,改为1500个,这之前需要先调整Linux系统的最大打开文件数的限制,该限制默认是1024,调为102400。

启动1500个客户端,运行一段时间后,崩溃出现了!

查找原因:

考虑到崩溃问题大部分都是因为内存问题引起的,因此尝试使用valgrind工具查找崩溃原因。

valgrind是一套Linux下的仿真调试工具集合,其中的memcheck工具是检查内存问题的利器,它能够检查C/C++中的内存问题有:

内存泄露;

访问非法的内存地址,比如堆和栈之外的内存,访问已经被释放的内存等;

使用未初始化的值;

错误的释放内存,比如重复释放,错误的malloc/new/new[]和free/delete/delete[]匹配;

memcpy()相关函数中的dst和src指针重叠;

申请内存时传递给分配函数错误的size参数;

使用valgrind启动服务端程序,命令如下:

valgrind --tool=memcheck --leak-check=full --show-leak-kinds=all --track-origins=yes /path/to/service -c /path/to/service/configfile > /path/to/logfile 2>&1 &

“--tool=memcheck”表示使用内存检查工具memcheck;

”--leak-check=full --show-leak-kinds=all”表示检查并列出所有类型的内存泄露信息;

“--track-origins=yes”表示列出所有使用未初始化值时的信息;

“/path/to/service -c /path/to/service/configfile > /path/to/logfile 2>&1”表示启动服务端程序,并将所有标准输出和标准错误输出都重定向到/path/to/logfile中。

使用valgrind启动服务端程序,然后启动1500个客户端,崩溃很快就出现了。查看日志文件,发现了下面的信息:

==4663== Syscall param select(writefds) points to uninitialised byte(s)
==4663==    at 0x5D6DBD3: ??? (in /usr/lib64/libc-2.17.so)
==4663==    by 0x584984: Socket::WaitToWrite(int) (socket.cpp:553)
==4663==    by 0x583FC7: Socket::TimedSend(char const*, int, int, bool) (socket.cpp:251)
...
==4663==  Address 0x1ffeffeff0 is on thread 1‘s stack
==4663==  in frame #1, created by Socket::WaitToWrite(int) (socket.cpp:547)

...

==4663== Invalid write of size 4
==4663==    at 0x583FC8: Socket::TimedSend(char const*, int, int, bool) (socket.cpp:251)
...
==4663==  Address 0x4001ffefff05c is not stack‘d, malloc‘d or (recently) free‘d

前面的信息:”Syscall param select(writefds) points to uninitialised byte(s)”说明select系统调用中的writefds参数指向了未初始化的内存,内存地址是0x1ffeffeff0,该地址在线程1的栈中;

后面的信息:”Invalid write of size 4”表示一个非法的内存写操作,写入的地址0x4001ffefff05c既不是栈上的地址,也不是malloc申请的堆上地址。这就是造成崩溃的原因了。

根据valgrind给出的信息,查看源代码,这里的函数调用关系是Socket::TimedSend->Socket::WaitToWrite->select。在Socket::WaitToWrite函数中,调用select部分的代码是:

fd_set writeSet;
FD_ZERO(&writeSet);
FD_SET(m_hSocket, &writeSet);
timeval tv = { nTimeout / 1000, (nTimeout % 1000) * 1000 };
return select(m_hSocket + 1, NULL, &writeSet, NULL, &tv);

这段代码就是监控描述符m_hSocket在nTimeout毫秒的时间内是否可写。到这里,基本已经知道出问题的原因了。原因就在于linux下select的限制造成的。

linux下的select限制

select所使用的fd_set结构,本质上是一个固定长度的位数组。宏FD_CLR() 和 FD_SET()根据描述符的值,设置位数组中相应的位为0或为1,以此决定监控哪些描述符。在linux下,fd_set这个位数组固定为1024bit,也就是仅能处理值为0到1023的描述符。

因此,当连接数越大时,服务端创建的描述符越多,描述符的值也就会越大。对于有上万连接的服务端而言,描述符的值肯定已远远超过1024。

具体到代码中,如果m_hSocket这个描述符的值很大,则FD_SET根据其值设置writeSet位数组的相应位时,就是一个内存越界的写操作,对应于valgrind给出的信息:”Invalid write of size 4”。对于以万计的m_hSocket而言,这个写操作修改的很可能是其他函数栈的信息,因而崩溃时的函数调用栈各不相同且比较奇怪了。

并且,select系统调用根据m_hSocket的值决定访问writefds的界限,m_hSocket的值很大的情况下,select系统调用也就访问到了writefds实际长度之后的内容,因而valgrind会打印:”Syscall param select(writefds) points to uninitialised byte(s)”

解决方法:

在linux平台下,使用poll代替select。

总结:

Linux下select的限制问题是网络编程中容易被忽视的坑,有一些很成熟的开源代码如redis和rabbitmq-c都曾遇到过这个坑:https://github.com/antirez/redis/issues/267https://github.com/alanxz/rabbitmq-c/issues/168

当前的网络环境下,连接数上万是很稀松平常的是,因此在Linux平台下的网络服务端程序中,应该尽量避免使用select,而改用poll或epoll。

时间: 2024-10-15 04:26:47

select引起的服务端程序崩溃问题的相关文章

一个简单的客户单与服务端程序

实验环境是linux系统,效果如下: 1.启动服务端程序,监听在6666端口上  2.启动客户端,与服务端建立TCP连接  3.建立完TCP连接,在客户端上向服务端发送消息 4.断开连接 实现的功能很简单,但是对于初来乍到的我费了不少劲,因此在此总结一下,如有错点请各位大神指点指点 什么是SOCKET(插口): 这里不用 "套接字" 而是用 "插口" 是因为在<TCP/IP协议卷二>中,翻译时也是用 "插口" 来表示socket的.

站在服务端程序员的角度下的一下编程看法

作者:陈硕链接:https://www.zhihu.com/question/22608820/answer/21968467来源:知乎著作权归作者所有.商业转载请联系作者获得授权,非商业转载请注明出处. 既然你是在校学生,而且编程语言和数据结构的基础还不错,我认为应该在<操作系统>和<计算机体系结构>这两门课上下功夫,然后才去读编程方面的 APUE.UNP 等书. 下面简单谈谈我对学习这两门课的看法和建议,都是站在服务端程序员的角度,从实用主义(pragmatic)的立场出发而言

也谈如何构建高性能服务端程序

引子 我接触过很多编程语言,接触过各种各样的服务器端开发,Java,Go,Ruby,Javascript等语 言,Spring,Node.js,Rails等等常见服务器端框架和编程模型都有接触.这里谈一下我个人对高性能服务器端程序的一些看法,希望给各位读 者一些认识.这片文章提到的内容也是 Coding(https://coding.net) 代码托管乃至整站都在使用的一些概念和技术. 此外,阅读这篇文章,有如下几个前提:不谈硬件,不评论编程语言以及框架的好坏,不谈高级算法,可拍砖,拒绝喷子 三

[转]也谈如何构建高性能服务端程序

我接触过很多编程语言,接触过各种各样的服务器端开发,Java,Go,Ruby,Javascript等语言,Spring,Node.js,Rails 等等常见服务器端框架和编程模型都有接触.这里谈一下我个人对高性能服务器端程序的一些看法,希望给各位读者一些认识.这片文章提到的内容也是 Coding(https://coding.net) 代码托管乃至整站都在使用的一些概念和技术. 此外,阅读这篇文章,有如下几个前提:不谈硬件,不评论编程语言以及框架的好坏,不谈高级算法,可拍砖,拒绝喷子 三个关键词

第5章-unix网络编程 TCP/服务端程序示例

这一章主要是完成一个完整的tcp客户/服务器程序.通过一很简单的例子.弄清客户和服务器如何启动,如何终止,发生了某些错误会发生什么.这些事很重要的 客户端代码 #include "unp.h" //static void str_cli1(FILE*fp,int sockfd); int main(int argc,char *argv[]) { int sockfd; struct sockaddr_in servaddr; sockfd=Socket(AF_INET,SOCK_ST

win32汇编实现一个简单的TCP服务端程序(WinSock的简单认知应用)

Windows网络编程,相信好多人都知道,但是我们一般都是用其他语言编写,例如C,C++,JAVA,python等等,这些语言都可以,但是汇编语言比较底层,利用它,我们可以更清晰的了解到网络编程的内在部分,这是其他语言不能相比的,好了,废话不多说,这其实就是这次的目的(毕竟水平欠缺,还是先来按照罗云斌老师的WIN32汇编书上的例子加以学习,举一反三吧). 说道网络编程,现在我所接触到的程序开发,工具软件的使用,库等等都是基于Windows平台的,想要了解Windows的网络编程就必须要知道Win

轻易实现基于linux或win运行的聊天服务端程序

对于不了解网络编程的开发人员来说,编写一个良好的服务端通讯程序是一件比较麻烦的事情.然而通过EC这个免费组件你可以非常简单地构建一个基于linux或win部署运行的网络服务程序.这种便利性完全得益于mono这些年来的不停发展.下面介绍通过EC这个组件如何通过短短十来分钟的时候内就能实现一个聊天室通讯服务程序. 在实现一个网络通讯程序的时候需要定义一个通讯协议,但EC已经集成了基础的协议功能,只需要根据交互的数据定义消息类型即可(EC提供两种序列化对象描述分别是protobuf和msgpack).

用socaket编写客户端与服务端程序相互发送消息

//运行环境:eclipse || MyEclipse package socaket; //这些类全放在socaket的包下 //这是服务器端消息发送类 import java.io.IOException;import java.io.PrintWriter;import java.net.Socket;import java.util.Scanner; public class ServerChatSend extends Thread {//服务端发送线程 Socket ssk = nu

Client(客户端) / Server(服务端) 程序

1. 客户端程序 import java.io.*; import java.net.*; public class Client { public static void main(String args[]) { try{ Socket socket=new Socket("127.0.0.1",4700); //向本机的4700端口发出客户请求 BufferedReader in=new BufferedReader(new InputStreamReader(System.in