Linux記憶體管理的相關知識
2010-07-02 14:44:39| 分類: Linux C | 标簽:linux記憶體管理的相關知識 |字号 訂閱
記憶體管理
1、C程式結構
C程式在沒有調入記憶體之前(也就是在存儲時),分為代碼區(text)、資料區(data)和未初始化資料區(bss)3個部分。
¨ 代碼區 存放CPU執行的機器指令,即函數體的二進制代碼。由于對于頻繁被執行的程式,隻需要在記憶體中有一份代碼即可,是以代碼區是可共享的(可以被别的程式調用)。為了防止程式意外地修改代碼區的機器指令,通常代碼區是隻讀的。
¨ 全局初始化資料區和靜态資料區 包含已經被初始化的全局變量、靜态變量(包括全局靜态變量和局部靜态變量)和常量資料(如字元串常量)。
¨ 未初始化資料區 存儲的是全局未初始化變量。
以上談的是存儲時C語言的程式結構,下面看看運作時C語言的程式結構。
¨ 代碼區 該區的機器指令根據程式設計流程依次執行。代碼區的指令包括操作碼和要操作的對象(或對象位址引用,即寄存器間接尋址等)。如果操作對象是立即數,則将直接包含在代碼中;如果操作對象是局部資料,則将在棧區配置設定空間,然後引用該資料位址;如果操作對象存在于BSS區和資料區,則在代碼區中也将引用該資料位址。
¨ 全局初始化資料區和靜态資料區 隻做一次初始化
¨ 未初始化資料區 在運作時改變其值
¨ 棧區 由編譯器自動配置設定釋放,存放函數的參數值、局部變量的值等等,其操作方式類似于資料結構中的棧。 這裡簡要談一下C程式函數調用過程中棧架構的建立過程:
1) 第一個進棧的是主函數中函數調用後的下一條指令(函數調用語句的下一條可執行語句)的位址(目的是為了恢複現場)
2) 然後是函數的各個參數,在大多數的C編譯器中,參數是由右往左入棧的
3) 然後是函數中的局部變量(注意靜态變量是不入棧的)
4) 當本次函數調用結束後,局部變量先出棧,然後是參數,最後棧頂指針指向最開始存的位址,也就是主函數中的下一條指令,程式由該點繼續運作。
¨ 堆區 用于動态配置設定記憶體,位于未初始化資料區和棧區之間,一般由程式員配置設定和釋放,若程式員不釋放,程式結束時可能由OS回收。
問題:為什麼要專門開辟代碼區
一個程序在運作的過程中,代碼是根據流程一次執行的,隻需要執行一次(當然跳轉和遞歸也可使代碼執行多次),然而程式可能會對資料進行多次通路(但沒有必要為了對資料進行多次通路而多次通路代碼),此時我們就有必要把代碼區和資料區區分開來管理。
一例程式
//main.cpp
int a = 0; //a在全局初始化資料區
char *p1; //p1在全局未初始化資料區
int main(int argc, char *argv[ ])
{
int b; // b為局部變量,是以存儲于棧區
char s[] = "abc"; // s為數組局部變量,存在于棧區
char *p2; // p2為局部變量,是以存儲于棧區
char *p3 = "123456"; // 123456\0在常量區(已初始化資料區),p3在棧上。
static int c =0; // c存儲于靜态資料區(靜态資料區和全局初始化區同在一個區域)
p1 = (char *)malloc(10); // 系統動态配置設定得來的10和20位元組的區域就在堆區
p2 = (char *)malloc(20);
free(p1);
free(p2);
return 0;
}
2、記憶體的配置設定(記憶體的申請)
1) 申請方式
¨ Stack(靜态配置設定):靜态對象是有名字的變量,可以直接對其進行操作。由系統自動配置設定記憶體。 例如,聲明在函數中一個局部變量 int b; 系統自動在棧中為b開辟空間。
¨ Heap(動态配置設定):動态對象是沒有名字的變量,需要通過指針間接地對它進行操作。需要程式員自己申請記憶體,并指明大小。
在C中malloc函數,如p1 = (char *)malloc(10);
在C++中用new運算符,如p2 = new char[20]; //(char *)malloc(20);
配置設定堆空間之後,p1、p2得到所配置設定堆空間首位址,将會指向堆空間。
2) 申請後系統的響應
¨ 棧:隻要棧的剩餘空間大于所申請空間,系統将為程式提供記憶體,否則将報異常提示棧溢出
¨ 堆:首先應該知道作業系統有一個記錄空閑記憶體位址的連結清單,當系統收到程式的申請時,會周遊該連結清單,尋找第一個空間大于所申請空間的堆結點,然後将該結點從空閑結點連結清單中删除,并将該結點的空間配置設定給程式。
其次,大多數系統,會在這塊記憶體空間中的首位址處記錄本次配置設定的大小,這樣,代碼中的delete語句才能正确的釋放本記憶體空間。
此外,由于找到的堆結點的大小不一定正好等于申請的大小,系統會自動的将多餘的那部分重新放入空閑連結清單中。
3) 申請大小的限制
棧:在Windows下,棧是高位址向低位址擴充的資料結構,是一塊連續的記憶體的區域。這句話的意思是棧頂的位址和棧的最大容量是系統預先規定好的。
在WINDOWS下,棧的大小是2M(也有的說是1M,總之它是一個編譯時就确定的常數),如果申請的空間超過棧的剩餘空間時,将提示overflow。是以,能從棧獲得的空間較小。
堆:堆是低位址向高位址擴充的資料結構,是不連續的記憶體區域。這是由于系統是用連結清單來存儲的空閑記憶體位址的,自然是不連續的,而連結清單的周遊方向是由低位址向高位址。
堆的大小受限于計算機系統中有效的虛拟記憶體,由此可見,堆獲得的空間很靈活。
但由程式員操作的過程中容易發生記憶體洩露,也極易産生記憶體空間的不連續,即記憶體碎片(頻繁使用malloc和free(new和delete)的結果)。
4) 申請效率的比較
¨ 棧由系統自動配置設定,速度較快,但程式員是無法控制的。
¨ 堆是由malloc或者new配置設定的記憶體,一般速度比較慢,而且容易産生記憶體碎片,不過用起來最友善.
5) 配置設定效率的比較
棧是機器提供的資料結構,機器會在底層對棧提供支援:配置設定專門的寄存器存放棧的位址,壓棧出棧都有專門的指令。
堆則是C函數庫提供的,它的機制很複雜。首先應該知道作業系統有一個記錄空閑記憶體位址的連結清單,當系統收到程式的申請時,會周遊該連結清單,尋找第一個空間大于所申請空間的堆結點,然後将該結點從空閑結點連結清單中删除,并将該結點的空間配置設定給程式。
此外,由于找到的堆結點的大小不一定正好等于申請的大小,系統會自動的将多餘的那部分重新放入空閑連結清單中。
可見,堆得存取效率和棧比較起來要低得多。
3、下面再給出一個資料存儲區域的執行個體
#include <stdio.h>
#include <malloc.h>
#include <unistd.h>
#include <alloca.h>
extern void afunc(void); //聲明afunc()函數
extern etext, edata, end; //聲明三個外部變量
int bss_var; // 未初始化全局資料存儲在BSS區
int data_var = 42; // 初始化全局資料存儲在資料區
// 定義了一個宏,用于列印位址
#define SHW_ADR(ID, I) printf("the %8s\t is at adr:%8x\n", ID, &I);
int main(int argc, char *argv[])
{
char *p, *b, *nb;
printf("Adr etext:%8x\t Adr edata%8x\t Adr end %8x\t\n", &etext, &edata,&end);
printf("\ntext Location:\n");
SHW_ADR("main", main); // 檢視代碼段main函數位置
SHW_ADR("afunc", afunc); // 檢視代碼段afunc函數位置
printf("\NBSS Location:\n");
SHW_ADR("bss_var", bss_var); // 檢視BSS段變量位置
printf("\NDATA Location:\n");
SHW_ADR("data_var", data_var); // 檢視資料段變量位置
printf("\nSTACK Location:\n");
afunc();
p = (char *)alloca(32); // 從棧中配置設定空間
if(p != NULL){
SHW_ADR("start", p); // 列印棧空間的起始位置
SHW_ADR("end", p+31); // 列印棧空間的結束位置
}
b = (char *)malloc(32*sizeof(char)); // 從堆中配置設定空間
nb = (char *)malloc(16*sizeof(char)); // 從堆中配置設定空間
printf("\NHEAP Location:\n");
printf("the heap start: %p\n", b); // 列印堆起始位置
printf("the heap end: %p\n", (nb+16*sizeof(char))); // 列印堆結束位置
printf("\nb and nb in Stack\n");
SHW_ADR("b", b); // 列印棧中字元指針變量b的存儲位置
SHW_ADR("nb", nb); // 列印棧中字元指針變量nb的存儲位置
free(b); // 釋放申請的堆空間
free(nb); // 釋放申請的堆空間
return 0;
}
void afunc(void)
{
static int long level = 0; // 靜态資料存儲在資料段中
int stack_var; // 局部變量,存儲在棧區
if(++level == 5){
return;
}
printf("stack_var is at:%p\n", &stack_var); // 列印局部變量的位址
afunc(); // 遞歸執行afunc函數
}
4、介紹幾個記憶體管理函數
1) Malloc/free函數
原型:extern void *malloc(size_t num_bytes);
頭檔案:include <stdlib.h>
功能:配置設定長度為num_bytes位元組的記憶體塊
傳回值:如果配置設定成功則傳回指向被配置設定記憶體首位址的指針,否則傳回空指針NULL。
說明:
¨ 該函數傳回為void型指針,是以必要時要進行類型轉換。
¨ 當記憶體不再使用時,應使用free()函數将記憶體塊釋放。
問題:為什麼不再使用時,要用free()釋放掉所申請的記憶體?
¨ 由于記憶體區域總是有限的,不能無限制地配置設定下去。
¨ 程式應該盡可能地去節省資源,當申請的堆空間不再使用時,應該釋放掉,交由其它程序來使用。
注意:不能用free()來釋放非malloc(), calloc(), realloc()函數所建立的堆空間,否則會發生錯誤。
2)new/delete(在C++中)
使用new/delete運算符實作記憶體管理比malloc/free函數更有優越性。它們的定義如下:
Static void* operator new(size_t sz);
Static void* operator delete(void* p);
先看一段C++代碼:
void test(void)
{
//申請一個sizeof(obj)大小的一塊動态記憶體,并把頭指針指派給obj類型的指針變量a
obj *a = new obj;
delete a; //清除并且釋放所申請的記憶體
}
下面通過一段代碼具體介紹一下new/delete的用法:
Class A
{
Public:
A() { count << “A is here!” << endl; } //構造函數
~A() { count << “A is here!” << endl; } //析構函數
Private:
Int I;
};
A* pA = new A; // 調用new運算符申請空間
delete pA; // 删除pA
其中,語句new A完成了一下兩個功能:
A. 調用new運算符,在堆上配置設定一個sizeof(A)大小的記憶體空間。
B. 調用構造函數A(),在所配置設定的記憶體空間上初始化對象。
語句delete pA完成的是相反的兩件事:
A. 調用析構函數~A(),銷毀對象。
B. 調用運算符delete,釋放記憶體。
注意:
¨ 使用new比使用malloc()有以下優點
A. New 自動計算要配置設定給對象的記憶體空間大小,不使用sizeof運算符,這樣一來簡單,而來可以避免錯誤。
B. 自動地傳回正确的指針類型,不用進行強制類型轉換。
C. 用構造函數給配置設定的對象進行初始化。
¨ 使用malloc函數和new配置設定記憶體的時候,本身并沒有對這塊記憶體空間做清零等任何工作。是以,申請記憶體空間後,其傳回的新配置設定的空間是沒有用零填充的,程式員須使用memset()函數來初始化記憶體。
3) realloc函數(更改已經配置的記憶體空間)
頭檔案:include <stdlib.h>
函數定義:
void *realloc(void *ptr, size_t size)
參數ptr為先前由malloc、calloc和realloc所傳回的記憶體指針,而參數size為新配置的記憶體大小。
realloc函數用來從堆上配置設定記憶體,當需要擴大一塊記憶體空間時,realloc()試圖直接從堆上目前記憶體段後面的位元組中獲得更多的記憶體空間:
¨ 如果能夠配置設定成功,則傳回指向這塊新記憶體空間的首位址,而将原來的指針(realloc函數的參數指針)指向的空間釋放掉;
¨ 如果目前記憶體段後面的空閑位元組不夠,那麼就使用堆上第一個能夠滿足這一要求的記憶體塊,将目前的資料複制到新的位置,而将原來的資料塊釋放掉;
¨ 如果記憶體不足,重新申請空間失敗,則傳回NULL,此時原來的指針(realloc函數的參數指針)仍有效。
#include <stdio.h>
#include <stdlib.h>
int main (int argc, char* argv[], char* envp[]) //主函數
{
int input;
int n;
int *numbers1;
int *numbers2;
numbers1 = NULL;
if((numbers2 = (int *)malloc(5*sizeof(int))) == NULL) //numbers2指針申請空間
{
printf("malloc memory unsuccessful");
//free(numbers2);
//numbers2=NULL;
exit(1);
}
for (n=0; n<5; n++) //初始化(0-4)
{
*(numbers2+n)=n;
printf("numbers2's data: %d\n", *(numbers2+n)); //把0-4列印出來
}
printf("Enter an integer value you want to remalloc ( enter 0 to stop)\n"); //新申請空間大小
scanf ("%d", &input);
numbers1 = (int *)realloc(numbers2, (input+5)*sizeof(int)); // 重新申請空間
if (numbers1 == NULL)
{
printf("Error (re)allocating memory");
exit (1);
}
for(n=0;n<5;n++) // 這5個數是從numbers2(原指針空間)拷貝而來
{
printf("the numbers1s's data copy from numbers2: %d\n", *(numbers1+n));
}
for(n=0; n<input; n++) // 新資料初始化(0-input)
{
*(numbers1+5+n) = n*2;
printf ("nummber1's new data: %d\n", *(numbers1+5+n)); // numbers1++;
}
printf("\n");
free(numbers1); // 釋放numbers1
numbers1 = NULL;
// free(numbers2); // 不能再釋放numbers2,因為number2早已被系統自動釋放
return 0;
}