操做系統思考 第六章 內存管理

第六章 內存管理

做者:Allen B. Downeyhtml

原文:Chapter 6 Memory managementgit

譯者:飛龍程序員

協議:CC BY-NC-SA 4.0github

C提供了4種用於動態內存分配的函數:數組

  • malloc,它接受表示字節單位的大小的整數,返回指向新分配的、(至少)爲指定大小的內存塊的指針。若是不能知足要求,它會返回特殊的值爲NULL的指針。瀏覽器

  • calloc,它和malloc同樣,除了它會清空新分配的空間。也就是說,它會設置塊中全部字節爲0。緩存

  • free,它接受指向以前分配的內存塊的指針,並會釋放它。也就是說,使這塊空間可用於將來的分配。安全

  • realloc,它接受指向以前分配的內存塊的指針,和一個新的大小。它使用新的大小來分配內存塊,將舊內存塊中的數據複製到新內存塊中,釋放舊內存塊,並返回指向新內存塊的指針。數據結構

這套API是出了名的易錯和苛刻。內存管理是設計大型系統中,最具備挑戰性的一部分,它正是許多現代語言提供高階內存管理特性,例如垃圾回收的緣由。函數

6.1 內存錯誤

C的內存管理API有點像Jasper Beardly,動畫片《辛普森一家》中的一個配角,他是一個嚴厲的代課老師,喜歡體罰別人,並使用戒尺懲罰任何違規行爲。

下面是一些應受到懲罰的程序行爲:

  • 若是你訪問任何沒有分配的內存塊,就應受到懲罰。

  • 若是你釋放了某個內存塊以後再訪問它,就應受到懲罰。

  • 若是你嘗試釋放一個沒有分配的內存塊,就應受到懲罰。

  • 若是你釋放屢次相同的內存塊,就應受到懲罰。

  • 若是你使用沒有分配或者已經釋放的內存塊調用realloc,就應受到懲罰。

這些規則聽起來好像不難遵循,可是在一個大型程序中,一塊內存可能由程序一部分分配,在另外一個部分中使用,以後在其餘部分中釋放。因此一部分中的變化也須要其它部分跟着變化。

同時,同一個內存塊在程序的不一樣部分中,也可能有許多別名或者引用。這些內存塊在全部引用再也不使用時,才應該被釋放。正確處理這件事情一般須要細心的分析程序的全部部分,這很是困難,而且與良好的軟件工程的基本原則相違背。

理論上,每一個分配內存的函數都應包含內存如何釋放的信息,做爲接口文檔的一部分。成熟的庫一般作得很好,可是實際上,軟件工程的實踐一般不是這樣理想化的。

內存錯誤很是難以發現,由於這些症狀是不可預測的,這使得事情更加糟糕,例如:

  • 若是從未分配的內存塊中讀取值,系統可能會檢測到錯誤,觸發叫作「段錯誤」的運行時錯誤,而且停止程序。這個結果很是合理,由於它表示程序所讀取的位置會致使錯誤。可是,遺憾的是,這種結果很是少見。更一般的是,程序讀取了未分配的內存塊,而沒有檢測到錯誤,程序所讀取的未分配內存正好儲存在一塊特定區域中。若是這個值沒有解釋爲正確的類型,結果可能會難以解釋。例如,若是你讀取字符串中的字節,將它們解釋爲浮點數,你可能會獲得一個無效的數值,很是大或很是小的數值。若是你向函數傳遞它沒法處理的值,結果會很是怪異。

  • 若是你向未分配的內存塊中寫入值,會更加糟糕。由於在值被寫入以後,須要很長時間值才能被讀取而且發生錯誤。此時尋找問題來源就會很是困難。事情還可能更加糟糕!C風格內存管理的一個最廣泛的問題是,用於實現mallocfree的數據結構(咱們將會看到)一般和分配的內存塊儲存在一塊兒。因此若是你無心中越過動態分配塊的末尾寫入值,你就可能破壞了這些數據結構。系統一般直到最後纔會檢測到這種問題,當你調用mallocfree時,這些函數會因爲一些謎之緣由調用失敗。

你應該從中總結出一條規律,就是安全的內存管理須要設計和規範。若是你編寫了一個分配內存的庫或模塊,你應該同時提供釋放它的接口,而且內存管理從開始就應該做爲API設計的一部分。

若是你使用了分配內存的庫,你應該按照規範使用API。例如,若是庫提供了分配和釋放儲存空間的函數,你應該一塊兒使用或都不使用它們。例如,不要在不是malloc分配的內存塊上調用free。你應該避免在程序的不一樣部分中持有相同內存塊的多個引用。

一般在安全的內存管理和性能之間有個權衡。例如,內存錯誤的的最廣泛來源是數組的越界寫入。這一問題的最顯然的解決方法就是邊界檢查。也就是說,每次對數組的訪問都應該檢查下標是否越界。提供數組結構的高階庫一般會進行邊界檢查。可是C風格數據和大多數底層庫不會這樣作。

6.2 內存泄漏

有一種可能會也可能不會受到懲罰的內存錯誤。若是你分配了一塊內存,而且沒有釋放它,就會產生「內存泄漏」。

對於一些程序,內存泄露是OK的。若是你的程序分配內存,對其執行計算,以後退出,這可能就不須要釋放內存。當程序退出時,全部分配的內存都會由操做系統釋放。在退出前當即釋放內存彷佛很負責任,可是一般很浪費時間。

可是若是一個程序運行了很長時間,而且泄露內存的話,它的內存總量會無限增加。此時會發生一些事情:

  • 某個時候,系統會耗完全部物理內存。在沒有虛擬內存的系統上,下一次的malloc調用會失敗,返回NULL

  • 在帶有虛擬內存的系統上,操做系統能夠將其它進程的頁面從內存移動到磁盤上,以後分配更多空間給泄露的進程。我會在7.8節解釋這一機制。

  • 單個進程可能有內存總量的限制,超過它的話,malloc會返回NULL

  • 最後,進程可能會用完它的虛擬地址空間(或者可用的部分)。以後,沒有更多的地址可分配,malloc會返回NULL

若是malloc返回了NULL,可是你仍舊把它當成分配的內存塊進行訪問,你會獲得段錯誤。所以,在使用以前檢查malloc的結果是個很好的習慣。一種選擇是在每一個malloc調用以後添加一個條件判斷,就像這樣:

void *p = malloc(size);
if (p == NULL) {
    perror("malloc failed");
    exit(-1);
}

perrorstdio.h中聲明,它會打印出關於最後發生的錯誤的錯誤信息和額外的信息。

exitstdlib.h中聲明,會使進程終止。它的參數是一個表示進程如何終止的狀態碼。按照慣例,狀態碼0表示一般終止,-1表示錯誤狀況。有時其它狀態碼用於表示不一樣的錯誤狀況。

錯誤檢查的代碼十分討厭,而且使程序難以閱讀。可是你能夠經過將庫函數的調用和錯誤檢查包裝在你本身的函數中,來解決這個問題。例如,下面是檢查返回值的malloc包裝:

void *check_malloc(int size)
{
  void *p = malloc (size);
  if (p == NULL) {
    perror("malloc failed");
    exit(-1);
  }
  return p;
}

因爲內存管理很是困難,多數大型程序,例如Web瀏覽器都會泄露內存。你可使用Unix的pstop工具來查看系統上的哪一個程序佔用了最多的內存。

6.3 實現

當進程啓動時,系統爲text段、靜態分配的數據、棧和堆分配空間,堆中含有動態分配的數據。

並非全部程序都動態分配數據,因此堆的大小可能很小,或者爲0。最開始堆只含有一個空閒塊。

malloc調用時,它會檢查這個空閒塊是否足夠大。若是不是,它會向系統請求更多內存。作這件事的函數叫作sbrk,它設置「程序中斷點」(program break),你能夠將其看作一個指向堆底部的指針。

譯者注:sbrk是Linux上的系統API,Windows上使用HeapAllocHeapFree來管理堆區。

sbrk調用時,它分配的新的物理內存頁,更新進程的頁表,並設置程序中斷點。

理論上,程序應該直接調用sbrk(而不是經過malloc),而且本身管理堆區。可是malloc易於使用,而且對於大多數內存使用模式,它運行速度快而且高效利用內存。

爲了實現內存管理API,多數Linux系統都使用ptmalloc,它基於dlmalloc,由Doug Lea編寫。一篇描述這個實現要素的論文可在http://gee.cs.oswego.edu/dl/html/malloc.html訪問。

對於程序員來講,須要注意的最重要的要素是:

  • malloc在運行時一般不依賴塊的大小,可是可能取決於空閒塊的數量。free一般很快,和空閒塊的數量無關。由於calloc會清空塊中的每一個字節,執行時間取決於塊的大小(以及空閒塊的數量)。realloc有時很快,若是新的大小比以前更小,或者空間可用於擴展示有的內存塊。不然,它須要從舊內存塊中複製數據到新內存塊,這種狀況下,執行時間取決於舊內存塊的大小。

  • 邊界標籤:當malloc分配一個快時,它在頭部和尾部添加空間來儲存塊的信息,包括它的大小和狀態(分配仍是釋放)。這些數據位叫作「邊界標籤」。使用這些標籤,malloc就能夠從任何塊移動到內存中上一個或下一個塊。此外,空閒塊會連接到一個雙向鏈表中,因此每一個空閒塊也包含指向「空閒鏈表」中下一個塊和上一個塊的指針。邊界標籤和空閒鏈表指針構成了malloc的內部數據結構。這些數據結構穿插在程序的數據中,因此程序錯誤很容易破壞它們。

  • 空間開銷:邊界標籤和空閒鏈表指針也佔據空間。最小的內存塊大小在大多數系統上是16字節。因此對於很是小的內存塊,malloc在空間上並不高效。若是你的程序須要大量的小型數據結構,將它們分配在數組中可能更高效一些。

  • 碎片:若是你以多種大小分配和釋放塊,堆區就會變得碎片化。也就是說,空閒空間會打碎成許多小型片斷。碎片很是浪費空間,它也會經過使緩存效率低下來下降程序的速度。

  • 裝箱和緩存:空閒鏈表在箱子中以大小排序,因此當malloc搜索特定大小的內存塊時,它知道應該在哪一個箱子中尋找。因此若是你釋放了一塊內存,以後當即以相同大小分配一塊內存,malloc一般會很快。

相關文章
相關標籤/搜索