现象:

某个线上的服务最近频繁崩溃。该服务使用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 >& &

“--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个客户端,崩溃很快就出现了。查看日志文件,发现了下面的信息:

==== Syscall param select(writefds) points to uninitialised byte(s)
==== at 0x5D6DBD3: ??? (in /usr/lib64/libc-2.17.so)
==== by 0x584984: Socket::WaitToWrite(int) (socket.cpp:)
==== by 0x583FC7: Socket::TimedSend(char const*, int, int, bool) (socket.cpp:)
...
==== Address 0x1ffeffeff0 is on thread 's stack
==== in frame #, created by Socket::WaitToWrite(int) (socket.cpp:) ... ==== Invalid write of size
==== at 0x583FC8: Socket::TimedSend(char const*, int, int, bool) (socket.cpp:)
...
==== 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 / , (nTimeout % ) * };
return select(m_hSocket + , 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。

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

  1. websocketj--随时随地在Web浏览器中操作你的服务端程序

    0 - 有没有觉得Linux标准终端界面输入输出枯燥无味? 1 - 什么?vmstat命令的输出数据不直观?有没有想过能够可视化该命令的输出? 2 - 尝试过用浏览器操作Windows中的cmd吗? ...

  2. 03-案例——多任务版TCP服务端程序开发

    案例——多任务版TCP服务端程序开发   1. 需求     目前我们开发的TCP服务端程序只能服务于一个客户端,如何开发一个多任务版的TCP服务端程序能够服务于多个客户端呢?完成多任务,可以使用线程 ...

  3. node.js服务端程序在Linux上持久运行

    如果要想在服务端部署node.js程序,让其持久化运行,就不能单单使用npm start命令运行,当然了,这样运行是毫无问题的,但是当关闭xshell窗口或者是关闭进程的时候(其实关闭xshell窗口 ...

  4. zabbix3.0.4关于java服务端程序内存溢出的处理

    关于java服务端程序内存溢出的处理 java服务端程序内存溢出会产生jvm.log文件,此时程序会挂掉,无法正常处理业务,需要重启服务 思路: 当存在jvm.log这个文件的时候则触发clean_j ...

  5. 第一个socket服务端程序

    第一个socket服务端程序 #include <stdio.h> #include <stdlib.h> #include <string.h> #include ...

  6. Python中的Tcp协议的应用之Tcp服务端程序开发

    TCP通信协议是面向连接的可靠的网络通信协议. 网络间想要进行数据传输必须要用到socket,socket翻译过来叫做套接字,其主要作用是不同设备或同一台设备之间的进程通信工具. Python中的Tc ...

  7. 【技术分享】linux各种一句话反弹shell总结——攻击者指定服务端,受害者主机(无公网IP)主动连接攻击者的服务端程序(CC server),开启一个shell交互,就叫反弹shell。

    反弹shell背景: 想要搞清楚这个问题,首先要搞清楚什么是反弹,为什么要反弹.假设我们攻击了一台机器,打开了该机器的一个端口,攻击者在自己的机器去连接目标机器(目标ip:目标机器端口),这是比较常规 ...

  8. 《精通并发与Netty》学习笔记(02 - 服务端程序编写)

    上节我们介绍了开发netty项目所必需的开发环境及工具的使用,这节我们来写第一个netty项目 开发步骤 第一步:打开https://search.maven.org 找到netty依赖库 第二步:打 ...

  9. Redis 服务端程序实现原理

    上篇我们简单介绍了 redis 客户端的一些基本概念,包括其 client 数据结构中对应的相关字段的含义,本篇我们结合这些,来分析分析 redis 服务端程序是如何运行的.一条命令请求的完成,客户端 ...

随机推荐

  1. Hadoop生态系统概况(转)图文并茂说的不错

    Hadoop是一个能够对大量数据进行分布式处理的软件框架.具有可靠.高效.可伸缩的特点. Hadoop的核心是HDFS和Mapreduce,hadoop2.0还包括YARN. 下图为hadoop的生态 ...

  2. Leetcode506.Relative Ranks相对名次

    给出 N 名运动员的成绩,找出他们的相对名次并授予前三名对应的奖牌.前三名运动员将会被分别授予 "金牌","银牌" 和" 铜牌"(" ...

  3. 跟我一起做一个vue的小项目(四)

    接下来我们进行的是轮播页面下面的导航页的开发 我们需要的是实现轮播页下面的图标,并且实现轮播效果 这个话,其实基本思路先是渲染出小图标,然后,我们要对页数进行判断,如果图标的个数展示的就是8个,那个这 ...

  4. 微服务开源生态报告 No.10

    「微服务开源生态报告」,汇集各个开源项目近期的社区动态,帮助开发者们更高效的了解到各开源项目的最新进展. 社区动态包括,但不限于:版本发布.人员动态.项目动态和规划.培训和活动. 非常欢迎国内其他微服 ...

  5. android 数据绑定(2)绑定表达式

    1.官方文档 https://developer.android.com/topic/libraries/data-binding/expressions.html 2.绑定表达式的约束 2.1 允许 ...

  6. clientHeight、offsetHeight 区别 笔记

    一张图 说明全部 clientHeight和clientWidth用于描述元素内尺寸,是指元素内容+内边距大小,不包括边框(低版本IE下实际包括).外边距.滚动条部分 offsetHeight和off ...

  7. 容斥原理学习(Hdu 4135,Hdu 1796)

    题目链接Hdu4135 Co-prime Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Oth ...

  8. C#基础之Async和Await 的异步编程

    官方文档地址:https://docs.microsoft.com/zh-cn/dotnet/csharp/programming-guide/concepts/async/ Coffee cup = ...

  9. python 产生二项分布图练习

  10. 七牛云+MPic-图床神器搭建

    1. 注册七牛云 2. 新建存储空间 3. 密钥 4. 记录自己账户四个值: 测试域名:xxxxx.xx.clouddn.com ak:xxxxxxxxxxxxxxxxxxxx sk:xxxxxxxx ...