內存管理子系統是操作系統中最重要的部分之一。
1、地址類型
物理地址:指出現在CPU外部地址總線上的尋址物理內存的地址信號,是地址變換的最終結果,是內存中實實在在存在的硬件地址。
邏輯地址:程序代碼經過編譯后在匯編程序中使用的地址,C/C++程序中取地址求出來的地址就是邏輯地址。
虛擬地址(線性地址):在32位CPU架構下,可以表示4G的地址空間,用16進制表示就是0x00000000到0xffffffff。只有啟動分頁機制的時候才有線性地址,如果沒有分頁機制,那么線性地址就是物理地址。
2、地址轉換
CPU要將一個邏輯地址轉換為物理地址,需要兩步:首先CPU利用段式內存管理單元,將邏輯地址轉換成虛擬地址,再利用頁式內存管理單元,把虛擬地址最終轉換為物理地址。
即MMU(內存控制單元)通過分段單元把一個邏輯地址轉為線性地址,接著通過分頁單元把線性地址轉為物理地址。
3、段式管理
(1)16位cpu的段式管理
16位CPU內部擁有20位的地址線,它的尋址范圍就是2的20次方,也就是1M的內存空間。但是16位CPU用于存放地址的寄存器(IP,SP……)只有16位,因此只能訪問65536個存儲單元,64K。
為了能夠訪問1M的內存空間,CPU就采用了內存分段的管理模式,并在CPU內部加入了段寄存器。16位CPU把1M內存空間分為若干個邏輯段,每個邏輯段的要求如下:
a、邏輯段的起始地址(段地址)必須是16的倍數,即最后4個二進制位必須全為0。
b、邏輯段的最大容量為64K。
段寄存器---->
段寄存器是為了對內存進行分段管理而增加的,16位CPU有四個段寄存器,程序可同時訪問四個不同含義的段。
1)CS+IP:用于代碼段的訪問,CS指向存放程序的段基址,IP指向下條要執行的指令在CS段的偏移量,用這兩個寄存器就可以得到一個內存物理地址,該地址存放著一條要執行的指令。
2)SS+SP:用于堆棧段的訪問,SS指向堆棧段的基地址,SP指向棧頂,可以通過SS和SP兩個寄存器直接訪問棧頂單元的內存物理位置。
3)DS+BX:用于數據段的訪問。DS中的值左移四位得到數據段起始地址,再加上BX中的偏移量,得到一個存儲單元的物理地址。
4)ES+BX:用于附加段的訪問。ES中的值左移四位得到附加段起始地,再加上BX中的偏移量,得到一個存儲單元的物理地址。
物理地址的形成方式--->
由于段地址必須是16的倍數,所以值的一般形式為XXXX0H,即前16位二進制位是變化的,后四位是固定的0,鑒于段地址的這種特性,可以只保存前16位二進制位來保存整個段基地址,所以每次使用時要用段寄存器左移補4個0(乘以16)來得到實際的段地址。
在確定了某個存儲單元所屬的段后,只是知道了該存儲單元所屬的范圍(段地址->段地址+65536),如果想確定該內存單元的具體位置,還必須知道該單元在段內的偏移。有了段地址和偏移量,就可以唯一的確定內存單元在存儲器中的具體位置。
而,邏輯地址=段內偏移量
所以,由邏輯地址得到物理地址的公式為:PA=段寄存器的值*16+邏輯地址
(2)32位cpu的段式管理
32位pc的內存管理依然采用“分段”的管理模式,物理地址同樣由段地址和偏移量組成。但既然分為32位和16位,就肯定有不同之處,32位pc采用了兩種不同的工作方式:實模式和保護模式。
1)實模式
在實模式下,32位CPU的內存管理與16位CPU是一致的。最大尋址空間1MB,最大分段64KB。
2)保護模式
段基地址長達32位,每個段的最大容量可達4G,段寄存器的值是段地址的“選擇器”(Selector),用該“選擇器”從內存中得到一個32位的段地址,存
儲單元的物理地址就是該段地址加上段內偏移量。
3、分頁管理
線性地址被分為固定長度的組,稱為頁。例如,32位的機器,線性地址最大可為4G,如果用4kb為一個頁來劃分,這樣整個線性地址就被劃分為2的20次方個頁。
分頁單元把所有的物理內存也劃分為固定長度的管理單位,它的長度一般與線性地址頁是相同的。
如上圖所示,每一個32位的線性地址被劃分為3部分:頁目錄索引(10位)、頁表索引(10位)、偏移(12位)。
分頁管理的步驟---->
1)裝入進程的頁目錄地址(操作系統在調度進程時,把這個地址裝入CR3)
2)根據線性地址的前十位,在頁目錄中,找到對應的索引項,頁目錄中的項是一個頁表的地址
3)根據線性地址的中間十位,在頁表中找到頁的起始地址
4)將頁的起始地址與線性地址與線性地址的最后12位相加,得到物理地址
4、linux頁式管理
Linux操作系統采用虛擬內存管理技術,使得每個進程都有獨立的進程地址空間,該空間是大小為3G,用戶看到和接觸的都是虛擬地址,無法看到實際的物理地址。利用這種虛擬地址不但能起到保護操作系統的作用,而且更重要的是用戶程序可使用比實際物理內存更大的地址空間。
Linux將4G的虛擬地址空間劃分為兩個部分——用戶空間與內核空間。用戶進程通常情況下只能訪問用戶空間的虛擬地址,不能訪問內核空間。例外情況是用戶進程通過系統調用訪問內核空間。
如上圖展示了一個Linux進程的地址空間的組織結構,對于32位進程來說,代碼段從地址0x08048000開始;對于64位進程來說,代碼段從0x00400000開始。
關于上圖堆和棧的區別--->
stack:由系統自動分配,地址增長方向是從高到低。第一個進棧的是主函數中的下一條指令(函數調用語句的下一條可執行語句)的地址,然后是函數的各個參數(以從右往左的方式入棧),然后是函數中的局部變量
heap:需要程序員自己申請,并指明大小。以鏈表的形式進行管理,地址增長方向是從低到高。
char *p1 = (char *)malloc(10);//c語言申請堆的方式
char *p2 = new char(10);//在c++中申請堆的方式
注:p1 p2本身在棧中,而分配的10字節則在堆區。
注:關于棧是向低地址增長,而堆向高地址增長,這樣設計可以使得堆和棧可以充分地利用空閑的地址空間
程序執行時的內存分配情況 -->
int a = 0; //a 在全局已初始化數據區
char *p1; //p1 在 BSS 區(未初始化全局變量)
main()
{
int b; //b 在棧區
char s[] = "abc";
//s 為數組變量,存儲在棧區, //“abc”為字符串常量,存儲在.rodata常量字符串區
char *p1,p2; //p1、p2 在棧區
char *p3 = "123456"; //123456\0 在.rodata常量字符串區,p3 在棧區
static int c =0; //c為局部靜態數據,data數據區
p1 = (char *)malloc(10); //分配得來的 10 個字節的區域在堆區
p2 = (char *)malloc(20); //分配得來的 20 個字節的區域在堆區
free(p1);
free(p2);
}
------end
ps:小小總結,望對在路上的你有所幫助。