周會娜,何 鑫,涂貴文,羅 實,梁鴻斌
(成都三零瑞通移動通信有限公司,四川 成都 610041)
近幾年,隨著國內(nèi)信息安全事件頻繁發(fā)生,加之相關國家對關鍵技術的封鎖,國家倡導并推動核心技術實現(xiàn)自主可控。在這樣的背景下,國產(chǎn)CPU的發(fā)展和應用得到了國家的高度重視[1-2]。目前,國產(chǎn)CPU 正處于快速追趕的關鍵階段,初步構建起了完整的產(chǎn)品線和上下游全產(chǎn)業(yè)鏈生態(tài)體系。在國家大力推進國產(chǎn)化替代工程的環(huán)境下,諸多項目開始從非國產(chǎn)化平臺移植到國產(chǎn)化平臺,期間不可避免出現(xiàn)了一些問題。本文結合某具體項目,針對一個國產(chǎn)CPU 弱一致性存儲模型引起的進程異常退出問題進行了深入分析,并給出了解決措施。
某國產(chǎn)服務器設備在穩(wěn)定運行1 年后出現(xiàn)應用軟件進程異常退出的故障,導致系統(tǒng)功能無法正常使用。
該設備為全國產(chǎn)化設備,基礎設施平臺中CPU、操作系統(tǒng)以及數(shù)據(jù)庫等均選取國內(nèi)廠家的型號產(chǎn)品。自主研發(fā)的應用軟件在原非國產(chǎn)化平臺運行穩(wěn)定可靠,在從非國產(chǎn)化平臺移植到國產(chǎn)平臺時,上述設備出現(xiàn)故障。該設備在使用時需要做到“多任務,高并發(fā),高可靠”,需要同在同一時間處理多任務、并發(fā)消息,具有高吞吐量和低延時,且7×24 h 無故障運行。
故障發(fā)生后,遠程登錄設備,查看到數(shù)據(jù)庫和操作系統(tǒng)運行正常,且在進程退出的同時生成了系統(tǒng)內(nèi)核文件(coredump)。通過gdb 工具將進程退出時運行狀態(tài)翻譯為調(diào)用棧,可以看出進程退出時處理線程使用關鍵字delete 來釋放內(nèi)存,最終調(diào)用free 時發(fā)生異常導致進程退出(abort)。
通過Linux 系統(tǒng)編程手冊[3]中free()函數(shù)的描述可以看出,釋放一塊未被分配的內(nèi)存會導致系統(tǒng)工作異常。通過上文對故障現(xiàn)場數(shù)據(jù)的分析可以判定,故障現(xiàn)場的進程退出是由于釋放一塊未被分配的內(nèi)存造成的,即定位問題的關鍵是找到“釋放一塊未被分配內(nèi)存”的原因。結合代碼實現(xiàn),整理故障分析樹如圖1 所示。
圖1 故障分析樹
故障出現(xiàn)時釋放的內(nèi)存是用于存放業(yè)務數(shù)據(jù)的,相關業(yè)務數(shù)據(jù)的流轉軌跡如圖2 所示。當發(fā)送線程接收完來源數(shù)據(jù)后,申請一塊內(nèi)存來構造業(yè)務數(shù)據(jù),然后通過消息隊列發(fā)送到處理線程;處理線程根據(jù)數(shù)據(jù)結構解析業(yè)務數(shù)據(jù)并進行分類處理,處理完成后釋放業(yè)務數(shù)據(jù)對應的內(nèi)存;當消息隊列滿時,發(fā)送線程需要釋放業(yè)務數(shù)據(jù)的內(nèi)存。
對于業(yè)務數(shù)據(jù)的流轉過程和代碼實現(xiàn),以下幾個環(huán)節(jié)構成問題分析的完整路徑:業(yè)務數(shù)據(jù)對應內(nèi)存的申請、釋放過程;業(yè)務數(shù)據(jù)對應的內(nèi)存指針的使用過程、業(yè)務數(shù)據(jù)對應的指針從內(nèi)存申請成功到被釋放之間是否被改動過,由此得出如圖1 所示的故障分析樹。針對該故障分析樹具體排查如下。
通過指針顯示使用內(nèi)存時,如果未能正確處理,往往會導致指針指向一塊未被分配的內(nèi)存,而釋放這個指針會導致當前問題的出現(xiàn)。未能正確處理有很多表現(xiàn)形式,常用的且與本項目相關的幾點包括內(nèi)存申請失敗的處理和直接使用指針時運算錯誤,導致使用未被分配的內(nèi)存,詳見X1、X2。
2.1.1 申請內(nèi)存失?。╔1)
從業(yè)務數(shù)據(jù)流轉軌跡圖(圖2)可以看到,在構造業(yè)務數(shù)據(jù)前有申請內(nèi)存的操作。如果申請內(nèi)存失敗,同時未做好失敗處理,可能導致非法使用內(nèi)存,從而釋放未被分配的內(nèi)存。此時,可以通過代碼走讀的方式遍歷所有申請內(nèi)存的位置,查看申請內(nèi)存失敗時是否有非法使用內(nèi)存的情況。該項目未發(fā)現(xiàn)申請內(nèi)存失敗時有非法使用內(nèi)存的情況。
2.1.2 指針偏移計算錯誤(X2)
在規(guī)劃編制過程中,無論是否開展規(guī)劃水資源論證,水資源都是規(guī)劃無法回避的問題。為避免脫離論證對象,規(guī)劃水資源論證的介入至少要在規(guī)劃方案 (包括推薦方案和替代方案)形成之后才能進行。在規(guī)劃方案形成階段介入,由于不確定性因素較多,往往只能定性評價,很難作出定量估算。在規(guī)劃方案形成和優(yōu)化階段介入,適合于綜合規(guī)劃和專項規(guī)劃中的指導性規(guī)劃;在規(guī)劃編制草案形成之后介入,適合于專項規(guī)劃,此時水資源論證的對象已經(jīng)明確,可以對水資源配置、水資源利用作出定量分析和比選,通過規(guī)劃水資源論證發(fā)現(xiàn)規(guī)劃中存在的問題,提出進一步調(diào)整和完善規(guī)劃的意見和建議。
從業(yè)務數(shù)據(jù)流轉軌跡圖(圖2)可以看出,有多個地方訪問內(nèi)存。如果通過指針運算來直接訪問內(nèi)存,在運算出錯時會導致非法使用內(nèi)存。此時,可以通過靜態(tài)代碼檢查工具(如Klockwork8)對程序代碼進行掃描或對代碼進行人工走查,著重檢查指針偏移量的情況、計算的準確性、字節(jié)對齊以及數(shù)據(jù)結構定義的一致性等,查看是否存在問題。該項目未發(fā)現(xiàn)上述問題。所以可以排除X2。
內(nèi)存申請、釋放過程中出現(xiàn)重復釋放,導致再次被釋放的內(nèi)存是未被分配的內(nèi)存。根據(jù)業(yè)務數(shù)據(jù)流轉軌跡圖(圖2)可見,可能重復釋放內(nèi)存的情況是發(fā)送線程將業(yè)務數(shù)據(jù)送入隊列時消息隊列已滿,拒絕將業(yè)務數(shù)據(jù)存入隊列,發(fā)送線程將該業(yè)務數(shù)據(jù)釋放,此時消息隊列仍將該業(yè)務數(shù)據(jù)復制了一份到處理線程,從而引起處理線程重復釋放內(nèi)存。針對上述懷疑,可以編寫一組測試程序來確認消息隊列滿時是否將已經(jīng)拒絕的業(yè)務數(shù)據(jù)復制一份到處理線程。結果顯示:在消息隊列滿的情況下,不會有復制的數(shù)據(jù)送給處理線程,故排除處理線程重復釋放業(yè)務數(shù)據(jù)內(nèi)存的情況。
業(yè)務數(shù)據(jù)對應內(nèi)存的指針在系統(tǒng)運行過程中被修改,根據(jù)業(yè)務數(shù)據(jù)流轉軌跡圖(圖2)需要分3個階段排查,分別為進入消息隊列前(X4)、在消息隊列中(X5)和出消息隊列后(X6)。
2.3.1 消息隊列前被修改(X4)
如果業(yè)務數(shù)據(jù)的內(nèi)存指針在進入消息隊列前被修改,那么修改后的指針很可能指向一塊未被分配的內(nèi)存。為了確認是否存在該問題,在原程序中添加調(diào)試代碼來跟蹤業(yè)務數(shù)據(jù)內(nèi)存指針的變化情況。調(diào)試代碼分別添加在申請內(nèi)存成功處和消息對列入口處。在測試環(huán)境中進行問題復現(xiàn),當問題出現(xiàn)時對比兩處的指針。結果顯示,指針未被修改,如圖3 所示,排除X4。
2.3.2 消息隊列中被修改(X5)
設備軟件采用自己構建的消息隊列模塊實現(xiàn)線程間通信。該模塊具有高吞吐量和低延時的特點,廣泛應用于通信系統(tǒng)的信令控制、媒體轉發(fā)以及業(yè)務調(diào)度等軟件中。圖4 描述了該消息隊列中關于內(nèi)存及指針的處理過程,即業(yè)務數(shù)據(jù)的指針被發(fā)送線程傳入消息隊列,消息隊列會把這個指針存儲到指針數(shù)組。當處理線程需要獲取業(yè)務數(shù)據(jù)時,消息隊列會把該業(yè)務數(shù)據(jù)指針返回給處理線程。
在消息隊列入口、寫指針數(shù)組以及消息隊列出口3 處增加調(diào)試代碼,記錄業(yè)務數(shù)據(jù)指針.問題出現(xiàn)時對比3 處記錄,以判斷指針是否被修改。
圖3 申請內(nèi)存與放入隊列前對比
實驗發(fā)現(xiàn):消息隊列入口處與寫指針數(shù)組處的指針相同;消息隊列出口處的指針與消息隊列入口處不同。根據(jù)實驗結果,問題定位需要聚焦在消息隊列出口處讀到的業(yè)務數(shù)據(jù)指針與寫指針數(shù)組時不同的原因。為了分析指針在隊列的傳輸中被改變的原因,在原代碼中增加調(diào)試代碼,當問題出現(xiàn)時觀測到的情況如圖5 所示。
圖4 消息隊列內(nèi)存及指針處理過程
圖5 消息隊列調(diào)試處理過程
②為了確認Data1 數(shù)據(jù)是否寫入指針I(yè)ndex62處,寫線程在數(shù)據(jù)寫入完成后,讀取指針I(yè)ndex62處的數(shù)據(jù)(標記為Data2),發(fā)現(xiàn)Data2=Data1,確認Data1 數(shù)據(jù)已經(jīng)寫入指針I(yè)ndex62 處。此時的時間點標記為T2。
③讀線程再次從消息隊列中讀取指針I(yè)ndex62處數(shù)據(jù)(標記為Data4),發(fā)現(xiàn)Data4=Data1。此時的時間點標記為T3。
綜上所述,讀到的錯誤指針是隊列中的“舊”值。再次讀取該地址可以讀到正確指針,證明指針讀取錯誤是由內(nèi)存刷新延遲造成的。
經(jīng)過與CPU 廠家技術人員共同分析,本項目中的內(nèi)存刷新延遲可能與國產(chǎn)CPU 架構弱一致性存儲模型[4]有關,同時給出了驗證方案,即增加內(nèi)存屏障[5],對比使用前后內(nèi)存刷新延遲的情況。內(nèi)存屏障用于保證操作有序,屏障之前的操作一定會先于內(nèi)存屏障之后的操作。
根據(jù)原程序的架構擬制了驗證程序,功能包括:3 個寫線程向隊列中寫入指針,寫入時打印指針地址;1 個線程從隊列中讀取指針,讀取到指針后打印指針地址。在該程序代碼中加入了內(nèi)存屏障,通過對比打開和關閉內(nèi)存屏障的結果,進行問題原因驗證。
①開啟內(nèi)存屏障,未出現(xiàn)內(nèi)存刷新延遲,程序運行正常;
②關閉內(nèi)存屏障,出現(xiàn)內(nèi)存刷新延遲。
據(jù)此證明內(nèi)存刷新延遲是由弱一致性存儲模型造成的。
2.3.3 消息隊列后被修改(X6)
如果業(yè)務數(shù)據(jù)的內(nèi)存指針在出消息隊列后被修改,修改后的指針很可能指向一塊未被分配的內(nèi)存。為了確認指針是否被修改,可以在原程序中添加調(diào)試代碼來跟蹤業(yè)務數(shù)據(jù)內(nèi)存指針的變化情況。在消息對列出口處和釋放內(nèi)存前記錄內(nèi)存指針,對比觀測結果,兩處指針相同,指針在出隊列后未被修改,排除X6。
通過以上故障樹分析和實驗結果得知,故障的根本原因是該國產(chǎn)CPU 采用的是弱一致性存儲模型,在與其適配過程中應用軟件未做內(nèi)存屏障,引起應用軟件讀到錯誤指針,致使一塊未被分配的內(nèi)存被釋放而應用軟件進程退出故障。
消息隊列模塊廣泛應用于通信系統(tǒng)的信令控制、媒體轉發(fā)以及業(yè)務調(diào)度等軟件中,在x86、ARM 和PowerPC 等架構處理器上穩(wěn)定運行多年,最大特點是高吞吐量和低延時。為了實現(xiàn)上述特點,在數(shù)據(jù)結構、指針數(shù)組管理和并發(fā)處理上做了很多優(yōu)化,如消息隊列的讀寫同步通過指針數(shù)組的計數(shù)變量來實現(xiàn),可以極大地減少系統(tǒng)開銷。但是,這個優(yōu)化在多核系統(tǒng)中對緩存的一致性有很大挑戰(zhàn)[6]。
本項目使用的國產(chǎn)CPU 芯片有4 個核,每個核包含獨有的指令緩存(I-Cache)、一級緩存(D-Cache)和二級緩存(V-Cache),4 個核之間通過交叉互聯(lián)網(wǎng)絡與三級緩存(S-Cache)相連,進而再通過另一個交叉互聯(lián)網(wǎng)絡與內(nèi)存相連。該國產(chǎn)CPU 采用GS464E 處理器核,實現(xiàn)的是弱一致性存儲模型。
該國產(chǎn)CPU 對弱一致性存儲模型的描述為“弱一致性存儲模型,即多條不相關的加載指令或存儲指令的返回結果的到達的先后次序跟處理器內(nèi)部數(shù)據(jù)通路的暢通性有關系,不一定按照發(fā)出的次序依次返回,這不影響訪存操作的正確性。如果程序具有顯式的因果關系,弱序一致性一定會尊重這種序關系,否則亂序有可能會打破原有的程序邏輯,就需要使用屏障來抑制亂序,以維持程序所期望的邏輯”。由此可知,弱一致性的影響與處理器內(nèi)部數(shù)據(jù)通路的暢通性有關。在CPU 內(nèi)部數(shù)據(jù)通路繁忙時,會概率性地出現(xiàn)無顯式因果關系的代碼被亂序執(zhí)行。
根據(jù)業(yè)務數(shù)據(jù)流轉軌跡圖(圖2),結合原程序中的相關代碼,在故障發(fā)生時,弱一致性存儲模型在多核系統(tǒng)中的讀寫行為如圖6 左側所示。
應用程序軟件寫線程利用Fifo[NextIn]=A 將業(yè)務數(shù)據(jù)指針(A)存入消息隊列,然后利用NextIn=(NextIn+1)通知讀線程取出業(yè)務數(shù)據(jù)指針。在CPU內(nèi)部數(shù)據(jù)通路繁忙時,弱一致性會概率性導致如下情況:讀線程先讀到已經(jīng)NextIn=(NextIn+1)的數(shù)據(jù),再通過Fifo[NextOut]讀取業(yè)務數(shù)據(jù)指針(A)的數(shù)據(jù),此時數(shù)據(jù)A 還未完成存儲,當前Fifo[NextOut]中的值還是上一輪存儲的“舊數(shù)據(jù)”。這個舊數(shù)據(jù)指向的是一塊未被分配的內(nèi)存,如果被處理線程釋放會引起“釋放一塊未被分配的內(nèi)存”錯誤。
內(nèi)存屏障用于保證操作有序,屏障之前的操作一定會先于內(nèi)存屏障之后的操作。大多數(shù)現(xiàn)代計算機為了提高性能而采取亂序執(zhí)行,使得內(nèi)存屏障成為必須。工程實現(xiàn)上,它經(jīng)常應用于對存儲時序有嚴格要求的場景。圖6 右側結合原程序中的相關代碼描述了加入內(nèi)存屏障后弱一致性存儲模型在多核系統(tǒng)中的讀寫行為。
圖6 弱一致性存儲模型在多核系統(tǒng)中的讀寫行為和加入內(nèi)存屏障后的讀寫行為
內(nèi)存屏障會保證寫線程將業(yè)務數(shù)據(jù)指針存入消息隊列指令(Fifo[NextIn]=A)先于通知讀線程指令(NextIn=(NextIn+1))執(zhí)行,從而保證讀線程能獲取到正確的業(yè)務數(shù)據(jù)指針進行處理。
在原應用程序中增加內(nèi)存屏障,針對整改后的應用軟件版本,采用故障復現(xiàn)時同樣的測試環(huán)境、測試數(shù)據(jù)和測試方法進行驗證。驗證包括“驗證測試”和“極限測試”兩部分。其中,“驗證測試”的目的是驗證在設計指標下程序是否穩(wěn)定運行;“極限測試”的目的是驗證在超過軟件處理能力的條件下程序是否穩(wěn)定運行。
通過實測驗證,修改措施有效,設備滿足設計指標要求,在超過軟件處理能力時設備可以正常運行,丟棄了處理不過來的數(shù)據(jù)。
本文對某項目在國產(chǎn)化替代移植過程中遇到的典型故障“進程異常退出”,采用故障樹分析法進行問題分析,最終定位故障的根本原因是國產(chǎn)CPU采用的是弱一致性存儲模型,在與其適配過程中應用軟件未做內(nèi)存屏障,引起應用軟件讀到錯誤指針,致使一塊未被分配的內(nèi)存被釋放,導致應用軟件進程退出。文章給出了添加內(nèi)存屏障的解決方法,經(jīng)實測驗證,解決措施有效,可以為解決同類問題提供參考。