虛擬內存管理【轉】
現代操作系統普遍采用虛擬內存管理(Virtual Memory Management)機制,這需要處理器中的MMU(Memory Management Unit,內存管理單元)提供支持。首先引入 PA 和 VA 兩個概念。
1.PA(Physical Address)---物理地址
如果處理器沒有MMU,或者有MMU但沒有啟用,CPU執行單元發出的內存地址將直接傳到芯片引腳上,被內存芯片(以下稱為物理內存,以便與虛擬內存區分)接收,這稱為PA(Physical Address,以下簡稱PA),如下圖所示。
![技術分享](http://hahack.com/images/c-memory/arch.pabox.png)
物理地址
2.VA(Virtual Address)---虛擬地址
如果處理器啟用了MMU,CPU執行單元發出的內存地址將被MMU截獲,從CPU到MMU的地址稱為虛擬地址(Virtual Address,以下簡稱VA),而MMU將這個地址翻譯成另一個地址發到CPU芯片的外部地址引腳上,也就是將VA映射成PA,如下圖所示。
![技術分享](http://hahack.com/images/c-memory/arch.vabox.png)
虛擬地址
如果是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. 進程地址空間
![技術分享](http://hahack.com/images/c-memory/link.addrspace.png)
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,並且在某一個進程中訪問任何地址都不可能訪問到另外一個進程的數據,這樣使得任何一個進程由於執行錯誤指令或惡意代碼導致的非法內存訪問都不會意外改寫其它進程的數據,不會影響其它進程的運行,從而保證整個系統的穩定性。另一方面,每個進程都認為自己獨占整個虛擬地址空間,這樣鏈接器和加載器的實現會比較容易,不必考慮各進程的地址範圍是否沖突。
![技術分享](http://hahack.com/images/c-memory/link.sepva.png)
進程地址空間是獨立的
- VA到PA的映射會給分配和釋放內存帶來方便,物理地址不連續的幾塊內存可以映射成虛擬地址連續的一塊內存。比如要用malloc分配一塊很大的內存空間,雖然有足夠多的空閑物理內存,卻沒有足夠大的連續空閑內存,這時就可以分配多個不連續的物理頁面而映射到連續的虛擬地址範圍。
![技術分享](http://hahack.com/images/c-memory/link.discontpa.png)
不連續的PA可以映射為連續的VA
- 一個系統如果同時運行著很多進程,為各進程分配的內存之和可能會大於實際可用的物理內存,虛擬內存管理使得這種情況下各進程仍然能夠正常運行。因為各進程分配的只不過是虛擬內存的頁面,這些頁面的數據可以映射到物理頁面,也可以臨時保存到磁盤上而不占用物理頁面,在磁盤上臨時保存虛擬內存頁面的可能是一個磁盤分區,也可能是一個磁盤文件,稱為交換設備(Swap Device)。當物理內存不夠用時,將一些不常用的物理頁面中的數據臨時保存到交換設備,然後這個物理頁面就認為是空閑的了,可以重新分配給進程使用,這個過程稱為換出(Page out)。如果進程要用到被換出的頁面,就從交換設備再加載回物理內存,這稱為換入(Page in)。換出和換入操作統稱為換頁(Paging),因此: 系統中可分配的內存總量=物理內存的大小+交換設備的大小
如下圖所示。第一張圖是換出,將物理頁面中的數據保存到磁盤,並解除地址映射,釋放物理頁面。第二張圖是換入,從空閑的物理頁面中分配一個,將磁盤暫存的頁面加載回內存,並建立地址映射。
![技術分享](http://hahack.com/images/c-memory/link.swap.png)
換頁
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);,則後一次調用會產生運行時錯誤。
虛擬內存管理【轉】