認識各種記憶體地址

什麼是物理地址?

物理地址就是記憶體單元的絕對地址,物理地址0x0000就表示記憶體條的第一個存儲單元,0x0010(16進位)就表示記憶體條的第17個存儲單元,一個存儲單元是1byte(8bit)。

你問為什麼是1byte?
mem
一個記憶體條是由若干個黑色的記憶體顆粒構成的。每一個記憶體顆粒叫做一個chip。
bank
每個chip中又疊了若干bank
row
在每個bank內部,就是電容的行列矩陣結構了,每一個元素有8個小電容,存儲8個bit,也就是一個位元組。

什麼是線性地址和虛擬地址?

在80286系列以前,CPU只支援實模式操作模式。16位暫存器想要對20位地址線進行定址,使用分段機制,段基址(16位) x 16(左移4位) + 段內偏移地址(4位)就是物理地址了。
這樣的好處是所見即所得,程式設計師指定的地址就是物理地址,物理地址對程式設計師是可見的。同時也帶來了一些問題:
1) 無法支援多任務。
2) 程式的安全性無法得到保證。
(根本原因就是一個程式直接修改了其他程式的記憶體,導致崩潰)

80286系列則是被設計來解決這些問題,段式訪存得到的改進,原來段基址+段內偏移得到的地址不再是實際的物理地址,而是被稱作為線性地址,要經過一個轉換層轉換才變成一個物理地址。這種CPU操作模式就被稱為保護模式了。保護的是:分清楚各個程式使用的存儲區域,不允許隨便跨界訪問。

80286系列之後就進入32位CPU時代了,32位暫存器可以直接訪問32位地址匯流排。但是在保護模式下,地址仍然採用「段地址:偏移地址」的方式來表示。
段值仍然由原來的16位的cs、ds等暫存器表示,但是此時它們僅僅是一個索引,這些個索引指向一個數據結構的表項,表項中詳細定義了一個段的起始地址、界限、屬性等內容,這個數據結構,叫做GDT(其實還可能是LDT),GDT中的每一個表項,叫做描述符。
這裡我們就詳細看一下保護模式的定址方式吧。
protect
1) 定址時,先找到gdtr暫存器,從中得到GDT的基址。
2) 有了GDT的基址,又有段暫存器中保存的索引,可以得到段暫存器「所指」的那個表項,即所指的那個描述符。
3) 得到了描述符,就可以從描述符中得到該描述符所描述的那個段的起始地址。
4) 有了段的起始地址,將偏移地址拿過來與之相加,便能得到最後的線性地址。
5) 有了線性地址,經過變換,即可得到相應的物理地址。

保護模式雖然解決了記憶體不被跨界訪問,但是其也帶來了新的問題,那就是記憶體碎片。
首先我們了解一下記憶體碎片為何產生:
首先假設我們有10B記憶體:

ID 首地址 尾地址 長度 狀態
0 0 9 10 空閑

當程式申請一個長度為3的記憶體空間後:

ID 首地址 尾地址 長度 狀態
0 0 2 3
1 3 9 7 空閑

當程式再申請一個長度為2,以及長度為4的記憶體空間後:

ID 首地址 尾地址 長度 狀態
0 0 2 3
1 3 4 2
2 5 8 4
3 9 9 1 空閑

此時,只剩1個可用空間。如果這時程式再來申請長度大於1的空間,就申請不了,也就是記憶體不夠。
現在,釋放掉ID=1的空間:

ID 首地址 尾地址 長度 狀態
0 0 2 3
1 3 4 2 空閑
2 5 8 4
3 9 9 1 空閑

我們發現,現在可用記憶體空間為3,但是,這3個空閑空間,並不是連續的。
所以,如果程式現在申請長度為3的記憶體空間,同樣會申請不了,會出現記憶體不夠。我們把這種情況,稱之為記憶體碎片

那記憶體碎片怎麼解決呢?於是就有了分頁機制,接下來我們詳細講一下分頁:
首先,把物理記憶體,按照某種尺寸,進行平均分割。比如我現在以2個記憶體單位,來分割記憶體,也就是每兩個連續的記憶體空間,組成一個記憶體頁:

地址 頁ID 狀態
0 0 空閑
1
2 1 空閑
3
4 2 空閑
5
6 3 空閑
7
8 4 空閑
9

接著,系統同樣需要維護一個記憶體資訊表:

ID 使用的記憶體頁ID

現在,程式申請長度為3的記憶體空間,不過由於現在申請的最小單位為頁面,而一個頁面的長度為2,因此現在需要申請2個頁面,也就是4個記憶體空間。你看,這就浪費了1個記憶體空間。

地址 頁ID 狀態
0 0
1
2 1
3
4 2 空閑
5
6 3 空閑
7
8 4 空閑
9
ID 使用的記憶體頁ID
0 0,1

接著,程式再申請長度為1,長度為2的空間:

地址 頁ID 狀態
0 0
1
2 1
3
4 2
5
6 3
7
8 4 空閑
9
ID 使用的記憶體頁ID
0 0,1
1 2
2 3

釋放掉ID=1,記憶體頁ID為2的那條記憶體空間資訊:

地址 頁ID 狀態
0 0
1
2 1
3
4 2 空閑
5
6 3
7
8 4 空閑
9
ID 使用的記憶體頁ID
0 0,1
2 3

現在,就出現了之前的情況:目前一共有4個記憶體空間,但是不連續。
不過,因為現在是分頁管理機制,因此,現在仍然可以繼續申請長度為4的記憶體空間。
沒有碎片,能夠盡量地全部用完空間。但仔細想想,這種優勢背後,也是需要付出大量代價的。
分頁的方式下,程式需要記錄記憶體頁ID,每次使用時,需要從記憶體頁ID翻譯成實際記憶體地址,多了一次轉換。
而且這種模式,會浪費一些記憶體,比如上面申請3個記憶體空間,實際分配了2個頁面共4個記憶體空間,浪費了1個記憶體空間。

還有一個要注意的地方,這個時候”段基址+段內偏移地址”經過段部件處理後得到的線性地址就不再是物理地址了,而是虛擬地址了。
從下圖我們能夠清楚的看出來我們最後的線性地址表示的是頁表的地址,而不是物理地址了。
page

什麼是邏輯地址和有效地址?

無論CPU在什麼模式下,段內偏移地址又稱為有效地址或者邏輯地址(只是叫法不一樣罷了),例如實模式下mov ax, [0x7c00]0x7c00就是邏輯地址(或有效地址),但這條指令最終操作的物理地址是DS*16+0x7c00
Linux最初就是在32位的80386系列上設計的,並且沒有使用分段機制,所以在Linux上邏輯地址和線性地址就是一回事了。

總結

一圖以概括:
mem_address

我的部落格://geanqin.github.io/