EISCONN的故事
在這春風明媚的日子里,有位T同學很苦惱。忙碌了一整天,有個BUG愣是定位不出來。簡單描述呢,現象是這樣子的:
第一次處理是正常的,但是后續的處理就是報錯。sendto()調用錯誤碼是 EISCONN(已被連接)。
憂傷的問題
當然,代碼BUG的范圍也很快確定了,就是新加入的statsd-client-cpp工具庫里。代碼量不到兩百行,失敗的地方就是在sendto()的執行里(代碼看這兒)。一看錯誤碼(EISCONN,比較少見),說“socket已經被連接”——但咱這明明是UDP協議啊,無連接無connect的!
T同學咨詢了周圍的大師們,翻閱了《Unix網絡編程》(沒錯,這書在上次的故事里也出場了!),里面說:
sendto()函數的執行流大約是這樣子的:
無語了。按照圣經里說法,連接都是被斷開了的啊!還怎么會報錯“已被連接”?!!
失敗的補救
T同學比較耿直,對付BUG比較直接粗暴:報啥錯誤就解決啥錯誤!思路有倆:
人肉斷開它!
這個實際上是行不通的(只能整個兒socket關閉銷毀掉,不能只斷開)
連接了也繼續發!
雖然一般情況下UDP協議的程序都是不管連接直接發sendto()的,但是先連接后再write()也是可行的。
于是按照方法2搞起!然而,實踐證明這是不給力的,UDP對端根本沒有收到任何數據!
正確的思路
這種頭痛醫頭、瞎試幾次的做法,本質上就不能算正確的方法。我們的思路應該是:
上面折騰了這么久,基本上還是只有一個錯誤碼和代碼出錯的位置,現象數據太少了。T同學冷靜下來后,開始祭出大殺器strace工具程序!
strace程序能夠捕抓系統調用(system call),并把這些調用接口的時間、參數、返回值、耗時等等都記錄下來;輸出信息可讀性相當的高(比起gdb爽多了),能反映出系統最底層的運行狀況,是后臺開發程序員的居家旅行必備的強(zhuang)大(bi)工具。
神奇的零
具體strace的過程就不多說了。調整進程數量、執行strace、查看log、研究執行狀況:
strace32 -s 10086 -o /tmp/Strace.log -tt -p $(pidof -s get_api_key)
終于發現一個關鍵現象:每次的連接socket()返回值都是0!
這里稍微解釋一下,為什么零值會是很特殊:socket()返回值表示的是文件描述符;在POSIX標準里,有三個特殊的文件描述符值0、1、2,分別是STDIN(標準輸入)、STDOUT(標準輸出)、STDERR(標準錯誤)。所以默認情況下,零值都會作為STDIN(標準輸入)使用;只有當程序主動關閉了STDIN時,系統才會分配0值給socket()使用。
所以這時候思路有兩個了:
這種“辯證”的思路,讓我忽然想起了《擼擼姐的超本格事件簿》里的給出各種偽解答搞笑助手:每次破案分析都至少有正反兩個思路,看起來毫無盲點,但總是被擼擼姐指出第三種情況!哈哈哈!!!不過在現實場景里,正反兩面都深入思考的做法,一般幫助很大的。
誰弄壞了0
所以0是被誰弄壞的?怎么弄壞的?
為了深入探究這個問題,得先了解一下程序運行的環境。這個工具庫之前已經在現網的服務器里跑了很久,也有兩個簡約的單元測試。都沒有發現過問題。這次是在往CGI接口里使用,運行環境是QZHTTP+FastCGI。
所以正常情況下,STDIN應該是CGI與qzhttp收包程序之間的連接,用來傳遞HTTP請求報文。而分析到的一個現象是,CGI程序的功能完全正常!也就是說程序之間傳遞數據(STDIN)是正常的……等等,STDIN(0值)不是分配給我們用嗎??為毛還不影響功能啊啊啊???
秘訣就是——dup2(a, b)!!這個函數能夠把一個文件描述符的信息拷貝到另一個描述符里!所以文章最初出現的“EISCONN”錯誤的原因是:
原本UDP無連接的socket,被人用dup2()“篡改”了,于是就變成了另外的文件描述符,出現了“已連接”的狀態。
寫個小程序驗證了一下,果然能夠隨便拷貝到STDIN描述符里!!!而再次strace抓包查看,也發現了明顯的dup調用:
可以清晰看到對STDIN、STDOUT都做了dup2()操作!!!太邪惡了!!!火速在萬能的StackOverflow.com上也找到了一篇關于0值的問答,里面有人提到了如何解決這個問題!那就是把0、1、2預留下來給系統!老子不陪你們玩零了!!(傲嬌地離去!)
另一個思路
等等,剛才怎么這么快就進入了“解決問題”的節奏??? T同學的案例雖然因為屏幕不夠長被滾動掉了,但是我們要分析所有的現象找到原因啊!
于是進入剛才的第二個思路:是誰把STDIN給關閉了??
深入strace的log,發現close(0)首次出現的位置沒有太特別,距離后來socket()和sendto()調用很遙遠,暫時發現不了什么(后來細想,考慮時序其實是個誤導)。
不過,因為是新引入的庫導致了這個問題,基本上猜測要么就是庫代碼有BUG,要么是庫和QZHTTP不兼容。重新閱讀代碼中與close()有關的片段,嘗試梳理一下思路.果然發現一個問題:
d->sock是個關鍵的值,而且沒有初始化!一般來說變量沒有初始化,會是一個隨機的值;但是我們的場景里,StatsdClient對象是一個單體實例,以static限定符實現的——所以是有初始值0的——剛好觸發了BUG。
解決
再次核對代碼邏輯和strace的log,腦補了一下執行的流程,基本上就確認BUG的原因就是這里了。
所以啊,在構造函數里增加一個初始化?d->sock = -1;?問題解決了。
寫到這里的時候,忽然想起上兩周給趙總用這個工具庫,他也出現了一些詭異問題。當時他的現象是:隨機出現accept錯誤。現在看來BUG原因都是一個,只不過趙總的使用方式是臨時變量,沒初始化的值就是隨機值,因此偶然觸發故障(他當時引起的是accept失敗,也是描述符問題)。而這次是必現的問題,定位起來輕松一些。
思考
所以,要保持良好的代碼編寫習慣。順手初始化了,就不會有這么糾結的問題了。
總結
以上是生活随笔為你收集整理的EISCONN的故事的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 曝小鹏汽车盗用图片宣传 官方回应:供应商
- 下一篇: u-boot的Makefile分析