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、總結
開發的時候總會遇到各種各樣的奇聞異事,習慣了就好了。今天把問題的發現及解決過程記錄一下,按照茨威格在《昨日的世界》中所說的“倘若你想完全領悟偉大的傑作,你不僅要看到過它們的成品,而且必須了解到它們形成的過程”。所以我要記錄下每一個問題的過程,不是因為這是什么傑作,而是因為這是我生命中的一件故事,當我垂垂暮年,回首往事,看到自己一路上記錄的點滴,想起自己年輕時努力的身影,也許,這才是我送給未來自己最好的禮物吧。