在 POSIX 線程編程中避免內存泄漏

https://www.ibm.com/developerworks/cn/linux/l-memory-leaks/

在 POSIX 線程編程中避免內存泄漏

Wei Dong Xie, IBM Systems Director 產品工程師, IBM

2010 年 9 月 27 日

檢測和避免 POSIX 線程內存泄漏的技巧

POSIX 線程(pthread)編程定義了一套標準的 C 編程語言類型、函數和常量 — 且 pthreads 提供了一種強大的線程管理工具。要充分使用 pthreads,您要避免常見錯誤。一個常見的錯誤就是忘記聯接可接合的線程,從而導致內存泄漏並增加工作量。在該篇技巧型文章中,學習 POSIX 線程基礎,瞭解如何識別和檢測線程內存泄漏,並獲得避免出現這種情況的可靠建議。

內容

POSIX 線程簡介

使用線程的主要原因是要提高程序性能。線程的創建和管理只需要較小的操作系統開銷和較少的系統資源。一個進程內的所有線程共享相同的地址空間,使得線程間的通信更高效,且比進程間通信更易於實現。例如,如果一個線程在等待一個輸入/輸出系統調用完成,其他線程可以處理 CPU 密集型任務。通過線程,可以優先調度重要任務 — 甚至中斷 — 低優先級任務。可將偶爾發生的任務放在定期調度的任務之間,創建調度靈活性。最後,pthreads 是在多 CPU 計算機上進行並行編程的理想之選。

而且使用 POSIX 線程或 pthreads 的主要原因更加簡單:作爲標準 C 語言線程編程接口的一部分,它們可高可移植的。

POSIX 線程編程有諸多優勢,但是如果您不明確一些基本規則,就有可能編寫一些難以調試的代碼並造成內存泄漏。我們首先回顧一下 POSIX 線程,分爲可接合線程分離線程

可接合線程

如果您希望生成一個新的線程,且需要知道它是如何終止的,那麼您需要一個可接合線程。對於可接合線程,系統分配專用存儲器來存儲線程終止狀態。線程終止後狀態得到更新。要獲得線程終止狀態,調用 pthread_join(pthread_t thread, void** value_ptr)

系統爲每個線程分配底層存儲,包括堆棧、線程 ID、線程終止狀態等。這個底層存儲將一直保留在進程空間(且不能回收),直至線程終止併爲其他線程所聯接。

分離線程

大多數時候,您只需創建一個線程,向其分配一些任務,然後繼續處理其他事務。在這些情況下,您不關注線程是如何終止的,這時使用分離線程是一個很好的選擇。

對於分離線程,在線程終止後系統自動回收其底層資源。

識別泄漏

如果您創建一個可接合的線程,但是忘記聯接它,其資源或私有內存一直保存在進程空間中,從未進行回收再利用。一定要聯接可接合的線程;否則,可能會引起嚴重的內存泄漏問題。

例如,Red Hat Enterprise Linux (RHEL4)上的一個線程需要一個 10MB 的堆棧,這意味着,如果不聯接它,會有至少 10MB 的內存泄漏。假設您設計一個管理器-工作線程模式的程序來處理傳入的請求。然後需要創建越來越多的工作線程來執行各個任務,最後終止這些線程。如果它們是可接合的線程,且您沒有調用 pthread_join() 來聯接它們,那麼在線程終止後,每個產生的線程都將泄漏大量的內存(至少每堆棧 10MB)。隨着創建並在未聯接的情況下終止的工作線程越來越多,泄漏的內存量也持續增加。另外,進程將無法創建新的線程,因爲無內存可供創建新線程使用。

清單 1 顯示在忘記聯接可接合線程時引發的嚴重內存泄漏。您還可以使用該代碼檢查可在一個進程空間中共存的線程體的最大量。

清單 1. 引發內存泄漏
#include<stdio.h>
#include<pthread.h>
void run() {
   pthread_exit(0);
}

int main () {
   pthread_t thread;
   int rc;
   long count = 0;
   while(1) {
      if(rc = pthread_create(&thread, 0, run, 0) ) {
         printf("ERROR, rc is %d, so far %ld threads created\n", rc, count);
         perror("Fail:");
         return -1;
      }
      count++;
   }
   return 0;
}

清單 1 中調用了 pthread_create() 來創建一個含默認線程屬性的新線程。默認情況下,新創建的線程是可接合的。它不斷創建新的可接合線程,直至有故障發生。然後輸出錯誤代碼和故障原因。

使用以下命令在 Red Hat Enterprise Linux Server 5.4 上編譯清單 1 中的代碼時: [root@server ~]# cc -lpthread thread.c -o thread, 您將獲得清單 2 所示的結果。

清單 2. 內存泄漏結果
[root@server ~]# ./thread
ERROR, rc is 12, so far 304 threads created
Fail:: Cannot allocate memory

在代碼創建了 304 個線程之後,它無法創建更多線程。錯誤代碼是 12,這表示無更多內存可用。

如清單 1 和清單 2 所示,雖然生成了可接合線程,但是卻未將其聯接,因此每個終止的可接合線程仍然佔用進程空間,泄漏進程內存。

RHEL 上的一個 POSIX 線程擁有一個大小爲 10MB 的私有堆棧。換言之,系統爲每個 pthread 分配至少 10MB 的專用存儲。在我們的示例中,304 個線程是在進程停止前創建的;這些線程佔用 304*10MB 內存,合計約 3GB。一個進程的虛擬內存的大小是 4GB,其中四分之一的進程空間是爲 Linux 內核預留的。這樣一來,就有 3GB 的內存空間可用作用戶空間。因此,3GB 內存由死線程消耗。這是很嚴重的內存泄漏。而且很容易理解它發生的速度爲何如此之快。

要修復泄漏,您可以添加代碼調用 pthread_join(),該方法可聯接每個可接合線程。

檢測泄漏

如同其他內存泄漏中一樣,進程啓動時問題可能沒那麼明顯。這裏介紹一種無需訪問源代碼便可檢測此類問題的方法:

  1. 計算進程中線程堆棧的數量。這包括正在運行的活動線程和已終止線程的數量。
  2. 計算進程中正在運行的活動線程的數量。
  3. 比較兩者。如果現有線程堆棧的數量大於正在運行的活動線程的數量,且在程序運行時這兩個數字的差量在不斷增加,那麼內存在泄漏。

這種內存泄漏很有可能是因未能聯接可接合線程而造成的。

使用 pmap 計算線程堆棧數

在一個運行的進程中,線程堆棧的數量等於進程中線程體的數量。線程體包括運行的活動線程和可接合的死線程。

pmap 是一種用於彙報進程內存的 Linux 工具。結合使用以下命令來獲取線程堆棧數:

[root@server ~]# pmap PID | grep 10240 | wc -l

(10240KB 是 Red Hat Enterprise Linux Server 5.4 上的默認堆棧大小。)

使用 /proc/PID/task 計算活動線程數

每次創建一個線程且該線程在運行時,會有一個條目填充到 /proc/PID/task 中。當線程終止時,不管該線程是可接合的還是分離的,都會將該條目從 /proc/PID/task 中刪除。因此活動線程數可通過運行以下命令得出:

[root@server ~]# ls /proc/PID/task | wc -l.

比較輸出

檢查 pmap PID | grep 10240 | wc -l 的輸出並將其與 ls /proc/PID/task | wc -l 的輸出進行比較。如果所有線程堆棧的數量大於活動線程的數量,且在程序運行時兩者的差量在持續增長,您可以確定內存泄漏問題確實存在。

預防泄漏

在編程過程中應當聯接可接合線程。如果您在程序中創建可接合的線程,切勿忘記調用 pthread_join(pthread_t, void**) 來回收分配給線程的專用存儲。否則,將引發嚴重的內存泄漏問題。

在編程後的測試階段,您可以使用 pmap/proc/PID/task 檢測這種泄漏是否存在。如果確實存在,檢查源代碼,看是否聯接了所有可接合線程。

就這些內容。只需少量預防工作即可爲您省掉大量後續工作,避免令人頭疼的內存泄漏問題。

參考資料

學習


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章