天天看點

kernel對于SO_REUSEADDR的處理——避免濫用引發Bug

本文的copyleft歸[email protected]所有,使用GPL釋出,可以自由拷貝,轉載。但轉載請保持文檔的完整性,注明原作者及原連結,嚴禁用于任何商業用途。

作者:[email protected]

部落格:linuxfocus.blog.chinaunix.net     

今天有一個客戶問題,問題的現象的大緻情形如下:有兩個不同的daemon服務程序,負責不同的服務。在某種情況下,程序A可以作為程序B的一個代理。某些client在通過程序A的認證後,通過程序A獲得程序B配置設定的資源。客戶的問題是在認證過後,無法獲得程序B配置設定的資源。

我檢查了log,在問題發生的時候,程序B有配置設定資源的記錄,而且從時間上看,配置設定并沒有耗費太多的時間。那麼是否是程序A的逾時機制處理有問題,導緻程序A錯誤的判斷逾時了呢。比如沒有考慮時間溢出的問題。因為程序A的代碼不是我寫的,是以我重新review了一些代碼。發現雖然代碼還是有些問題,不僅沒有考慮到時間溢出的問題,而且在儲存時間和比較時,使用的是有符号數。這樣即使沒有到時間溢出的時候,有符号的時間值就可能會被當成負數了,進而引發問題。不過按照目前時間,可以排除這個情況。還有其它的一些小問題,也不會有大的影響。

這樣,排除了逾時的錯誤後,我檢查了程序A用于向程序B發送請求的代碼,果然發現了問題。程序A是一個多線程的程序。估計是當時為了簡單處理和并發性,在向程序B發送請求時,使用的是局部變量socket來發送UDP請求給B,而不是有一個獨立的線程來做這件事情。且在使用socket時,又bind了本地位址和知名的本地端口。而為了多個socket可以同時bind本地位址和相同端口,特意在bind前,設定了SO_REUSEADDR。結果,恰恰是這種行為導緻了這個bug的發生!之是以要使用固定端口,因為程序B這個協定,在發送response時,不會根據client發過來的port回複,而隻是回複到client的知名端口,是以不能程序A不能使用随機端口。

在揭開謎底之前,讓我們先溫習一下究竟在什麼情況下需要使用SO_REUSEADDR呢。已故的W.Richard Stevens大師,在他的UNP1中列舉了四種情況。

當有一個有相同本地位址和端口的socket1處于TIME_WAIT狀态時,而你啟

動的程式的socket2要占用該位址和端口,你的程式就要用到該選項。另外作為服務daemon一般都會使用SO_REUSEADDR,避免服務意外崩潰而原有socket還未被kernel釋放時,重新開機的daemon仍然可以bind成功。

SO_REUSEADDR允許同一port上啟動同一伺服器的多個執行個體。但

每個執行個體綁定的IP位址是不能相同的。在有多塊網卡或用IP Alias技術的機器可

以測試這種情況。

SO_REUSEADDR允許單個程序綁定相同的端口到多個socket上,但每個soc

ket綁定的ip位址不同。這和2很相似,差別請看UNPv1。

SO_REUSEADDR允許完全相同的位址和端口的重複綁定。但這隻用于UDP的

多點傳播,不用于TCP。

而目前的情況并不屬于這4種的任何一種。那麼這時,假設程序A同時打開了5個socket,并都成功bind了本地位址和相同的端口,且發送了請求給程序B。那麼當程序B回應時,是什麼樣的情形呢?

按照第一感覺,似乎kernel應該把資料包發給每一個socket,也就是說如果這5個socket沒有關閉,每個socket都應該收到5個資料包。——至少以前的我,會這麼了解。

可現實往往與人們的第一感覺向左的。linux kernel在收到資料包,除了raw socket,隻會選擇一個最比對的socket來接收資料包。以UDP為例,執行這一個工作的是__udp4_lib_lookup_skb函數。通過比對源位址,源端口,目的位址,目的端口以及bind的網卡,來找到最比對的socket。使用compute_score這個函數來計算每個socket的分數,具體行為請參考kernel代碼。當沒有完全比對的時候,才會比對具有通配位址或端口的socket。

正因為linux kernel的這一行為,導緻了這5個socket中,隻有1個socket收到了所有的5個資料包。而這個socket隻關心其中的一個并成功處理而後關閉socket。而其他4個socket則根本收不到任何資料包。這時這4個socket重發請求,而4個回複仍然隻會被其中的1個socket收到,而導緻另外3個socket失敗。這裡之是以每次都是1個socket收到所有的資料包,是因為每個socket都bind相同的位址和端口,那麼對于kernel來說,這些socket的比對得分肯定都相等。那麼每次資料包到來,kernel都隻會選擇第一個socket。

通過上面的講解,就可以确定了正是對SO_REUSEADDR的濫用,才導緻了這個bug。那麼,究竟怎樣解決這個bug呢。我想出了2個方案:

1. 在程序A中,建立一個新的線程,專門用于向程序B發送請求和接受回複,來代替目前的機制。不過這樣的改動會比較大些。

2. 利用linux中raw socket的特性——linux會複制所有符合raw socket過濾條件(使用bind和connect)的資料包到每一個raw socket,替換現有的UDP socket。這樣每個socket都可以收到所有的回複,當然隻會處理對應該socket發送出的請求的回複。這樣的改動最簡單,不過在效率上稍微差了一點。

根據我們的情況,因為通過程序A來申請程序B資源的客戶請求并不經常使用。所有,最終,我選擇使用raw socket這個方案來解決這個Bug。

這次我能夠很快的發現這個根本原因。主要是因為這段時間我正在學些linux的TCP/IP的源代碼。是以在看到程序A使用SO_REUSEADDR的情況時,直接想起了kernel挑選socket的最佳比對政策。是以,直接找到了根本原因。可見,即使我們并不是kernel 開發人員,也要盡量的多了解kernel的内部原理和機制,對于應用層的開發有着極大的益處——如果我不知道這個匹

配政策,估計這個bug需要我研究好幾天,而非今天的迅速解決。

另外從這個bug中,我們也應該得到教訓。切莫濫用SO_REUSEADDR,隻能在正确的情況下使用!不然的話,等着bug吧

繼續閱讀