現代操作系統普遍採用虛擬記憶體管理(Virtual Memory Management)機制,這需要處理器中的MMU(Memory Management Unit,記憶體管理單元)提供支持。首先引入 PA 和 VA 兩個概念。 1.PA(Physical Address) 物理地址 如果處理器沒有MMU ...
現代操作系統普遍採用虛擬記憶體管理(Virtual Memory Management)機制,這需要處理器中的MMU(Memory Management Unit,記憶體管理單元)提供支持。首先引入 PA 和 VA 兩個概念。
1.PA(Physical Address)---物理地址
如果處理器沒有MMU,或者有MMU但沒有啟用,CPU執行單元發出的記憶體地址將直接傳到晶元引腳上,被記憶體晶元(以下稱為物理記憶體,以便與虛擬記憶體區分)接收,這稱為PA(Physical Address,以下簡稱PA),如下圖所示。
2.VA(Virtual Address)---虛擬地址
如果處理器啟用了MMU,CPU執行單元發出的記憶體地址將被MMU截獲,從CPU到MMU的地址稱為虛擬地址(Virtual Address,以下簡稱VA),而MMU將這個地址翻譯成另一個地址發到CPU晶元的外部地址引腳上,也就是將VA映射成PA,如下圖所示。
如果是32位處理器,則內地址匯流排是32位的,與CPU執行單元相連(圖中只是示意性地畫了4條地址線),而經過MMU轉換之後的外地址匯流排則不一定是32位的。也就是說,虛擬地址空間和物理地址空間是獨立的,32位處理器的虛擬地址空間是4GB,而物理地址空間既可以大於也可以小於4GB。
MMU將VA映射到PA是以頁(Page)為單位的,32位處理器的頁尺寸通常是4KB。例如,MMU可以通過一個映射項將VA的一頁0xb7001000~0xb7001fff映射到PA的一頁0x2000~0x2fff,如果CPU執行單元要訪問虛擬地址0xb7001008,則實際訪問到的物理地址是0x2008。物理記憶體中的頁稱為物理頁面或者頁幀(Page Frame)。虛擬記憶體的哪個頁面映射到物理記憶體的哪個頁幀是通過頁表(Page Table)來描述的,頁表保存在物理記憶體中,MMU會查找頁表來確定一個VA應該映射到什麼PA。
3. 進程地址空間
進程地址空間x86平臺的虛擬地址空間是0x0000 0000~0xffff ffff,大致上前3GB(0x0000 0000~0xbfff ffff)是用戶空間,後1GB(0xc000 0000~0xffff ffff)是內核空間。
Text Segmest 和 Data Segment
- Text Segment,包含.text段、.rodata段、.plt段等。是從/bin/bash載入到記憶體的,訪問許可權為r-x。
- Data Segment,包含.data段、.bss段等。也是從/bin/bash載入到記憶體的,訪問許可權為rw-。
堆和棧
- 堆(heap):堆說白了就是電腦記憶體中的剩餘空間,malloc函數動態分配記憶體是在這裡分配的。在動態分配記憶體時堆空間是可以向高地址增長的。堆空間的地址上限稱為Break,堆空間要向高地址增長就要抬高Break,映射新的虛擬記憶體頁面到物理記憶體,這是通過系統調用brk實現的,malloc函數也是調用brk向內核請求分配記憶體的。
- 棧(stack):棧是一個特定的記憶體區域,其中高地址的部分保存著進程的環境變數和命令行參數,低地址的部分保存函數棧幀,棧空間是向低地址增長的,但顯然沒有堆空間那麼大的可供增長的餘地,因為實際的應用程式動態分配大量記憶體的並不少見,但是有幾十層深的函數調用並且每層調用都有很多局部變數的非常少見。
如果寫程式的時候沒有註意好記憶體的分配問題,在堆和棧這兩個地方可能產生以下幾種問題:
- 記憶體泄露:如果你在一個函數里通過 malloc 在堆里申請了一塊空間,併在棧里聲明一個指針變數保存它,那麼當該函數結束時,該函數的成員變數將會被釋放,包括這個指針變數,那麼這塊空間也就找不回來了,也就無法得到釋放。久而久之,可能造成下麵的記憶體泄露問題。
- 棧溢出:如果你放太多數據到棧中(例如大型的結構體和數組),那麼就可能會造成“棧溢出”(Stack Overflow)問題,程式也將會終止。為了避免這個問題,在聲明這類變數時應使用 malloc 申請堆的空間。
- 野指針 和 段錯誤:如果一個指針所指向的空間已經被釋放,此時再試圖用該指針訪問已經被釋放了的空間將會造成“段錯誤”(Segment Fault)問題。此時指針已經變成野指針,應該及時手動將野指針置空。
4. 虛擬記憶體管理的作用
- 虛擬記憶體管理可以控制物理記憶體的訪問許可權。物理記憶體本身是不限制訪問的,任何地址都可以讀寫,而操作系統要求不同的頁面具有不同的訪問許可權,這是利用CPU模式和MMU的記憶體保護機制實現的。
- 虛擬記憶體管理最主要的作用是讓每個進程有獨立的地址空間。所謂獨立的地址空間是指,不同進程中的同一個VA被MMU映射到不同的PA,並且在某一個進程中訪問任何地址都不可能訪問到另外一個進程的數據,這樣使得任何一個進程由於執行錯誤指令或惡意代碼導致的非法記憶體訪問都不會意外改寫其它進程的數據,不會影響其它進程的運行,從而保證整個系統的穩定性。另一方面,每個進程都認為自己獨占整個虛擬地址空間,這樣鏈接器和載入器的實現會比較容易,不必考慮各進程的地址範圍是否衝突。
- VA到PA的映射會給分配和釋放記憶體帶來方便,物理地址不連續的幾塊記憶體可以映射成虛擬地址連續的一塊記憶體。比如要用malloc分配一塊很大的記憶體空間,雖然有足夠多的空閑物理記憶體,卻沒有足夠大的連續空閑記憶體,這時就可以分配多個不連續的物理頁面而映射到連續的虛擬地址範圍。
- 一個系統如果同時運行著很多進程,為各進程分配的記憶體之和可能會大於實際可用的物理記憶體,虛擬記憶體管理使得這種情況下各進程仍然能夠正常運行。因為各進程分配的只不過是虛擬記憶體的頁面,這些頁面的數據可以映射到物理頁面,也可以臨時保存到磁碟上而不占用物理頁面,在磁碟上臨時保存虛擬記憶體頁面的可能是一個磁碟分區,也可能是一個磁碟文件,稱為交換設備(Swap Device)。當物理記憶體不夠用時,將一些不常用的物理頁面中的數據臨時保存到交換設備,然後這個物理頁面就認為是空閑的了,可以重新分配給進程使用,這個過程稱為換出(Page out)。如果進程要用到被換出的頁面,就從交換設備再載入回物理記憶體,這稱為換入(Page in)。換出和換入操作統稱為換頁(Paging),因此: 系統中可分配的內存總量=物理內存的大小+交換設備的大小
如下圖所示。第一張圖是換出,將物理頁面中的數據保存到磁碟,並解除地址映射,釋放物理頁面。第二張圖是換入,從空閑的物理頁面中分配一個,將磁碟暫存的頁面載入回記憶體,並建立地址映射。
5.malloc 和 free
C標準庫函數malloc可以在堆空間動態分配記憶體,它的底層通過brk系統調用向操作系統申請記憶體。動態分配的記憶體用完之後可以用free釋放,更準確地說是歸還給malloc,這樣下次調用malloc時這塊記憶體可以再次被分配。
1 #include <stdlib.h> 2 void *malloc(size_t size); //返回值:成功返回所分配記憶體空間的首地址,出錯返回NULL 3 void free(void *ptr);
malloc的參數size表示要分配的位元組數,如果分配失敗(可能是由於系統記憶體耗盡)則返回NULL。由於malloc函數不知道用戶拿到這塊記憶體要存放什麼類型的數據,所以返回通用指針void *,用戶程式可以轉換成其它類型的指針再訪問這塊記憶體。malloc函數保證它返回的指針所指向的地址滿足系統的對齊要求,例如在32位平臺上返回的指針一定對齊到4位元組邊界,以保證用戶程式把它轉換成任何類型的指針都能用。
動態分配的記憶體用完之後可以用free釋放掉,傳給free的參數正是先前malloc返回的記憶體塊首地址。
示例
舉例如下:
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <string.h>
4 typedef struct {
5 int number;
6 char *msg;
7 } unit_t;
8 int main(void)
9 {
10 unit_t *p = malloc(sizeof(unit_t));
11 if (p == NULL) {
12 printf("out of memory\n");
13 exit(1);
14 }
15 p->number = 3;
16 p->msg = malloc(20);
17 strcpy(p->msg, "Hello world!");
18 printf("number: %d\nmsg: %s\n", p->number, p->msg);
19 free(p->msg);
20 free(p);
21 p = NULL;
22 return 0;
23 }
說明
unit_t *p = malloc(sizeof(unit_t));
這一句,等號右邊是void *
類型,等號左邊是unit_t *
類型,編譯器會做隱式類型轉換,我們講過void *
類型和任何指針類型之間可以相互隱式轉換。- 雖然記憶體耗儘是很不常見的錯誤,但寫程式要規範,malloc之後應該判斷是否成功。以後要學習的大部分系統函數都有成功的返回值和失敗的返回值,每次調用系統函數都應該判斷是否成功。
free(p);
之後,p所指的記憶體空間是歸還了,但是p的值並沒有變,因為從free的函數介面來看根本就沒法改變p的值,p現在指向的記憶體空間已經不屬於用戶,換句話說,p成了野指針,為避免出現野指針,我們應該在free(p);
之後手動置p = NULL;
。- 應該先
free(p->msg)
,再free(p)
。如果先free(p)
,p成了野指針,就不能再通過p->msg
訪問記憶體了。
6.記憶體泄漏
如果一個程式長年累月運行(例如網路伺服器程式),並且在迴圈或遞歸中調用malloc分配記憶體,則必須有free與之配對,分配一次就要釋放一次,否則每次迴圈都分配記憶體,分配完了又不釋放,就會慢慢耗盡系統記憶體,這種錯誤稱為記憶體泄漏(Memory Leak)。另外,malloc返回的指針一定要保存好,只有把它傳給free才能釋放這塊記憶體,如果這個指針丟失了,就沒有辦法free這塊記憶體了,也會造成記憶體泄漏。例如:
1 void foo(void) 2 { 3 char *p = malloc(10); 4 ... 5 }
foo函數返回時要釋放局部變數p的記憶體空間,它所指向的記憶體地址就丟失了,這10個位元組也就沒法釋放了。記憶體泄漏的Bug很難找到,因為它不會像訪問越界一樣導致程式運行錯誤,少量記憶體泄漏並不影響程式的正確運行,大量的記憶體泄漏會使系統記憶體緊缺,導致頻繁換頁,不僅影響當前進程,而且把整個系統都拖得很慢。
關於malloc和free還有一些特殊情況。malloc(0)這種調用也是合法的,也會返回一個非NULL的指針,這個指針也可以傳給free釋放,但是不能通過這個指針訪問記憶體。free(NULL)也是合法的,不做任何事情,但是free一個野指針是不合法的,例如先調用malloc返回一個指針p,然後連著調用兩次free(p);,則後一次調用會產生運行時錯誤。