現代作業系統普遍採用虛擬記憶體管理(Virtual Memory Management)機制,這需要處理器中的MMU(Memory Management Unit,記憶體管理單元)提供支援。首先引入 PA 和 VA 兩個概念。
如果處理器沒有MMU,或者有MMU但沒有啟用,CPU執行單元發出的記憶體位址將直接傳送到晶片引腳上,被內存晶片(以下稱為物理內存,以便與虛擬內存區分)接收,這稱為PA(Physical Address,以下簡稱PA),如下圖所示。
如果處理器啟用了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。
x86平台的虛擬位址空間是0x0000 0000~0xffff ffff,大致上前3GB(0x0000 0000~0xbfff ffff)是用戶空間,後1GB(0xc000 000ffff~0xffffffff,ffff~0xffff ffff~0xffff。
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)問題。此時指針已經變成野指針,應該及時手動將野指針置空。
虛擬記憶體管理可以控制實體記憶體的存取權。實體記憶體本身是不限制存取的,任何位址都可以讀寫,而作業系統要求不同的頁面具有不同的存取權限,這是利用CPU模式和MMU的記憶體保護機制實現的。
虛擬記憶體管理最主要的作用是讓每個行程有獨立的位址空間。所謂獨立的地址空間是指,不同進程中的同一個VA被MMU映射到不同的PA,並且在某一個進程中訪問任何地址都不可能訪問到另外一個進程的數據,這樣使得任何一個進程由於執行錯誤指令或惡意程式碼導致的非法記憶體存取不會意外改寫其它進程的數據,不會影響其它進程的運行,從而保證整個系統的穩定性。另一方面,每個行程都認為自己獨佔整個虛擬位址空間,這樣連結器和載入器的實作會比較容易,不必考慮各行程的位址範圍是否衝突。
VA到PA的對映會給分配和釋放內存帶來方便,物理位址不連續的幾塊記憶體可以映射成虛擬位址連續的一塊記憶體。例如要用malloc分配一塊很大的內存空間,雖然有足夠多的空閒物理內存,卻沒有足夠大的連續空閒內存,這時就可以分配多個不連續的物理頁面而映射到連續的虛擬地址範圍。
一個系統如果同時運行著很多進程,為各進程分配的內存總和可能會大於實際可用的物理內存,虛擬內存管理使得這種情況下各進程仍然能夠正常運行。因為各進程分配的只不過是虛擬記憶體的頁面,這些頁面的資料可以映射到實體頁面,也可以暫時保存到磁碟上而不佔用實體頁面,在磁碟上臨時保存虛擬記憶體頁面的可能是磁碟分割區,也可能是磁碟文件,稱為交換設備(Swap Device)。 當實體記憶體不夠用時,將一些不常用的實體頁面中的資料暫時儲存到交換裝置,然後這個實體頁面就認為是空閒的了,可以重新分配給行程使用,這個過程稱為換出(Page out)。如果進程要用到被換出的頁面,就從交換設備再載入回物理內存,這稱為換入(Page in)。換出和換入操作統稱為換頁(Paging),因此:
#如下圖所示。第一張圖是換出,將實體頁面中的資料儲存到磁碟,並解除位址映射,釋放實體頁面。第二張圖是換入,從空閒的實體頁面中分配一個,將磁碟暫存的頁面載入回內存,並建立位址映射。
C標準函式庫函數malloc可以在堆空間動態分配內存,它的底層透過brk系統呼叫向作業系統申請記憶體。動態分配的記憶體用完之後可以用free釋放,更準確地說是歸還給malloc,這樣下次呼叫malloc時這塊記憶體可以再被分配。
1 #include <stdlib.h>2 void *malloc(size_t size); //返回值:成功返回所分配内存空间的首地址,出错返回NULL3 void free(void *ptr);</stdlib.h>
# |
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 }</string.h></stdlib.h></stdio.h>
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
访问内存了。
如果一个程序长年累月运行(例如网络服务器程序),并且在循环或递归中调用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);,則後一次呼叫會產生運行時錯誤。
以上是詳解虛擬記憶體管理的詳細內容。更多資訊請關注PHP中文網其他相關文章!