基于嵌入式Linux的TCP通信异常问题
2019-11-18俞理超胡益群袁昌权许光
文/俞理超 胡益群 袁昌权 许光
当前,由于Linux资源完全公开,使得Linux的发展日益广泛快速。基于Linux的各种应用已逐渐深入日常生活的方方面面,尤其是在嵌入式领域,由于内核可裁减定制,因此可随意地根据用户需求进行整个系统的定制与重构。Linux由于其内核特性,在网络通信的使用上可能会存在令人棘手的问题,针对不同的用户场景模型,需要进行不同的问题分析,可能涉及操作系统内核、任务调度、硬件驱动等原因。
1 问题分析与解决
现象描述:主控板运行Linux系统,与DSP(信号处理机)进行tcp通信。在通信中,DSP时不时进行复位操作,主控中有监测线程,监测tcp中断后,关闭当前tcp连接,并重新创建线程,与dsp建立tcp连接,应对通信错误。正常情况下,dsp复位后,主控仍能与DSP建立连接。在反复复位几次后,出现主控与dsp无法正常通信的情况。
问题分析与复现:
在问题分析初期,我们怀疑现象与用户程序有关,根据用户描述,开始尝试复现目标现象。根据用户描述,linux先起一个TCP客户端任务,去连DSP板起的TCP服务端,数据由linux发送给DSP服务端,节拍约为800ms;在linux中起另一个监控线程,观察TCP的connect状态,若观察到TCP断开,则释放资源,并重启线程,另启一个TCP服务端。在发送过程中,若DSP板复位,监控线程在正常重启线程若干次后,会出现通讯/线程异常的情况。
为了复现现象,使用了单TCP线程加上监控线程的模式,在反复重启DSP的过程中,出现了类似用户描述的现象,linux在发送过程中,若DSP复位(TCP客户端异常结束,TCP异常断开),linux进程自动结束的现象。根据此现象,查阅相关资料,得到以下的第一个结论。
现象机理:当Linux服务器监听并接受一个客户端链接的时候,可以不断向客户端发送数据,这时如果客户端断开socket链接,服务器继续向一个关闭的socket发送数据(send,write)的时候,系统会默认对服务器进程发送一个SIGPIPE信号,系统会出BrokePipe,关闭当前进程。经测试,Linux作为客户端发送数据时,也会有同样的问题。
系统里边定义了三种处理方法:
(1)SIG_DFL /* Default action */信号专用的默认动作:
(a)如果默认动作是暂停线程,则该线程的执行被暂时挂起。当线程暂停期间,发送给线程的任何附加信号都不交付,直到该线程开始执行,但是SIGKILL除外。
(b)把挂起信号的信号动作设置成SIG_DFL,且其默认动作是忽略信号(SIGCHLD)。
(2)SIG_IGN /* Ignore action */忽略信号
(a)该信号的交付对线程没有影响
(3)系统不允许把SIGKILL或SIGTOP信号的动作设置为SIG_DFL
(4)SIG_ERR /* Error return */
而TCP通信中send()函数的定义如下:
ssize_t send(int sockfd,const void *buff,size_t nbytes,int flags);
其中,send()函数的最后一个参数flag可以设MSG_NOSIGNAL,,禁止send()函数向系统发送异常消息,这样在发送数据异常时,系统就不会异常退出。通过与项目人员沟通,协助其修改程序。
经修改后,线程退出的现象有所好转,但依然存在,并且上述三种方法都无法彻底解决目标问题。进一步与项目人员交流发现,他们的linux程序比我们的测试程序更复杂,线程更多。在出现问题时,也发现了另一个bond1(外口)在ifconfig中查看,存在dropped包的现象。现象如下:
程序共有10个子线程,分别是:
子线程1:tcp_recv接收DSP程序刚起来时的握手信号(1个字节),获取DSP的IP地址以及端口号,子线程1在完成上述工作后,自动退出线程;
子线程2:multi_recv通过bond1双冗余的外口接收阵元数据,累计一定数据量后转发给子线程3、4、5,由这3个线程分别处理阵元数据;
子线程3:work_dp按格式转换处理低频数据,累积到1024*4096 float后通过信号量通知子线程6转发;
子线程4:work_hp按格式转换处理高频数据,累计到768*4096*3 float后通过信号量通知子线程7转发;
子线程5:work_senor按格式转换处理处理传感器数据,累积到512float,和低频数据拼接在一起,由子线程6一起转发;
子线程6:tcp_dp_send,收到子线程3的信号量后,通过bond0内网卡双网冗余转发给DSP程序,当返回值sendNum<0时,清空资源,退出程序;
子线程7:tcp_hp_send,收到子线程4的信号量后,通过bond0内网卡双网冗余转发给DSP程序,当返回值sendNum<0时,清空资源,退出程序;
子线程8:监控子线程6和子线程7的TCP的Connect State,发现有TCP连接断开后重启线程1、线程6、线程7、等待DSP重连;
子线程9:通过bond1定时上报心跳程序;
子线程10:给数据库上报丢包信息;
系统工作流程:主线程运行后,将10个子线程创建并起动;其中可能涉及内存与调度/双网网卡状态等问题。
进一步测试发现,当DSP复位时,其0核和1核网络同时复位,但线程6,7中判定其TCP断开并非同时,当问题复现时,线程6的TCP状态为断开,监控线程重启线程1,6,但现场7的TCP未被判定断开,最终导致了通信错误。
监控线程未正常工作可能的原因是:
1、线程的切换优先级不恰当,导致监控线程中判断线程7中的标志位值未改变;
2、DSP复位的网络初始化问题导致。
针对上述两种可能,选择了修改程序优先级以及信号量触发监控线程的模式来测试;在测试结果中,监控线程正常的起到了kill线程并重启的功能;在另一种解决方式中,采取了如下的方式:当一路TCP断开时,直接kill线程6与线程7,并重启线程1,6,7,这是基于DSP(TCP客户端)两路会同时重启或断开的情景。
关于存在dropped包现象:
在SUSE的kb中可以发现如下内容:
Beginning with kernel 2.6.37,it has been changed the meaning of dropped packet count.Before,dropped packets was most likely due to an error.Now,the rx_dropped counter shows statistics for dropped frames because of:
从2.6.37内核以后,改变了dropped包的统计方式,其不再是以错误包的方式统计,以下情况也会计入dropped包。其值只是做为一种状态统计了。
最后一句中说明,在bond主备模式中,备用网卡接到的所有包都会计入dropped。
而redhat和其他发行对应的发行版如SUSE、ubuntu等来比,kernel和包都相对版本要低一些,这和其策略有关,未确认稳定的,不会加入到当前的发行版本中。在未单独升级过kernel的情况下,其只在rhel7中才会有该情况发生,而且其kb给出了前后统计方式不同的源码,具体变化在rt_kernel core/dev.c文件中。
因此dropped包原因可能是TCP中断导致的双网切换有关。
2 结语
针对linux的TCP通讯,linux不是一个完全照顾吞吐的系统,也不是一个完全照顾响应的系统,它是两者的兼容,是一个软实时系统。优先级和调度策略均会影响Linux的工作状态,并且在内核在处理错误信息时,部分信号可能会直接杀死进程,导致程序异常。根据不同的应用场景与网络问题,应同时考虑硬件、内核、任务调度等方面来考虑问题。在TCP通信过程中,任务调度会影响TCP的速率和响应,TCP通信异常的信号会导致内核直接杀死进程。