1. 程式人生 > >記憶體管理與分頁機制

記憶體管理與分頁機制

一、問題提出:

我們經常會使用malloc()以及free()函式進行堆區記憶體申請與釋放。那麼你是否會這樣做:

int * p = malloc(0);/*malloc分配了0個位元組嗎,如果是那麼p指向誰呢,是NULL嗎*/
free(p);/*假如malloc分配了0個位元組,p指向了NULL,那麼free(NULL)不會出現段錯誤嗎*/

我想很少有人這樣做,因為除了喜歡“打破砂鍋問到底”,或者經常使用測試一些特例的方法去學習的的人,一般人不會注意到這個問題到底是怎樣的結果。

我們可以做一個簡單的測試:

/*****************************
**
**2016年12月25日16:09:44
**測試環境:Redhat 6.4
**測試int * p = malloc(0);p是否指向NULL
**
*****************************/
#include<stdio.h> #include<stdlib.h> int main(void){ int * p = (int *)malloc(0); printf("%d,%d\n",*(p),*(p+1024)); free(p); int * q = NULL; printf("%d,%d\n",*(q),*(q+1024)); return 0; }

這裡寫圖片描述

在測試中我們可以看到,q指標指向NULL,所以對其取值會發生段錯誤,而對於p來說,雖然它申請了0位元組的空間,但是free()釋放以及取值時都不會發生段錯誤(讀者可以拆開測試,否則有人會懷疑是free()引發的的段錯誤,而不是*q

取q值時引發的段錯誤)。由此我們可以得知,malloc(0)分配的不是0個位元組,p也不是指向NULL。那malloc(0)分配了幾個位元組?並且為什麼*(p+1024)也不會越界發生段錯誤呢?這就是記憶體的分頁機制與記憶體管理所決定。

二、虛擬記憶體(Virtual Memory)與實體記憶體(Physical memory):

1、記憶體型別細分:

記憶體由於用途不同,分類也不盡相同,一般我們對於記憶體的分類也就這幾種:棧區(stack area)、堆區(heap area)、全域性區(靜態區)(存放全域性變數與靜態變數static)、BSS段(存放未初始化的全域性變數,未初始化的全域性變數預設值為0)、文字常量區、資料區(data area)、程式碼區(code area)等。

關於BSS段儲存的未初始化全域性變數的值,我們可以測試一下,如下(i為未初始化的全域性變數,其值為0):
這裡寫圖片描述

而關於這些不同型別的記憶體地址區域,其所在位置如下圖所示:
這裡寫圖片描述

2、Linux記憶體分配時的maps檔案:

關於上面所講記憶體劃分的各段地址位置關係,我們可以用程式進行測試:

# include<stdio.h>
# include<stdlib.h>
int num1;/*BSS段*/
int num2 = 2;/*全域性區*/
char * str1 = "str1";/*文字常量區*/
int main(void){
    printf("%d\n",getpid());/*獲取當前程序id號*/

    int num3 = 3;/*棧區*/
    static int num4 = 4;/*全域性區*/
    const int num5 = 5;/*棧區*/
    char * str2 = "str2";/*文字常量區*/
    char str3[] = "str3";/*棧區*/
    int * p = malloc(sizeof(0));/*&p在棧區,p在堆區*/

    printf("num1:%p\nnum2:%p\nnum3:%p\nnum4:%p\nnum5:%p\n",&num1,&num2,&num3,&num4,&num5);
    printf("str1:%p\nstr2:%p\nstr3:%p\n",str1,str2,str3);
    printf("&p:%p\np:%p\n",&p,p);

    while(1){}/*死迴圈以保證程序不會結束,方便檢視/proc/pid/maps檔案*/

    free(p);

    return 0;
}

我們可以檢視/proc/pid/maps檔案(pid表示以程序id號命名的檔名),其中有該pid的記憶體分配的詳細情況。注意:proc下各個程序目錄佔磁碟大小都是0(讀者可自行測試),因為其資料都存在於記憶體,該檔案只是一個對映。實際不存在,如果該程序消亡,pid這個目錄及其子目錄將會消失。所以可以用迴圈測試,並且maps檔案中的記憶體地址為已經映射了實體記憶體的虛擬記憶體地址。我們先執行程式,如下所示(獲得當前程序pid為5052):

這裡寫圖片描述

我們可以”vim /proc/5052/maps”檢視該檔案下的記憶體分配情況
cd proc/5052:
這裡寫圖片描述
vim maps:

這裡寫圖片描述

3、記憶體地址對映關係:

每個程序都先天設定了4G的虛擬記憶體地址(不是真實的地址,只是一個編號)。虛擬記憶體開始時不對應任何記憶體,直接使用會引發段錯誤,不進入核心就接觸不到實體記憶體地址,只會接觸到虛擬記憶體地址。虛擬記憶體地址必須對映實體記憶體(或者硬碟上的檔案)以後才能儲存資料(資料儲存在實體記憶體上,列印地址為虛擬記憶體地址)。而記憶體分配其實就是虛擬記憶體地址對映實體記憶體的過程,記憶體回收則是解除對映關係的過程。
虛擬記憶體中,0~3G是使用者控制,3~4G是核心空間。使用者層不能直接訪問核心層,可以通過Unix/Linux的系統函式訪問核心層。我們通常所講記憶體地址,其實都不是真正意義上的實體記憶體(PC機上記憶體硬體)的地址,而是虛擬記憶體地址。兩個不同的程序,當其某個變數地址一樣(虛擬),但是實體地址並不一樣。

對映關係如圖所示(A、B程序均已對映實體記憶體,而C程序未對映實體記憶體,注意:虛擬記憶體一般並不會全部對映):

這裡寫圖片描述

對於不同程序的同一地址,是虛擬地址而不是實體地址我們可以做個測試:
這裡寫圖片描述

由於兩個不同程序有各自的虛擬記憶體,列印的程序1的記憶體地址為虛擬記憶體地址,而程序2的相同的虛擬記憶體地址,不能操作程序1的虛擬記憶體地址已對映的實體記憶體地址,並且程序2的*p並沒有對映實體記憶體地址,所以程序2執行出現段錯誤。

三、記憶體分頁機制(Memory Paging Mechanism)與malloc詳解:

1、記憶體管理頁機制:

最小儲存單位是一個位元組(1B),最小管理單位是一頁(4KB),虛擬記憶體地址連續時實體記憶體地址可以不連續,即使一次分配6000位元組(不到兩頁也分配兩頁),兩個記憶體頁實體地址可能不挨著。多次申請記憶體時,如果之前分配的頁記憶體沒用完,則不再分配,除非之前分配的記憶體頁用完才繼續對映新的一頁。getpagesize()可以獲取當前記憶體頁的大小。硬碟也是如此(硬碟上稱為Block塊):即使一個.txt檔案中只有一個“a”字母,其大小為1B而其佔用大小為4K。

如圖所示:test.txt檔案中僅僅有14個’a’字元,但是現實其佔用磁碟大小仍然是4K(一頁)
這裡寫圖片描述

Windows下也有相同的機制(檔案大小小於實際佔用空間大小,佔用大小是磁碟分塊單位的整數倍):
這裡寫圖片描述

2、為什麼要有這種機制(一次性最少分配1頁(4K))?

一句話:為了方便管理。
不可能程序每次申請一次系統就需要向其分配一次。(就像你和弟弟管媽媽要1塊錢買辣條,你媽媽給了你倆十塊錢說:“一週內都不要給我再要”,其實就算你一週內再向她要,媽媽也會給你,她只是不想你們倆不停地要而已,這就是管理(只不過我管我媽要1塊,她好像給我5毛錢…….))。系統也是這樣,它一次分配至少一頁,在你(程序)沒用完之前它都不會再給你分配,而當你用完分配的記憶體之後,就需要重新分配了。

就拿malloc來說,第一次malloc(0)時一次性對映33個記憶體頁(Redhat6.4),關於這點我們測試一下:

這裡寫圖片描述

只malloc()了一次,分配了33頁,對前33頁操作不會出錯,但是一超過33頁(p相對位置不為0,p+33*1024為虛擬地址的第34頁)就產生了段錯誤,因為超過的虛擬記憶體地址並沒有對映(分配)實體記憶體。

3、malloc(0)分配了多少記憶體?

例如:malloc(sizeof(int))申請了4位元組,系統卻給它33頁,而malloc()給變數分配給變數記憶體時,除了資料區域外,還額外需要儲存一些資訊。底層有一個雙向連結串列儲存額外資訊。malloc()給指標了12個位元組,其中4個位元組存放資料,另外8個存放其他資訊或者空閒,如果將12個位元組中前(低位)幾個位元組清空或者進行修改,free就可能出錯,因為free只有首地址不能釋放,還得需要額外附加資訊(如malloc分配的長度)。(低八位是附加資料,高四位是int型資料)

就拿我們測試記憶體劃分時的例子來說(僅借用地址劃分關係,程式不同):
這裡寫圖片描述

p申請了0位元組,但是系統分配了0X08fa9000~0X08fca000(共0X08fca000-0X08fa9000=21000H=(2^17+2^12)Byte=(2^7+2^2)KB=(2^5+1)頁=33頁)

這裡寫圖片描述

而p指向的地址為0X08fa9008(偏移了8個位元組),直接指向高四位的4個位元組(共12位元組)。
這裡寫圖片描述
如果我們將低八位的資料進行清空或者修改(修改任意個位元組),free就有可能失敗,測試如下:
這裡寫圖片描述
將p的低四位資料清零之後,附加資訊出錯,free失敗,出錯結果如下:
這裡寫圖片描述