孫小偉,徐勁松,韓淑玲
(中興通訊 上海研發(fā)中心,上海201203)
內(nèi)存泄漏指由于疏忽或錯誤造成程序未能釋放已經(jīng)不再使用的內(nèi)存。內(nèi)存泄漏分為兩種情況:堆內(nèi)存泄漏和系統(tǒng)內(nèi)存泄漏。堆內(nèi)存是指應(yīng)用程序從堆中分配的、大小任意的(內(nèi)存塊的大小可以在程序運(yùn)行期決定)、使用完后必須顯式釋放的內(nèi)存。本文所描述的方法只針對發(fā)生在虛擬操作系統(tǒng)里的堆內(nèi)存泄漏。
在諸如通信設(shè)備這樣采用實(shí)時嵌入式操作系統(tǒng)(Real-Time Operating System,RTOS)[1]上 開 發(fā) 應(yīng) 用 程序,通常會在RTOS上搭建虛擬操作系統(tǒng)。虛擬操作系統(tǒng)處于應(yīng)用程序和RTOS之間,屏蔽了操作系統(tǒng)細(xì)節(jié),提供了諸如內(nèi)存管理、進(jìn)程調(diào)度、定時器管理、消息分發(fā)、有限狀態(tài)機(jī)(Finite-tate machine,F(xiàn)SM)[2]等功能,以接口的形式向上層應(yīng)用程序提供虛擬開發(fā)環(huán)境。虛擬操作系統(tǒng)相對于實(shí)時操作系統(tǒng)的位置可以參考圖1。
有了虛擬操作系統(tǒng)后,內(nèi)存的申請和釋放都在虛擬操作系統(tǒng)里實(shí)現(xiàn)。應(yīng)用程序不直接接觸內(nèi)存,這樣內(nèi)存泄漏只可能發(fā)生在虛擬操作系統(tǒng)里。虛擬操作系統(tǒng)有兩個重要功能:內(nèi)存管理和進(jìn)程管理。
(1)內(nèi)存管理
系統(tǒng)上電初始化時,虛擬操作系統(tǒng),就將所有內(nèi)存一次性申請好并按大小組織成多個堆棧。為了防止系統(tǒng)運(yùn)行引起的內(nèi)存碎片,一般將內(nèi)存空間劃分成多種大小,如16 B、32 B、64 B、128 B、256 B、512 B、1 KB、2 KB、4 KB、8 KB、16 KB,各種大小內(nèi)存塊的數(shù)量可根據(jù)需要配置。預(yù)分配的每塊緩沖區(qū)為一整段,用selector表示。初始化時,將全部selector值按大小壓入各自的堆棧。當(dāng)應(yīng)用程序申請分配內(nèi)存(GET_UB)時,虛擬操作系統(tǒng)從相應(yīng)堆棧中彈出一個selector值,并返回給應(yīng)用程序使用;當(dāng)應(yīng)用程序釋放內(nèi)存(RET_UB)時,虛擬操作系統(tǒng)將要釋放內(nèi)存的selector值壓入堆棧。
(2)進(jìn)程管理
虛擬操作系統(tǒng)在嵌入式實(shí)時操作系統(tǒng)的基礎(chǔ)上虛擬了進(jìn)程和進(jìn)程調(diào)度的概念,在任務(wù)基礎(chǔ)上實(shí)現(xiàn)了二次調(diào)度。進(jìn)程是一種擴(kuò)展的有限狀態(tài)機(jī),基本上處于等待消息狀態(tài)。當(dāng)接收到一個消息時,進(jìn)程作出響應(yīng),執(zhí)行特定的動作。進(jìn)程有3種調(diào)度狀態(tài):運(yùn)行狀態(tài)、就緒狀態(tài)、阻塞狀態(tài)。進(jìn)程調(diào)度的狀態(tài)轉(zhuǎn)換圖如圖2所示。
進(jìn)程被創(chuàng)建時,處于阻塞狀態(tài)。當(dāng)收到消息時,進(jìn)程調(diào)度將其放到所屬任務(wù)的就緒隊(duì)列中,進(jìn)入就緒狀態(tài),等待CPU資源,一旦得到CPU,就進(jìn)入運(yùn)行狀態(tài)。當(dāng)進(jìn)程無消息需要處理時,虛擬操作系統(tǒng)會掛起該進(jìn)程,放到所屬任務(wù)的阻塞隊(duì)列中,并設(shè)置為阻塞狀態(tài)。一個進(jìn)程始終在這3種狀態(tài)之間轉(zhuǎn)換。
圖1 虛擬操作系統(tǒng)相對于實(shí)時操作系統(tǒng)的位置圖
圖2 進(jìn)程調(diào)度的狀態(tài)遷移圖
進(jìn)程調(diào)度是在實(shí)時多任務(wù)操作系統(tǒng)的任務(wù)調(diào)度基礎(chǔ)上實(shí)現(xiàn)的。進(jìn)程本身無顯式的優(yōu)先級表示,但當(dāng)與每個任務(wù)聯(lián)系起來時,就賦予了與任務(wù)等同的優(yōu)先級,因而從上層應(yīng)用來看,進(jìn)程安排在不同優(yōu)先級的任務(wù)中,就具有了不同的優(yōu)先級。進(jìn)程可以用一個進(jìn)程控制塊(Process Control Block,PCB)的結(jié)構(gòu)來標(biāo)識,PCB保存了進(jìn)程的所有運(yùn)行狀態(tài)信息,如當(dāng)前狀態(tài)、當(dāng)前事件等。
傳統(tǒng)檢測內(nèi)存泄漏的方法是截獲對分配內(nèi)存和釋放內(nèi)存函數(shù)的調(diào)用。截獲住這兩個函數(shù),就能跟蹤每一塊內(nèi)存的生命周期。比如,每當(dāng)成功分配一塊內(nèi)存后,就把它的指針加入一個全局的鏈表中;每當(dāng)釋放一塊內(nèi)存后,再把它的指針從鏈表中刪除。這樣,當(dāng)程序結(jié)束的時候,鏈表中剩余的指針就指向那些沒有被釋放的內(nèi)存,這是定位內(nèi)存泄漏的一般方法。按照這種定位方法,可以在虛擬操作系統(tǒng)分配和釋放內(nèi)存的地方對每一塊分配的內(nèi)存進(jìn)行跟蹤,但是只跟蹤泄漏的內(nèi)存是不夠的,因?yàn)閮?nèi)存的內(nèi)容是應(yīng)用程序?qū)懭氲?,沒有統(tǒng)一的結(jié)構(gòu),即使跟蹤到了也很難進(jìn)行分析。
在虛擬操作系統(tǒng)中,要有效定位內(nèi)存泄漏并對內(nèi)存泄漏的源頭進(jìn)行分析,就需要對泄漏現(xiàn)場和泄漏內(nèi)存同步進(jìn)行記錄,也就是對泄漏點(diǎn)的進(jìn)程調(diào)度情況和泄漏內(nèi)存的內(nèi)容同時進(jìn)行記錄。這樣,當(dāng)發(fā)生內(nèi)存泄漏后,根據(jù)記錄的泄漏現(xiàn)場,可以定位發(fā)生泄漏的進(jìn)程和泄漏時的進(jìn)程運(yùn)行狀態(tài)。如果這不足以定位泄漏的原因,可以繼續(xù)分析泄漏內(nèi)存中的內(nèi)容來查明邏輯上的原因。因此,虛擬操作系統(tǒng)中定位內(nèi)存泄漏的關(guān)鍵在于對泄漏現(xiàn)場(也就是對泄漏點(diǎn)虛擬操作系統(tǒng)進(jìn)程調(diào)度現(xiàn)場)的記錄。
本文的核心思想是:先構(gòu)建一個鏈表,當(dāng)發(fā)生內(nèi)存泄漏后,將泄漏點(diǎn)的系統(tǒng)狀態(tài)信息和泄漏的內(nèi)存保存到鏈表中,并在系統(tǒng)崩潰前將鏈表存儲到存儲介質(zhì)(如硬盤)中,事后對記錄的文件進(jìn)行分析,通過對泄漏點(diǎn)的系統(tǒng)狀態(tài)以及泄漏內(nèi)存的內(nèi)容進(jìn)行還原,來準(zhǔn)確定位內(nèi)存泄漏點(diǎn)。
通過下述步驟實(shí)現(xiàn)內(nèi)存泄漏定位程序后,在虛擬操作系統(tǒng)中對內(nèi)存進(jìn)行分配和歸還的地方插入該定位程序。
①定義發(fā)生內(nèi)存泄漏的標(biāo)準(zhǔn),包括判斷內(nèi)存發(fā)生泄漏的閾值以及記錄文件到硬盤的閾值,比如可用內(nèi)存少于30%時認(rèn)為發(fā)生了內(nèi)存泄漏,當(dāng)可用內(nèi)存少于10%時開始保存鏈表到硬盤中。這樣,在系統(tǒng)正常情況下,本文描述的方法并不啟動,不會影響對實(shí)時性要求非常高的設(shè)備(諸如通信設(shè)備)的正常運(yùn)行。
②定義一個結(jié)構(gòu)UBLEAK_REG,用來保存泄漏點(diǎn)的系統(tǒng)運(yùn)行狀態(tài)。該結(jié)構(gòu)至少包括以下信息,主要是在泄漏點(diǎn)被調(diào)度進(jìn)程的PCB的內(nèi)容:調(diào)用分配內(nèi)存函數(shù)(GET_UB)的行號、調(diào)用釋放內(nèi)存函數(shù)(RET_UB)的文件名、內(nèi)存塊(UB)的地址、當(dāng)前任務(wù)號、當(dāng)前進(jìn)程名、當(dāng)前進(jìn)程在進(jìn)程屬性表中的索引、當(dāng)前進(jìn)程的進(jìn)程號、當(dāng)前進(jìn)程當(dāng)前狀態(tài)、當(dāng)前進(jìn)程上一個狀態(tài)、當(dāng)前進(jìn)程當(dāng)前事件、當(dāng)前進(jìn)程上一個事件、當(dāng)前事件發(fā)送進(jìn)程的進(jìn)程號、當(dāng)前進(jìn)程的堆棧內(nèi)容、當(dāng)前申請UB的時間。
③針對每種大小的內(nèi)存,分別定義一個鏈表UBLEAK_STACK,用來存儲結(jié)構(gòu)UBLEAK_REG的內(nèi)容。初始化時應(yīng)依據(jù)各內(nèi)存塊的總數(shù)和判斷泄漏的標(biāo)準(zhǔn),來預(yù)分配鏈表的大小。對鏈表存儲空間的分配應(yīng)該有個算法,使得不同大小內(nèi)存占用的鏈表空間是可配置的,最簡單的情況就是平均分配給不同大小的內(nèi)存塊。
④每次應(yīng)用程序申請內(nèi)存時,依據(jù)步驟①定義的標(biāo)準(zhǔn)判斷是否發(fā)生了內(nèi)存泄漏,一旦認(rèn)定發(fā)生了泄漏,將當(dāng)前進(jìn)程運(yùn)行狀態(tài)信息填寫到一個UBLEAK_REG結(jié)構(gòu)中,并將這個結(jié)構(gòu)插入該內(nèi)存對應(yīng)的UBLEAK_STACK鏈表。記錄內(nèi)存泄漏情況的鏈表結(jié)構(gòu)圖如圖3所示。
⑤應(yīng)用程序釋放內(nèi)存時,同樣要判斷是否發(fā)生了內(nèi)存泄漏,若未發(fā)生則不做任何處理,若已發(fā)生則需要從鏈表中找到該內(nèi)存的相關(guān)信息記錄并刪除。這樣可以在系統(tǒng)發(fā)生內(nèi)存泄漏時,將應(yīng)用程序正常的內(nèi)存申請和釋放信息排除,不占用寶貴的UBLEAK_STACK資源。
⑥如果內(nèi)存泄漏達(dá)到系統(tǒng)崩潰邊緣(閾值可定義),則需要保存鏈表到存儲介質(zhì)(如硬盤)中。在將鏈表保存到硬盤的過程中應(yīng)該注意的是,保存的不只是泄漏點(diǎn)的系統(tǒng)狀態(tài),還有內(nèi)存塊的內(nèi)容。保存動作如下:
將泄漏點(diǎn)現(xiàn)場信息寫到硬盤中;
根據(jù)泄漏內(nèi)存的地址指針,保存對應(yīng)的內(nèi)容到硬盤中;
繼續(xù)記錄下一個泄漏點(diǎn)信息。
圖3 記錄內(nèi)存泄漏情況的鏈表結(jié)構(gòu)圖
⑦在內(nèi)存泄漏導(dǎo)致系統(tǒng)崩潰后,可以從存儲介質(zhì)中將記錄內(nèi)存泄漏信息的文件拷貝出來進(jìn)行分析。這時,首先需要編寫解析該文件的工具,因?yàn)榇鎯Φ奈募嵌M(jìn)制格式,需要轉(zhuǎn)換成文本格式以便于閱讀,當(dāng)然每條記錄的結(jié)構(gòu)是已知的。接著對照文本文件的內(nèi)容和UBLEAK_REG的結(jié)構(gòu),可復(fù)原泄漏點(diǎn)的進(jìn)程狀態(tài):對照UBLEAK_REG結(jié)構(gòu),根據(jù)文件名和行號可知進(jìn)程哪一行發(fā)生泄漏;根據(jù)進(jìn)程當(dāng)前狀態(tài)和當(dāng)前事件,可知進(jìn)程泄漏時的狀態(tài)和導(dǎo)致泄漏的事件;根據(jù)當(dāng)前事件發(fā)送進(jìn)程號,可知是哪個進(jìn)程在發(fā)消息并最終導(dǎo)致了泄漏;根據(jù)進(jìn)程堆棧內(nèi)容,對照收到消息的結(jié)構(gòu),可對當(dāng)前消息的內(nèi)容進(jìn)行詳細(xì)分析,一般到這里泄漏點(diǎn)已經(jīng)準(zhǔn)確定位了。如果準(zhǔn)確定位了內(nèi)存泄漏點(diǎn)后,還不能定位內(nèi)存泄漏的根本原因,則可對泄漏內(nèi)存的內(nèi)容進(jìn)一步分析。分析方法是:在內(nèi)存泄漏點(diǎn)的代碼中找到填入內(nèi)存的數(shù)據(jù)結(jié)構(gòu),對照內(nèi)存的內(nèi)容,逐個字節(jié)進(jìn)行比較以還原收到的消息內(nèi)容。根據(jù)收到的消息內(nèi)容和泄漏點(diǎn)處理代碼,可讀取該消息的處理過程,進(jìn)一步查找泄漏原因。
本文描述了一種定位虛擬操作系統(tǒng)內(nèi)存泄漏的方法,通過詳細(xì)記錄實(shí)時系統(tǒng)發(fā)生內(nèi)存泄漏時內(nèi)存申請的系統(tǒng)運(yùn)行狀態(tài),并在系統(tǒng)崩潰前將信息保存到諸如硬盤的媒體介質(zhì)中,可以有效定位內(nèi)存泄漏的原因。
[1]實(shí)時操作系統(tǒng) [EB/OL].[2014-10].http://baike.baidu.com/view/18308.htm?fr=aladdin.
[2]有限狀態(tài)機(jī) [EB/OL].[2014-10].http://baike.baidu.com/view/115336.htm.