一個驅動導致的內存泄漏問題的分析過程(meminfo->pmap->slabtop->alloc_calls)


關鍵詞:sqllite、meminfo、slabinfo、alloc_calls、nand、SUnreclaim等等。

 

下面記錄一個由於驅動導致的內存泄漏問題分析過程。

首先介紹問題背景,在一款嵌入式設備上,新使用sqllite庫進行數據庫操作,在操作數據(大量讀寫操作)一段時間之后,發生OOM現象。

然后OOM會選擇進程kill,即使系統中不剩什么進程,仍然內存緊張。

下面就介紹從上往下查找問題,然后在底層掐住RootCause,進而解決問題的分析過程。

1. 問題初步分析

首先懷疑的是sqllite庫問題,在PC進行同樣的測試未發現內存泄漏。在另一款參考設備上,進行同樣的測試,未發現內存泄漏。

以上測試確保了測試程序、sqllite庫等一致,僅交叉工具鏈和平台不一致。

結論:可以基本肯定sqllite庫以及測試程序沒有問題,可能的問題包括交叉工具鏈、平台問題。平台問題更大,所以問題集中到具體平台上進行分析。

疑問點:

1. 為何進程退出后,泄漏的內存沒有釋放?參見分析是因為SUnreclaim的slab內存不在進程內存統計范圍之內

2. 是否由於工具鏈不同導致庫函數表現不一致?參見分析1內存泄漏點在內核驅動中。 參見分析2經過在RAM上運行sqllite測試;單獨測試NAND文件系統,得出泄漏和sqllite無關

3. 是平台內核導致的泄漏嗎?參見分析確定泄漏在kmalloc-4096這個slab中

2. 具體平台查找內存泄漏方向

定位內存泄漏按照從大到小的思路,即首先看系統內存哪里泄漏,然后再看進程內存哪里泄漏,最后看哪種內存泄漏。

2.1 分析系統內存

首先通過cat /proc/meminfo,然后分析泄漏點。

從MemFree和MemAvailable看,內存將低了235M和228M。

然后看一下下面內存消耗在哪里?可以看出slab消耗了228M,再細節可以看出SUreclaim消耗了228M。基本確定

結論:確定由於Unreclaim類型的slab泄漏導致的內存泄漏。

疑問點:找出具體哪個slab泄漏了?參見分析在kmalloc-4096這個slab中。 哪個調用的slab申請?參見分析通過kmalloc-4096的alloc_calls可以知道調用點

2.2 分析進程內存

通過pmap -X -p `pidof xxx`來獲取進程的地址映射空間,可以分析進程內存細節。

結論:通過下面的對比,可以看出進程本身沒有導致內存泄漏。所以內存泄漏雖然有此進程導致,但是泄漏點不在進程中。

2.3 分析slab內存泄漏點

既然確定slab導致的泄漏,那么就需要使用slabtop、/proc/slabinfo以及/sys/kernel/slab來分析。

從下面可以看出泄漏點在kmalloc-4096這個slab,這個slab消耗的內存為250M左右。

然后就是去找slab的調用記錄,幸運的是系統在/sys/kernel/slab/*/中提供了alloc_calls和free_calls。

alloc_calls:
1 pidmap_init+0x4e/0x10c age=418000 pid=0 1 con_init+0xf6/0x21c age=418007 pid=0 1 pcpu_mem_zalloc+0x36/0x74 age=417641 pid=1 1 seq_buf_alloc+0x26/0x54 age=0 pid=349 1 register_leaf_sysctl_tables+0x74/0x1b0 age=418000 pid=0 1 ubifs_mount+0x68/0x15e8 age=416589 pid=1 1 ubifs_mount+0xdaa/0x15e8 age=416588 pid=1 1 nand_scan_tail+0xa2/0x6a8 age=417469 pid=1 1 ubi_attach_mtd_dev+0x9a/0xc38 age=417168 pid=1 1 sourcesink_bind+0x382/0x4c0 age=417588 pid=1 1 vid_dev_probe+0x32/0x1a0 age=417569 pid=1 2 hantrodec_probe+0x56/0x944 age=417502/417512/417523 pid=1 55898 spinand_cmdfunc+0x236/0x52c age=8997/225988/416514 pid=1-202 1 flow_cache_cpu_prepare.isra.7+0x3c/0x74 age=417889 pid=1
free_calls: 55840 <not-available> age=343019 pid=0 59 kvfree+0x2a/0x60 age=0/239003/415431 pid=140-349 1 ubifs_read_superblock+0x690/0xe14 age=416600 pid=1 8 kobject_uevent_env+0xda/0x580 age=416605/417396/417653 pid=1 3 uevent_show+0x5e/0xf4 age=409638/411302/413798 pid=143-150 1 skb_free_head+0x2c/0x6c age=417902 pid=1

結論:從alloc_calls可以看出spinand_cmdfunc中申請了55898次slab,和系統內存泄漏量基本一致。

2.4 分析驅動內存泄漏

通過objdump -S -l -D vmlinux > vmlinux.txt,然后結合反匯編代碼,找到spinand_cmdfunc+0x236可以找到具體點。

805829e2:       e3f26433        bsr             0x803cf248      // 803cf248 <_end+0xff997448>
805829e6:       c4004820        lsli            r0, r0, 0
spinand_cmdfunc():
/home/al/deepeye1000/linux/drivers/staging/mt29f_spinand/mt29f_spinand.c:806 spinand_program_page(info->spi, state->row, state->col,

所以問題最終指向了mt29f_spinand.c的806行,spinand_program_page()這個函數里面。

分析此驅動代碼,可以看出通過devm_kzalloc()申請的內存沒有被釋放的點。雖然此內存在模塊卸載的時候會被自動釋放,但是NAND驅動一般不會被卸載。

結論:確定泄漏點在spinand_program_page()中。

3. 旁證測試

基本上找到的問題點,為了驗證上述分析做了幾個簡單的測試。

3.1 在ramfs中進行sqllite數據庫操作

既然泄漏點在NAND驅動中,那么避開在NAND中進行讀寫操作即可。在/tmp目錄下,進行數據庫操作,作為對比測試。

同樣的軟件和平台下,運行同樣的業務。

結論:在ramfs中進行操作,沒有發生內存泄漏。說明泄漏sqllite操作無關。。

3.2 在NAND上進行文件cp、rm等操作

既然泄漏點在NAND,那么不使用數據庫讀寫,純文件系統讀寫如何呢?

經過測試,同樣發現SUreclaim內存增加導致的泄漏。

結論:內存泄漏跟NAND上文件操作有關。

為什么之前沒有發現內存泄漏問題呢?原來主要業務是運行應用,很少對NAND進行寫,即使有寫也是偶爾的,發現不了內存泄漏。這種情況在頻繁的讀寫、刪除操作下比較容易復現。

4. 解決問題

解決的思路就是確保在函數退出的時候,保證wbuf的內存能夠得到釋放。

diff --git a/drivers/staging/mt29f_spinand/mt29f_spinand.c b/drivers/staging/mt29f_spinand/mt29f_spinand.c
index 2474d88..7042934 100644
--- a/drivers/staging/mt29f_spinand/mt29f_spinand.c
+++ b/drivers/staging/mt29f_spinand/mt29f_spinand.c
@@ -495,7 +495,8 @@ static int spinand_program_page(struct spi_device *spi_nand,
 #ifdef CONFIG_MTD_SPINAND_ONDIEECC
     unsigned int i, j;
 
-    wbuf = devm_kzalloc(&spi_nand->dev, CACHE_BUF, GFP_KERNEL);
+    wbuf = kzalloc(CACHE_BUF, GFP_KERNEL);
     if (!wbuf)
         return -ENOMEM;
 
@@ -509,7 +510,7 @@ static int spinand_program_page(struct spi_device *spi_nand,
         retval = spinand_enable_ecc(spi_nand);
         if (retval < 0) {
             dev_err(&spi_nand->dev, "enable ecc failed!!\n");
-            return retval;
+            goto exit;
         }
     }
 #else...
-
-    return 0;
+exit:
+#ifdef CONFIG_MTD_SPINAND_ONDIEECC
+    kfree(wbuf);
+#endif
+    return retval;
 }

5. 驗證測試

基於以上的分析過程,構建測試用例:

1. 進行同樣的NAND上sqllite數據操作

2. 同樣重復cp、rm操作NAND上文件系統

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM