关于多线程情况下Net-SNMP v3 版本导致进程假死情况的跟踪与分析
关于多线程情况下Net-SNMP v3 版本导致进程假死情况的跟踪与分析
1、问题描述
在使用net-snmp对交换机进行扫描的时候经常会出现进程假死的情况(就是进程并没有死掉,但是看不到它与外界进行任何的数据交互)。这时候不知道进程内部发生了什么,虽然有日志信息,但进程已经很长时间没有动静,根本不知道这段时间做了什么。用gdb att进去发现,进行snmp发送的线程已经被阻塞了。但是阻塞的情况并不是每次都发生,而是经常发生,这就导致很难捕捉问题。通过观察日志和 tcpdump 抓包,发现这种情况只在v3版本的时候出现,那就是v3版本有什么特别的地方。
2、调试跟踪
观察 gdb att 后的情况,发现每次都会挂在 recvmsg() 这个函数上。刚开始以为是在进行接收的时候出的问题,多看了几层栈才发现,原来在发送函数里面就卡住了,伤心啊……
下面是gdb看到的栈调用情况:
在snmp v3 协议中,要想能够请求数据,首先需要获取SNMP 协议引擎,也就是engineID号,然后根据这个再去请求索要的数据。目前遇到的情况就是在请求这个引擎ID的时候卡住了。看了下net-snmp的源码,在请求引擎ID的时候是没有设置超时的,也就是死等……
在 snmp_client.c 源文件中的 snmp_sess_synch_response() 函数中的源代码:
numfds = 0; FD_ZERO(&fdset); block = NETSNMP_SNMPBLOCK; tvp = &timeout; timerclear(tvp); snmp_sess_select_info(sessp, &numfds, &fdset, tvp, &block); if (block == 1) tvp = NULL; /* block without timeout */ count = select(numfds, &fdset, 0, 0, tvp); if (count > 0) { snmp_sess_read(sessp, &fdset); }
而且是用的 select !!!我们发送和接收用的可是多线程呀……
然后搜到这么一篇讨论 net-snmp 对多线程支持情况的文章:《Is Net-SNMP thread safe?》
译文在这里:《Net-SNMP是线程安全的吗》
文章开篇是这么说的,我忍不住要截图呀 !!!
问:Net-SNMP是线程安全的吗?
答:确切的说,不是!
我勒个去,多么干脆的回答,简直不忍直视……
虽然在文末给出了v3不支持多线程的原因,我还是感到不开心……
Unfortunately, the SNMPv3 support was added about the same time as the thread support and since they occurred in parallel the SNMPv3 support was never checked for multi-threading correctness. It is most likely that it is not thread-safe at this time.
3、分析多线程下net-snmp v3出现卡死的原因
在多线程下,因为我的程序对交换机的发送和接收是在不同线程的,导致一个很严重的问题就是线程间的同步。
我们假设有这样一种情况:
我们有两个线程,发送线程T1 和 接收线程 T2。T1 只负责调用 net-snmp 的发送函数接口,向交换机请求信息;T2只负责进行接收,并且是select 方式进行接收:
步骤:
1)T1线程 发送一个请求,然后就不管了,接收工作就交给T2线程 select吧
2)那么,此时这个发送线程 T1 可以继续向另一个snmp v3版本的交换机发送请求,在发送的过程中需要首先请求SNMP引擎,及请求engineID
3)T1线程发送完了,进行阻塞调用 select,直到有消息返回才会结束阻塞状态
注意,此时就出现问题了。我们发送请求和接收请求的时候使用两个不同的线程,也就是说,那个只负责接收的线程一直在进行select。这个时候就要看是谁能够接收到这个请求engineID的UDP包了,如果是此时的这个发送线程T1,那么万事大吉,程序继续向下走。如果这个请求engineID的包被只负责接收的线程 T2 收到了呢?那 T1 线程就没得接收了,那就只好等待了,由于没有设置超时,还是以阻塞方式进行调用的,结果就是死等……
所以现在遇到的问题是,发送线程 T1 不仅仅只是进行了发送,还进行了接收,并且在发送过程中出现的接收动作不是我们能控制的。当然我们也可以修改源代码,增加线程锁,但那需要更多的精力去研究 net-snmp 更多的代码,还不如在自己代码里加锁。
4、解决办法
还能有什么解决办法?只能加线程锁了。加锁的目的在于,使发送线程 T1 的发送过程和接收线程 T2 的select过程互斥的进行,不允许接收线程 T2 抢夺发送线程 T1 的数据。
解决方法:
1)在 T1 线程进行发送之前先加锁,发送完成后解锁
2)在 T2 线程 select 之前加锁,seelct结束之后解锁
结果:运行了很长事件了,没有再出现v3卡死的情况
但目前这种处理还是比较粗糙的,会导致T2线程在没有接收到返回数据时进行超时等待处理,一次超时等待需要3s,这对性能的损耗还是比较大的。
5、总结
开发的时候总会遇到各种各样的奇闻异事,习惯了就好了。今天把问题的发现及解决过程记录一下,按照茨威格在《昨日的世界》中所说的“倘若你想完全领悟伟大的杰作,你不仅要看到过它们的成品,而且必须了解到它们形成的过程”。所以我要记录下每一个问题的过程,不是因为这是什么杰作,而是因为这是我生命中的一件故事,当我垂垂暮年,回首往事,看到自己一路上记录的点滴,想起自己年轻时努力的身影,也许,这才是我送给未来自己最好的礼物吧。
作者:郝峰波
mail : fengbohello@qq.com