Linux系統內存管理與分頁機制

一、問題提出:

我們經常會使用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)也不會越界發生段錯誤呢?這就是內存的分頁機制與內存管理所決定。

另外,在stackoverflow上也有相關的回答:What's the point of malloc(0)?

二、虛擬內存(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))?

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

就拿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 之間的地址空間,共33個內存頁。
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失敗,出錯結果如下:

作者:Apollon_krj
鏈接:https://blog.csdn.net/apollon_krj/article/details/53869173
來源:CSDN
著作權歸作者所有。商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章