何愷
(中國石化石油物探技術(shù)研究院,南京 211100)
在嵌入式統(tǒng)一操作系統(tǒng)平臺(tái)(OSS)進(jìn)行某程序移植中,出現(xiàn)了一個(gè)令人比較費(fèi)解的故障,基于OSS 運(yùn)行的程序在單板上長(zhǎng)時(shí)間運(yùn)行后,異常終止,并且顯示段錯(cuò)誤(segment fault)。
在項(xiàng)目研發(fā)中,考慮到OSS 在VxWorks 操作系統(tǒng)、pSOS 操作系統(tǒng)、嵌入式Linux 操作系統(tǒng)等主要使用的操作系統(tǒng)下面都通過了產(chǎn)品線的測(cè)試和試用,它的性能和穩(wěn)定性是很有說服力的。同時(shí)為了提高終端產(chǎn)品的競(jìng)爭(zhēng)力和在操作系統(tǒng)方面獲得更大的資源支持,該項(xiàng)目把程序移植到Linux 操作系統(tǒng)上(硬件方案基于MIPS 芯片),操作系統(tǒng)支撐方面采用了OSS 平臺(tái)。
該移植項(xiàng)目在最初的開發(fā)結(jié)束后在PC(CPU 為x86)上基于 Redhat Linux 9.0 和 Montavisa Linux 進(jìn)行了自測(cè)試,然后再移到基于ADI 提供的fusiv Linux 平臺(tái)的單板上進(jìn)行系統(tǒng)測(cè)試。
使用GDB 調(diào)試工具對(duì)問題進(jìn)行調(diào)試時(shí),無法對(duì)多線程進(jìn)行跟蹤。GDB 調(diào)試出現(xiàn)如下的情況:
此時(shí)繼續(xù)調(diào)試(continue),程序可以正常運(yùn)行,但不能跟蹤到線程中,使用info thread 命令也不能顯示進(jìn)程號(hào)、線程號(hào)等信息。
由于在Linux 操作系統(tǒng)方面的產(chǎn)品線開發(fā)經(jīng)驗(yàn)積累不多,同時(shí)通過網(wǎng)上的資料查閱,GDB 在單板上面調(diào)試POSIX 線程庫確實(shí)存在不少問題,而且基本沒有給出解答。所以在短期內(nèi)通過調(diào)試工具GDB 來解決該故障是不大現(xiàn)實(shí)的,必須尋找其他有效的解決方法,快速對(duì)故障進(jìn)行定位和解決。
為了定位故障,一開始采用了在程序中加打印進(jìn)行跟蹤的調(diào)試手段,大量的測(cè)試顯示段錯(cuò)誤的出現(xiàn)位置并不固定,只要程序運(yùn)行較長(zhǎng)時(shí)間后,就可能出現(xiàn)異常終止的段錯(cuò)誤??紤]到異常的出現(xiàn)時(shí)機(jī)比較隨機(jī)、出現(xiàn)位置也不固定,初步斷定是內(nèi)存泄漏引起的。
開發(fā)人員進(jìn)行程序開發(fā)的過程中使用動(dòng)態(tài)變量時(shí),需要認(rèn)真把控內(nèi)存管理問題,如果出現(xiàn)內(nèi)存分配使用之后沒有進(jìn)行回收或者因?yàn)槌绦蛟O(shè)計(jì)的漏洞導(dǎo)致沒有對(duì)分配的內(nèi)存進(jìn)行釋放就會(huì)出現(xiàn)內(nèi)存泄漏問題。
一般而言,內(nèi)存泄漏具有隱蔽性,它不屬于直接的過錯(cuò)型缺陷,比較難以檢測(cè)。如果內(nèi)存泄漏在程序中反復(fù)出現(xiàn),不斷積累,會(huì)降低系統(tǒng)整體性能,極端情況下可能使系統(tǒng)崩潰。
隨著應(yīng)用程序的設(shè)計(jì)和開發(fā)日益復(fù)雜,在程序?qū)崿F(xiàn)過程中處理的數(shù)據(jù)變量也大幅增加,程序中對(duì)變量的內(nèi)存分配、釋放調(diào)用非常頻繁,防止內(nèi)存泄漏的問題變得越來越突出,由于內(nèi)存泄漏問題的隱蔽性,給開發(fā)人員、測(cè)試人員帶來解決內(nèi)存泄漏類型缺陷的難題。
一般情況下,在內(nèi)存泄漏檢測(cè)中比較常用的算法本質(zhì)上大部分都屬于程序插裝。程序插裝是在保持被測(cè)程序邏輯結(jié)構(gòu)完整的基礎(chǔ)上,在程序的某些特定位置加入一段檢測(cè)程序,借此了解程序執(zhí)行中的動(dòng)態(tài)信息。它是軟件開發(fā)和測(cè)試中常用的一種基本方法,獲取程序的動(dòng)態(tài)信息是其最主要的目標(biāo),它在軟件開發(fā)和測(cè)試過程中處于非常重要的地位。程序插裝技術(shù)是一個(gè)聯(lián)系靜態(tài)分析與動(dòng)態(tài)信息收集的關(guān)鍵紐帶,其中插裝過程是靜態(tài)的,而信息收集過程是動(dòng)態(tài)的。
基于程序插裝的內(nèi)存泄漏監(jiān)測(cè)算法在實(shí)現(xiàn)時(shí)往往需要關(guān)注兩個(gè)關(guān)鍵問題,分別是:
(1)程序插裝的位置選擇,需要選擇適當(dāng)?shù)奈恢?,以便獲取關(guān)鍵階段信息。
(2)程序插裝的內(nèi)容,即需要收集哪些動(dòng)態(tài)信息。
插裝的關(guān)鍵在于能夠全面而無冗余地收集程序的動(dòng)態(tài)信息,以達(dá)到揭示程序內(nèi)部行為和特性的目的,需要做到緊湊精干,這一點(diǎn)對(duì)于實(shí)時(shí)嵌入式操作系統(tǒng)軟件來說非常重要。
在程序插裝內(nèi)容選擇方面,需要圍繞能動(dòng)態(tài)地、深入地跟蹤內(nèi)存的動(dòng)態(tài)分配情況這一目標(biāo)??杉尤胍恍┛梢愿?、定位內(nèi)存分配與釋放的信息,例如:申請(qǐng)內(nèi)存時(shí),所調(diào)用的函數(shù)名稱、空間的大小、申請(qǐng)和釋放時(shí)間、對(duì)應(yīng)程序的行號(hào)及操作系統(tǒng)所在模塊的位置信息等。
插裝位置的確定相對(duì)比較簡(jiǎn)單,需要在每個(gè)內(nèi)存塊申請(qǐng)的地方和內(nèi)存塊釋放的地方進(jìn)行插裝,主要是為了跟蹤內(nèi)存分配、釋放情況,這樣才能做到信息全面又不冗余,更好地服務(wù)內(nèi)存泄漏監(jiān)測(cè)。
MemWatch 是一個(gè)開放源代碼C 語言內(nèi)存錯(cuò)誤檢測(cè)工具。具體使用上比較簡(jiǎn)單,需要在代碼中添加一個(gè)頭文件,并且在Makefile 的gcc 語句中進(jìn)行Mem-Watch 宏的定義之后,就能進(jìn)行程序中內(nèi)存泄漏和錯(cuò)誤的跟蹤了。MEMWATCH 能檢測(cè)雙重釋放(doublefree)、錯(cuò)誤釋放(erroneous free)、沒有釋放的內(nèi)存(unfreed memory)、溢出和下溢等,并且支持ANSI C,提供結(jié)果日志紀(jì)錄,對(duì)于malloc/free 引起的內(nèi)存泄漏檢查十分有效的。
由于OSS 中采用了自J10 機(jī)操作系統(tǒng)支撐發(fā)展而來的內(nèi)存分區(qū)(UB 塊)管理,只是在UB 初始化時(shí)對(duì)事前定義好的內(nèi)存大小進(jìn)行一次malloc,在整個(gè)OSS 進(jìn)程kill 掉的時(shí)候進(jìn)行釋放。所以需要對(duì)OSS_GetUB、OSS_RetUB 進(jìn)行重新封裝。封裝如下:
這樣,對(duì)OSS_GetUB、OSS_RetUB 的調(diào)用就變?yōu)閷?duì)malloc、free 的調(diào)用。重新編譯運(yùn)行程序。MemWatch檢測(cè)出確實(shí)存在內(nèi)存泄漏,但是內(nèi)存泄漏行指到OSS_STATUS OSS_RetUB(VOID *pBuf)所在文件的free(pBuf)對(duì)應(yīng)的所在行。
看來準(zhǔn)確定位只有進(jìn)行全部OSS_GetUB/OSS_RetUB 和malloc/free 替換。在經(jīng)過替換之后,找到發(fā)生內(nèi)存泄漏的語句行,問題得以解決。
優(yōu)點(diǎn):開源,加入工程編譯后可以直接使用,對(duì)malloc/free 導(dǎo)致的內(nèi)存泄漏能夠有效檢測(cè)。
缺點(diǎn):對(duì)于普遍使用的內(nèi)存分區(qū)(UB 塊)管理代碼改動(dòng)太大,加大了調(diào)試的工作量。
內(nèi)存監(jiān)測(cè)算法一般需要跟蹤所有分配的內(nèi)存塊,在監(jiān)測(cè)條件被觸發(fā)時(shí)對(duì)內(nèi)存塊的相關(guān)信息進(jìn)行查詢,造成一定的系統(tǒng)開銷。常規(guī)方法在進(jìn)行監(jiān)測(cè)時(shí),往往會(huì)將所有內(nèi)存塊添加到鏈表結(jié)構(gòu)中,若內(nèi)存塊被回收則進(jìn)行脫鏈操作,監(jiān)測(cè)條件滿足則會(huì)收集內(nèi)存塊的大小、位置、泄漏節(jié)點(diǎn)等信息,這種方法因?yàn)樾枰芷谛缘谋闅v鏈表,有可能會(huì)導(dǎo)致系統(tǒng)開銷的不穩(wěn)定,極端時(shí)會(huì)影響系統(tǒng)平穩(wěn)運(yùn)行。
針對(duì)普遍使用的內(nèi)存分區(qū)(UB 塊)管理,王澤民等人提出了一種新的內(nèi)存泄漏檢測(cè)算法,既能保證內(nèi)存泄漏檢測(cè)的平穩(wěn)性,又可以發(fā)揮UB 分區(qū)管理的優(yōu)勢(shì)(避免內(nèi)存碎片、充分利用系統(tǒng)內(nèi)存資源)。
(1)算法
該算法為了避免由于引入內(nèi)存泄漏監(jiān)測(cè)而導(dǎo)致的CPU 占用時(shí)間變化劇烈,保持軟件系統(tǒng)的穩(wěn)定性,新的監(jiān)測(cè)算法設(shè)計(jì)中使用了單循環(huán)隊(duì)列的方法實(shí)現(xiàn)內(nèi)存泄漏監(jiān)測(cè)。該種算法雖然也是在時(shí)間上周期性的對(duì)內(nèi)存泄漏進(jìn)行監(jiān)測(cè)的,但是對(duì)于每一個(gè)內(nèi)存塊的掃描是均分到每個(gè)時(shí)鐘tick 里的。
在如何確定內(nèi)存泄漏監(jiān)測(cè)掃描周期方面,該算法有了新的嘗試。眾所周知,內(nèi)存塊的使用是有一定生命周期的,那么將應(yīng)用軟件中內(nèi)存塊的最大使用時(shí)間作為監(jiān)測(cè)時(shí)間周期會(huì)是一種好的方法,這樣可以避免頻繁的監(jiān)測(cè)操作,減少對(duì)系統(tǒng)資源的過多占用。當(dāng)然,內(nèi)存塊的生命周期受具體業(yè)務(wù)類型的影響,最終的時(shí)間值可以通過應(yīng)用層進(jìn)行設(shè)置。
在本調(diào)試過程中,使用的最大時(shí)間是30 秒。
構(gòu)建單循環(huán)隊(duì)列是該算法的關(guān)鍵,假定構(gòu)建的隊(duì)列長(zhǎng)度為L(zhǎng),若程序中需要分配的最大內(nèi)存塊個(gè)數(shù)為M,則設(shè)置M 個(gè)內(nèi)存控制描述數(shù)組。捕捉底層的時(shí)鐘中斷,每T 秒,單循環(huán)隊(duì)列的計(jì)時(shí)游標(biāo)向后走一位,查找該位所指示的隊(duì)列中是否存在到期的內(nèi)存塊。如存在則表明該內(nèi)存塊已經(jīng)超過最大有效使用時(shí)效TL,已經(jīng)出現(xiàn)內(nèi)存泄漏的情況,可以進(jìn)行相應(yīng)的補(bǔ)救操作或信息輸出。
對(duì)于TL>L 長(zhǎng)度的內(nèi)存塊,因已計(jì)算出內(nèi)存塊時(shí)長(zhǎng)TL 與L 相除所得的倍數(shù),每次循環(huán)計(jì)時(shí)隊(duì)列頭經(jīng)過時(shí),該倍數(shù)值得減去1,直至為0,在此之后的檢查就可判斷內(nèi)存泄漏并進(jìn)行插入操作。插入操作時(shí)插入在原隊(duì)列的頭部。
新內(nèi)存塊在循環(huán)隊(duì)列中的插入位置計(jì)算公式如下:
新內(nèi)存塊的插入位置=(當(dāng)前循環(huán)計(jì)時(shí)隊(duì)列頭位置+新內(nèi)存塊時(shí)長(zhǎng)與循環(huán)隊(duì)列長(zhǎng)度相除的余數(shù))與循環(huán)計(jì)時(shí)隊(duì)列長(zhǎng)度相除取余。
構(gòu)建的隊(duì)列長(zhǎng)度L 取值需要注意,若L 太小容易造成倍數(shù)值的減操作過多,影響效率,L 太大則浪費(fèi)存儲(chǔ)空間。L 的取值可以與M 的大小基本相同,以便當(dāng)使用的內(nèi)存塊接近M 個(gè)時(shí),內(nèi)存塊可以隨機(jī)均勻地分布到循環(huán)隊(duì)列的各數(shù)組元素中,使內(nèi)存塊負(fù)載比較均衡。同時(shí)為了避免在單循環(huán)隊(duì)列中引入繁瑣的隊(duì)列操作,可以適當(dāng)增大L 的值,避免TL >L 長(zhǎng)度的內(nèi)存塊。
(2)實(shí)現(xiàn)
在算法具體實(shí)現(xiàn)中,指針變量需要以循環(huán)的方式逐一指向各元素,形成的單循環(huán)隊(duì)列的每個(gè)節(jié)點(diǎn)又是一個(gè)單向的有序隊(duì)列。循環(huán)隊(duì)列的數(shù)據(jù)結(jié)構(gòu)需要進(jìn)行特定的設(shè)計(jì),定義的數(shù)據(jù)結(jié)構(gòu)中需要包含節(jié)點(diǎn)上監(jiān)控的內(nèi)存控制塊總個(gè)數(shù)、節(jié)點(diǎn)上第一個(gè)及最后一個(gè)內(nèi)存泄漏塊的ID 等信息。
內(nèi)存泄漏控制塊的數(shù)據(jù)結(jié)構(gòu)需要包含內(nèi)存泄漏控制塊在數(shù)組中的ID、循環(huán)隊(duì)列中前后元素的指針、內(nèi)存塊的起始地址、內(nèi)存塊的大小、申請(qǐng)行號(hào)、申請(qǐng)文件名等信息。在內(nèi)存塊申請(qǐng)時(shí),創(chuàng)建相應(yīng)的內(nèi)存泄漏控制塊結(jié)構(gòu),根據(jù)內(nèi)存泄漏監(jiān)測(cè)時(shí)間,將內(nèi)存泄漏控制塊結(jié)構(gòu)掛接到循環(huán)隊(duì)列上的對(duì)應(yīng)位置。在周期性掃描過程中,若發(fā)現(xiàn)被掃描的循環(huán)隊(duì)列的節(jié)點(diǎn)上有1 個(gè)以上內(nèi)存泄漏控制塊結(jié)構(gòu),則認(rèn)為這些內(nèi)存泄漏控制塊結(jié)構(gòu)對(duì)應(yīng)的UB 塊是超時(shí)未釋放的,存在內(nèi)存泄漏,根據(jù)設(shè)定輸出這些UB 塊相關(guān)信息。
在循環(huán)隊(duì)列及內(nèi)存泄漏控制塊的數(shù)據(jù)結(jié)構(gòu)定義完成之后,內(nèi)存泄漏監(jiān)測(cè)算法的實(shí)現(xiàn)過程可以劃分為資源初始化、內(nèi)存申請(qǐng)入鏈操作、內(nèi)存回收脫鏈操作及單循環(huán)隊(duì)列的掃描等步驟。內(nèi)存泄漏監(jiān)測(cè)相關(guān)資源初始化這一步驟,其主要目的是構(gòu)建單循環(huán)隊(duì)列,實(shí)現(xiàn)中需要時(shí)間源來對(duì)循環(huán)隊(duì)列進(jìn)行掃描,因此準(zhǔn)確的時(shí)間源信息獲取十分重要,在嵌入式Linux 系統(tǒng)中需要底層板級(jí)支撐包提供。在OSS 平臺(tái)中,本身實(shí)現(xiàn)的定時(shí)器管理采用了同樣的算法,所以對(duì)內(nèi)存泄漏提供時(shí)間源是很方便的。
總體而言,該算法的核心是:根據(jù)實(shí)際業(yè)務(wù)情況,為分配的UB 塊設(shè)定一個(gè)最長(zhǎng)使用時(shí)間,將該時(shí)間作為內(nèi)存泄漏監(jiān)測(cè)的時(shí)間周期。實(shí)現(xiàn)在內(nèi)存泄漏時(shí)間的周期內(nèi)不斷監(jiān)測(cè)系統(tǒng)中已分配UB 塊,如果超過了最長(zhǎng)使用時(shí)間,UB 塊依然沒有被釋放,則視為內(nèi)存泄漏,進(jìn)行警告并打印出相關(guān)信息。算法的重點(diǎn)是實(shí)現(xiàn)內(nèi)存泄漏循環(huán)隊(duì)列的周期性掃描,使用循環(huán)隊(duì)列游標(biāo)進(jìn)行相應(yīng)的標(biāo)識(shí),對(duì)每一個(gè)tick 進(jìn)行計(jì)數(shù)。同時(shí)為分配的UB 塊設(shè)置一個(gè)內(nèi)存控制單元,將其加入到單循環(huán)隊(duì)列,實(shí)現(xiàn)內(nèi)存泄漏跟蹤,上述的這些環(huán)節(jié)構(gòu)成了程序插裝的主要實(shí)現(xiàn)。若UB 塊正常釋放時(shí),則移除單循環(huán)隊(duì)列上的內(nèi)存泄漏控制塊,取消對(duì)應(yīng)UB 塊的內(nèi)存泄漏跟蹤。
通過把實(shí)現(xiàn)該種算法的實(shí)現(xiàn)文件ossmemleak.c、ossmemleak.h 加入工程,在系統(tǒng)初始化時(shí)加入內(nèi)存泄漏相關(guān)數(shù)據(jù)結(jié)構(gòu)的初始化。軟件模塊的內(nèi)存泄漏有效的被檢測(cè)出來,顯示出內(nèi)存泄漏時(shí)的函數(shù)、文件、行號(hào)、申請(qǐng)內(nèi)存大小、本次申請(qǐng)時(shí)間,問題得以解決。
優(yōu)點(diǎn):可以平穩(wěn)的對(duì)內(nèi)存泄漏進(jìn)行有效的檢測(cè),非常適合在OSS 平臺(tái)中使用。
缺點(diǎn):文件大小比MemWatch 大,同平臺(tái)相關(guān)性比較大。
雖然調(diào)試工具GDB 在單板上面調(diào)試多線程存在問題,但可以使用程序產(chǎn)生段錯(cuò)誤時(shí)的core dump 文件進(jìn)行堆?;厮?,通過GDB,找到產(chǎn)生段錯(cuò)誤的函數(shù)(其所屬的文件和行號(hào))。為此首先要解決的是如何使運(yùn)行在單板的程序在段錯(cuò)誤時(shí)產(chǎn)生core dump 文件。
在單板上運(yùn)行的程序的在段錯(cuò)誤時(shí)產(chǎn)生core dump 文件需要進(jìn)行如下的操作:
(1)使用ulimit 命令打開core dump 生成功能
#ulimit-c unlimited
(2)編譯應(yīng)用時(shí)加上-g 選項(xiàng),不要去掉符號(hào)表,最好是靜態(tài)編譯,這樣bt 堆?;厮輹r(shí)可以查看到函數(shù)的調(diào)用層次關(guān)系。同時(shí)因?yàn)閼?yīng)用程序、CORE、mips_gdb都很大,最好采用nfs 方式運(yùn)行程序。
(3)執(zhí)行應(yīng)用程序。當(dāng)程序出現(xiàn)segment fault 時(shí)候,生成core 文件,位于被測(cè)試程序運(yùn)行目錄。
如調(diào)試中的目錄:#/mnt/oss
(4)用gdb 查看core 文件,這樣可以得知程序異常退出時(shí)的錯(cuò)誤地方(位于哪個(gè)文件哪一行),用p 命令可以查看當(dāng)時(shí)的變量,bt 可以查看它們的堆棧情況,gdb 其他查看運(yùn)行狀態(tài)的命令都可以使用。
例如#/mnt/mips_gdb/mnt/oss core
根據(jù)coredump 文件進(jìn)行堆?;厮荩梢员容^好的解決諸如程序宕機(jī)、內(nèi)存泄漏、堆棧溢出等段錯(cuò)誤的問題。但需要指出的是對(duì)于內(nèi)存泄漏,它有時(shí)不能很好的定位到具體的程序異常處,例如A 處對(duì)指針重復(fù)操作,但core dump 顯示的異常位置卻是相關(guān)的另外一個(gè)地方B 的malloc 或者free 這樣的系統(tǒng)調(diào)用。
優(yōu)點(diǎn):通用性好,可以解決程序跑飛、程序異常終止等異常,對(duì)內(nèi)存泄漏檢測(cè)也有一定幫助作用。
缺點(diǎn):需要Linux 符號(hào)表的支持,有時(shí)不能準(zhǔn)確定位內(nèi)存泄漏所在行。
針對(duì)基于OSS 程序移植中出現(xiàn)的內(nèi)存泄漏問題,對(duì)問題調(diào)試、定位中使用的解決方法進(jìn)行了歸納、總結(jié)。這些方法不僅可以運(yùn)行在使用Linux 操作系統(tǒng)的內(nèi)存泄漏問題中,同時(shí)前兩種方法也可以在其他操作系統(tǒng)下面為解決同類問題參考、借鑒。
內(nèi)存泄漏問題,是開發(fā)過程中比較普遍的一個(gè)問題,往往會(huì)給測(cè)試、調(diào)試工作帶來很大的麻煩。通過這次項(xiàng)目的調(diào)試,認(rèn)為解決內(nèi)存泄漏問題,還是應(yīng)該從下面兩個(gè)方面入手:
(1)開發(fā)人員的編程風(fēng)格,當(dāng)然這個(gè)編程風(fēng)格在這里更多的不是指編程規(guī)范,多考慮一下代碼的擴(kuò)展性和移植性也是避免內(nèi)存泄漏的一個(gè)根本手段。
例如使用UB 塊管理
這樣的代碼在UB 塊下是可以正常運(yùn)行的,但是這樣的代碼缺乏可移植性,一旦被封裝成malloc/free,就是問題代碼,而且會(huì)給調(diào)試工作帶來大麻煩。
(2)改進(jìn)、完善內(nèi)存泄漏檢測(cè)、定位工具,做到快速定位,有效解決。本文歸納的三種針對(duì)嵌入式Linux 的內(nèi)存泄漏檢測(cè)、定位方法,通過實(shí)際的測(cè)試、調(diào)試工作,證明是有效的,可以被其他嵌入式Linux 開發(fā)借鑒以定位、解決內(nèi)存泄漏問題。