分段可以給每個程序分配乙個不同的程序位址空間,而分頁可以把相同的線性位址空間對映到不同的實體地址空間。
通俗的講,就是每個程序都有乙個虛擬位址到實體地址的轉換表,而這個轉換表不同的程序是不同的。所以就會出現不同程序的同乙個虛擬位址對應不同的實體地址
一、邏輯位址轉線性位址
機器語言指令中出現的記憶體位址,都是邏輯位址
,需要轉換成線性位址,再經過mmu(cpu中的記憶體管理單元)轉換成實體地址才能夠被訪問到。
我們寫個最簡單的hello world程式,用gccs編譯,再反編譯後會看到以下指令:
mov 0x80495b0, %eax
這裡的記憶體位址0x80495b0 就是乙個邏輯位址,
必須加上隱含的ds 資料段的基位址
,才能構成線性位址。也就是說
0x80495b0 是當前任務的ds資料段內的偏移。
段式對映過程中所有程序全都共用乙個gdt,沒有用到ldt。
而頁式對映中,每個程序都有其自身的頁面目錄pgd
,指向這個目錄的指標儲存在每個程序的mm_struct 資料結構中,每當排程乙個程序進入執行的時候,核心都要為即將執行的程序設定好控制暫存器cr3,而mmu的硬體總是從cr3中取得指向當前頁面目錄的指標。而cpu在執行時使用的是虛擬位址,而mmu硬體在進行對映是使用的則是實體地址(cr3)。這個是在inline函式switch_mm()中完成的,如下:
static inline void switch_mm(struct mm_struct *prev,struct mm_struct *next, struct tast_struct *tsk)
從硬體角度來說的話,linux核心只要能為硬體準備好頁面目錄pgd、頁面表pt以及全域性段描述表gdt和區域性段描述表ldt,並正確地設定了有關的暫存器,就完成了記憶體管理機制中位址對映部分的準備工作。
核心中有乙個mem_map,這是乙個全域性變數,是乙個指標,指向乙個page資料結構的陣列,每個page資料結構代表著乙個物理頁面,整個陣列代表著系統中的全部物理頁面,因此,頁面表項的高20位對於軟體和mmu硬體有著不同的意義,對於軟體,這是乙個物理頁面的序號,將這個序號用作下標就可以從mem_map找到代表這個物理頁面的page資料結構,對於硬體,則(在低位補上12個0後)就是物理頁面的起始位址。
在x86保護模式下,段的資訊(段基線性位址、長度、許可權等)即段描述符佔8個位元組,段資訊無法直接存放在段暫存器中(段暫存器只有2位元組)。intel的設計是段描述符集中存放在gdt或ldt中,而段暫存器存放的是段描述符在
gdt或ldt
內的索引值
(index)。
linux中邏輯位址等於線性位址
。為什麼這麼說呢?因為
linux
所有的段(使用者**段、使用者資料段、核心**段、核心資料段)的線性位址都是從
0x00000000
開始,長度
4g,這樣
線性位址=邏輯位址+ 0x00000000
,也就是說邏輯位址等於線性位址了。
這樣的情況下linux只用到了gdt
,不論是使用者任務還是核心任務,都沒有用到ldt。gdt的第12和13項段描述符是__kernel_cs 和__kernel_ds,第14和15項段描述符是__user_cs 和__user_ds。核心任務使用__kernel_cs和__kernel_ds,所有的使用者任務共用__user_cs 和__user_ds,也就是說不需要給每個任務再單獨分配段描述符。核心段描述符和使用者段描述符雖然起始線性位址和長度都一樣,但dpl(描述符特權級)是不一樣的。__kernel_cs 和__kernel_ds 的dpl值為0(最高特權),__user_cs 和__user_ds的dpl值為3。
用gdb除錯程式的時候,用info reg 顯示當前暫存器的值:
cs 0x73 115 (使用者**段)
ss 0x7b 123 (使用者資料段)
ds 0x7b 123
(使用者資料段)
es 0x7b 123
(使用者資料段)
可以看到ds值為0x7b, 轉換成二進位制為00000000 01111011,ti字段值為0,表示使用gdt,gdt索引值為01111,即十進位制15,對應的就是gdt內的__user_data 使用者資料段描述符。
從上面可以看到,linux在x86的分段機制上執行,卻通過乙個巧妙的方式繞開了分段。
linux主要以分頁的方式實現記憶體管理。
二、線性位址轉實體地址
前面說了
linux
中邏輯位址等於線性位址,那麼線性位址怎麼對應到實體地址呢?這個大家都知道,那就是通過分頁機制,具體的說,就是通過頁表查詢來對應實體地址。
準確的說分頁是
cpu提供的一種機制,
linux
只是根據這種機制的規則,利用它實現了記憶體管理。
在保護模式下,
控制暫存器
cr0的最高位
pg位控制著分頁管理機制是否生效,如果
pg=1
,分頁機制生效,需通過頁表查詢才能把線性位址轉換實體地址。如果
pg=0
,則分頁機制無效,線性位址就直接做為實體地址
。分頁的基本原理是把記憶體劃分成大小固定的若干單元,每個單元稱為一頁(
page
),每頁包含
4k位元組的位址空間
(為簡化分析,我們不考慮擴充套件分頁的情況)。
這樣每一頁的起始位址都是
4k位元組對齊的
。為了能轉換成實體地址,我們需要給
cpu提供當前任務的線性位址轉實體地址的查詢表,即頁表
(page table)
。注意,為了實現每個任務的平坦的虛擬記憶體,每個任務都有自己的頁目錄表和頁表。
為了節約頁表占用的記憶體空間,
x86將線性位址通過頁目錄表和頁表兩級查詢轉換成實體地址。
32位的線性位址被分成
3個部分:
最高10位
directory
頁目錄表偏移量,中間10位
table
是頁表偏移量,最低12位
offset
是物理頁內的位元組偏移量。
頁目錄表的大小為
4k(剛好是乙個頁的大小),包含
1024
項,每個項
4位元組(
32位),專案裡儲存的內容就是頁表的實體地址。如果頁目錄表中的頁表尚未分配,則實體地址填0。
頁表的大小也是
4k,同樣包含
1024
項,每個項
4位元組,內容為最終物理頁的物理記憶體起始位址。
每個活動的任務,必須要先分配給它乙個自己的頁目錄表,並把頁目錄表的實體地址存入
cr3暫存器。頁表可以提前分配好,也可以在用到的時候再分配。
還是以mov0x80495b0, %eax
中的位址為例分析一下線性位址轉實體地址的過程。
前面說到
linux
中邏輯位址等於線性位址,那麼我們要轉換的線性位址就是
0x80495b0
。轉換的過程是由cpu
自動完成的,
linux
所要做的就是準備好轉換所需的頁目錄表和頁表(假設已經準備好,給頁目錄表和頁表分配物理記憶體的過程很複雜,後面再分析)。
核心先將當前任務的頁目錄表的實體地址填入
cr3暫存器。
每乙個程序有它自己的頁全域性目錄和自己的頁表集。當發生程序切換時,linux把cr3控制暫存器的內容儲存在前乙個執行程序的描述符中,然後把下乙個要執行程序的描述符的值裝入cr3暫存器中。
因此,當新程序重新開始在cpu上執行時,分頁單元指向一組正確的頁表。
線性位址
0x80495b0
轉換成二進位制後是
0000 1000 0000 0100 1001 0101 1011 0000
,最高10
位0000 1000 00
的十進位制是32,
cpu檢視頁目錄表第
32項,裡面存放的是頁表的實體地址。線性位址中間10位
00 0100 1001
的十進位制是
73,頁表的第
73項儲存的是最終物理頁的物理起始位址。物理頁基位址加上線性位址中最低
12位的偏移量,
cpu就找到了線性位址最終對應的物理記憶體單元。
我們知道
linux
中使用者程序線性位址能定址的範圍是0 -
3g,那麼是不是需要提前先把這
3g虛擬記憶體的頁表都建立好呢?一般情況下,物理記憶體是遠遠小於
3g的,加上同時有很多程序都在執行,根本無法給每個程序提前建立
3g的線性位址頁表。
linux
利用cpu
的乙個機制解決了這個問題。程序建立後我們可以給頁目錄表的表項值都填0,
cpu在查詢頁表時,如果表項的內容為
0,則會引發乙個缺頁異常,程序暫停執行,
linux
核心這時候可以通過一系列複雜的演算法給分配乙個物理頁,並把物理頁的位址填入表項中,程序再恢復執行。當然程序在這個過程中是被蒙蔽的,它自己的感覺還是正常訪問到了物理記憶體。
Linux的分頁機制
先前我們介紹段機制的時候說到,x86的段機制把程式的邏輯位址轉換成線性位址,這裡要講的分頁機制是把線性位址對映成實體地址,也就說說,x86其實是用了兩套機制把邏輯位址轉換成實體地址的。我們也提到linux核心是怎樣繞過段機制從而讓x86的分段機制看起來不起作用的,我們還說到這樣的處理造成了段的資料保...
linux分頁機制
在linux分頁機制中有幾個概念需要了解,線性位址,實體地址,頁目錄表,頁表,頁目錄項,頁表項。首先為了防止乙個程序非法跨越到另乙個程序或者乙個程序非法跨越到核心中,linux中使用了線性位址。線性位址是乙個程序執行過程中產生的位址,在32位的系統中其位址空間為0x00000000 0xffffff...
linux分段分頁機制
mmu使用分段單元硬體把邏輯位址轉換為虛擬位址,再使用分頁單元硬體把虛擬位址轉換為實體地址。因為這兩部分表示乙個獨一無二的邏輯位址,虛擬位址作為這個段位址另一種形式,當然也需要這兩個部分作為轉換的 原材料。這裡涉及乙個叫做段的暫存器,它的作用是放段選擇符 識別符號 共有六種 cs 儲存指向 的段的選...