孫小偉,徐勁松,韓淑玲
(中興通訊 上海研發中心,上海201203)
內存泄漏指由于疏忽或錯誤造成程序未能釋放已經不再使用的內存。內存泄漏分為兩種情況:堆內存泄漏和系統內存泄漏。堆內存是指應用程序從堆中分配的、大小任意的(內存塊的大小可以在程序運行期決定)、使用完后必須顯式釋放的內存。本文所描述的方法只針對發生在虛擬操作系統里的堆內存泄漏。
在諸如通信設備這樣采用實時嵌入式操作系統(Real-Time Operating System,RTOS)[1]上 開 發 應 用 程序,通常會在RTOS上搭建虛擬操作系統。虛擬操作系統處于應用程序和RTOS之間,屏蔽了操作系統細節,提供了諸如內存管理、進程調度、定時器管理、消息分發、有限狀態機(Finite-tate machine,FSM)[2]等功能,以接口的形式向上層應用程序提供虛擬開發環境。虛擬操作系統相對于實時操作系統的位置可以參考圖1。
有了虛擬操作系統后,內存的申請和釋放都在虛擬操作系統里實現。應用程序不直接接觸內存,這樣內存泄漏只可能發生在虛擬操作系統里。虛擬操作系統有兩個重要功能:內存管理和進程管理。
(1)內存管理
系統上電初始化時,虛擬操作系統,就將所有內存一次性申請好并按大小組織成多個堆棧。為了防止系統運行引起的內存碎片,一般將內存空間劃分成多種大小,如16 B、32 B、64 B、128 B、256 B、512 B、1 KB、2 KB、4 KB、8 KB、16 KB,各種大小內存塊的數量可根據需要配置。預分配的每塊緩沖區為一整段,用selector表示。初始化時,將全部selector值按大小壓入各自的堆棧。當應用程序申請分配內存(GET_UB)時,虛擬操作系統從相應堆棧中彈出一個selector值,并返回給應用程序使用;當應用程序釋放內存(RET_UB)時,虛擬操作系統將要釋放內存的selector值壓入堆棧。
(2)進程管理
虛擬操作系統在嵌入式實時操作系統的基礎上虛擬了進程和進程調度的概念,在任務基礎上實現了二次調度。進程是一種擴展的有限狀態機,基本上處于等待消息狀態。當接收到一個消息時,進程作出響應,執行特定的動作。進程有3種調度狀態:運行狀態、就緒狀態、阻塞狀態。進程調度的狀態轉換圖如圖2所示。
進程被創建時,處于阻塞狀態。當收到消息時,進程調度將其放到所屬任務的就緒隊列中,進入就緒狀態,等待CPU資源,一旦得到CPU,就進入運行狀態。當進程無消息需要處理時,虛擬操作系統會掛起該進程,放到所屬任務的阻塞隊列中,并設置為阻塞狀態。一個進程始終在這3種狀態之間轉換。

圖1 虛擬操作系統相對于實時操作系統的位置圖

圖2 進程調度的狀態遷移圖
進程調度是在實時多任務操作系統的任務調度基礎上實現的。進程本身無顯式的優先級表示,但當與每個任務聯系起來時,就賦予了與任務等同的優先級,因而從上層應用來看,進程安排在不同優先級的任務中,就具有了不同的優先級。進程可以用一個進程控制塊(Process Control Block,PCB)的結構來標識,PCB保存了進程的所有運行狀態信息,如當前狀態、當前事件等。
傳統檢測內存泄漏的方法是截獲對分配內存和釋放內存函數的調用。截獲住這兩個函數,就能跟蹤每一塊內存的生命周期。比如,每當成功分配一塊內存后,就把它的指針加入一個全局的鏈表中;每當釋放一塊內存后,再把它的指針從鏈表中刪除。這樣,當程序結束的時候,鏈表中剩余的指針就指向那些沒有被釋放的內存,這是定位內存泄漏的一般方法。按照這種定位方法,可以在虛擬操作系統分配和釋放內存的地方對每一塊分配的內存進行跟蹤,但是只跟蹤泄漏的內存是不夠的,因為內存的內容是應用程序寫入的,沒有統一的結構,即使跟蹤到了也很難進行分析。
在虛擬操作系統中,要有效定位內存泄漏并對內存泄漏的源頭進行分析,就需要對泄漏現場和泄漏內存同步進行記錄,也就是對泄漏點的進程調度情況和泄漏內存的內容同時進行記錄。這樣,當發生內存泄漏后,根據記錄的泄漏現場,可以定位發生泄漏的進程和泄漏時的進程運行狀態。如果這不足以定位泄漏的原因,可以繼續分析泄漏內存中的內容來查明邏輯上的原因。因此,虛擬操作系統中定位內存泄漏的關鍵在于對泄漏現場(也就是對泄漏點虛擬操作系統進程調度現場)的記錄。
本文的核心思想是:先構建一個鏈表,當發生內存泄漏后,將泄漏點的系統狀態信息和泄漏的內存保存到鏈表中,并在系統崩潰前將鏈表存儲到存儲介質(如硬盤)中,事后對記錄的文件進行分析,通過對泄漏點的系統狀態以及泄漏內存的內容進行還原,來準確定位內存泄漏點。
通過下述步驟實現內存泄漏定位程序后,在虛擬操作系統中對內存進行分配和歸還的地方插入該定位程序。
①定義發生內存泄漏的標準,包括判斷內存發生泄漏的閾值以及記錄文件到硬盤的閾值,比如可用內存少于30%時認為發生了內存泄漏,當可用內存少于10%時開始保存鏈表到硬盤中。這樣,在系統正常情況下,本文描述的方法并不啟動,不會影響對實時性要求非常高的設備(諸如通信設備)的正常運行。
②定義一個結構UBLEAK_REG,用來保存泄漏點的系統運行狀態。該結構至少包括以下信息,主要是在泄漏點被調度進程的PCB的內容:調用分配內存函數(GET_UB)的行號、調用釋放內存函數(RET_UB)的文件名、內存塊(UB)的地址、當前任務號、當前進程名、當前進程在進程屬性表中的索引、當前進程的進程號、當前進程當前狀態、當前進程上一個狀態、當前進程當前事件、當前進程上一個事件、當前事件發送進程的進程號、當前進程的堆棧內容、當前申請UB的時間。
③針對每種大小的內存,分別定義一個鏈表UBLEAK_STACK,用來存儲結構UBLEAK_REG的內容。初始化時應依據各內存塊的總數和判斷泄漏的標準,來預分配鏈表的大小。對鏈表存儲空間的分配應該有個算法,使得不同大小內存占用的鏈表空間是可配置的,最簡單的情況就是平均分配給不同大小的內存塊。
④每次應用程序申請內存時,依據步驟①定義的標準判斷是否發生了內存泄漏,一旦認定發生了泄漏,將當前進程運行狀態信息填寫到一個UBLEAK_REG結構中,并將這個結構插入該內存對應的UBLEAK_STACK鏈表。記錄內存泄漏情況的鏈表結構圖如圖3所示。
⑤應用程序釋放內存時,同樣要判斷是否發生了內存泄漏,若未發生則不做任何處理,若已發生則需要從鏈表中找到該內存的相關信息記錄并刪除。這樣可以在系統發生內存泄漏時,將應用程序正常的內存申請和釋放信息排除,不占用寶貴的UBLEAK_STACK資源。
⑥如果內存泄漏達到系統崩潰邊緣(閾值可定義),則需要保存鏈表到存儲介質(如硬盤)中。在將鏈表保存到硬盤的過程中應該注意的是,保存的不只是泄漏點的系統狀態,還有內存塊的內容。保存動作如下:
將泄漏點現場信息寫到硬盤中;
根據泄漏內存的地址指針,保存對應的內容到硬盤中;
繼續記錄下一個泄漏點信息。

圖3 記錄內存泄漏情況的鏈表結構圖
⑦在內存泄漏導致系統崩潰后,可以從存儲介質中將記錄內存泄漏信息的文件拷貝出來進行分析。這時,首先需要編寫解析該文件的工具,因為存儲的文件是二進制格式,需要轉換成文本格式以便于閱讀,當然每條記錄的結構是已知的。接著對照文本文件的內容和UBLEAK_REG的結構,可復原泄漏點的進程狀態:對照UBLEAK_REG結構,根據文件名和行號可知進程哪一行發生泄漏;根據進程當前狀態和當前事件,可知進程泄漏時的狀態和導致泄漏的事件;根據當前事件發送進程號,可知是哪個進程在發消息并最終導致了泄漏;根據進程堆棧內容,對照收到消息的結構,可對當前消息的內容進行詳細分析,一般到這里泄漏點已經準確定位了。如果準確定位了內存泄漏點后,還不能定位內存泄漏的根本原因,則可對泄漏內存的內容進一步分析。分析方法是:在內存泄漏點的代碼中找到填入內存的數據結構,對照內存的內容,逐個字節進行比較以還原收到的消息內容。根據收到的消息內容和泄漏點處理代碼,可讀取該消息的處理過程,進一步查找泄漏原因。
本文描述了一種定位虛擬操作系統內存泄漏的方法,通過詳細記錄實時系統發生內存泄漏時內存申請的系統運行狀態,并在系統崩潰前將信息保存到諸如硬盤的媒體介質中,可以有效定位內存泄漏的原因。
[1]實時操作系統 [EB/OL].[2014-10].http://baike.baidu.com/view/18308.htm?fr=aladdin.
[2]有限狀態機 [EB/OL].[2014-10].http://baike.baidu.com/view/115336.htm.