深入解析分段與分頁


文章已收錄我的倉庫:Java學習筆記與免費書籍分享

分段、分頁

引言

什么是碎片?

碎片分為內部碎片與外部碎片,都是指浪費而不能使用的空間。

內部碎片是指已分配但未被使用的地址空間。例如在64位空間內,你只適用7字節但由於內存對齊不得不為你分配8字節空間,這就產生了1字節內部碎片。

外部碎片是指未分配且未使用的地址空間。例如,你申請4字節的Int類型,再申請8字節的long類型,為了內存對齊,其中4字節無法裝入8字節類型,這就產生了4字節的外部碎片,如下圖所示。

內部碎片是已被分配的空間,是操作系統不可利用的空間;外部碎片是未被分配的,是可分配的,但該空間過小(碎片的含義)無法裝入資源,導致不可利用,但外部碎片是可解決的,可以將多個外部碎片緊湊成一個大的空閑空間,但這需要大量成本。

外部碎片

段式模型的前身:基址加界限寄存器(動態重定位)

要想理解分段與分頁,必須先談談早期的虛擬內存模型。

在經歷了純物理地址后,科學家們期望解決這種內存模型難以統一的問題,於是虛擬內存技術孕育而生,但困擾科學家們的是,如何將虛擬地址轉換成物理地址。

早期的科學家們很容易的想到將整個程序作為一個整體,並為每個進程分配一個基址寄存器和界限寄存器,基址寄存器存放該虛擬地址在實際物理地址的起點,而界限寄存器則用以判定程序是否訪問非法地址。

通過這種方式,實際的地址很好計算:
$$
實際地址 = 虛擬地址 + 基址
$$
但是,這種方式仍然將進程的全部地址空間加載內存中,雖然解決了地址翻譯問題,但仍然產生了大量的內部碎片,如下圖中該進程棧堆區很小,於是在棧堆區之間產生了內部碎片。

image-20210912181943123

從圖中可以看出,如果我們將整個地址空間放入物理內存,那么棧和堆之間的空間並沒有被進程使用,卻依然占用了實際的物理內存。因此,簡單的通過基址寄存器和界限寄存器實現的虛擬內存很浪費。

另外,我們必須要確保內存足夠放得下進程的虛擬地址空間,但通常主存成本是比較昂貴的,不如磁盤廉價,這種方式通常不支持大的虛擬地址,如果剩余物理內存無法提供連續區域來放置完整的地址空間,進程便無法運行。例如現在32位的進程空間通常是4GB,主存根本就裝不下幾個進程。

關鍵問題是:怎樣支持大虛擬地址空間,同時棧和堆之間(可能)有大量空閑空間?在之前的例子里,地址空間非常小,所以這種浪費並不明顯。但設想一個32位(4GB)的地址空間,通常的程序只會使用幾兆的內存,但卻需要一次性的將整個地址空間都放在內存中。

我們需要更復雜的機制以利用物理內存,避免內部碎片,早期的科學家們想出了分段這種思想。

分段式管理

分段思想

分段思想其實就是將基址加界限的概念泛化,在上述例子中,我們為代碼、堆和棧段分別維護一個段基址加段界限寄存器,樣我們不必要每次都強制的裝入整個進程空間,每個基址寄存器存放該段在物理地址的實際空間,界限寄存器仍然用於保護地址空間。

image-20210912185535856

現在對於程序未使用的空間,我們沒必要為其分配了(注意:我們仍需要為堆預留較多空間,除了堆段,其余段空間都是在編譯器就確定了),這樣便大大增加了內存的利用率,此外我們發現可以離散的分配空間,即物理內存中的地址不必要是連續的,這也能大大的提高對物理地址的利用率。

分段地址轉換

分段地址轉換與基址加界限的思想大同小異,在分段思想中,程序可能具有多個段,操作系統通過一個段表來維護各段信息:

段表的地址是操作系統維護的,段表項主要維護段長和段基址,段基址指該段在物理內存中的起始地址,那么該段中的虛擬地址對於實際的地址即為 段基址 + 段內偏移

分段系統的邏輯地址結構是由段號(段名)和段內地址(段內偏移量)所組成。

例如,若系統是按字節尋址,用3二進制位表示邏輯地址,如果段號占和段內地址各占16位,那么它的邏輯地址結構圖如下所示。

那么我們讀取前16位作為段號,后16位作為段內偏移,操作系統通過計算 addr = 段號 * 段表項大小 + 段表地址得出對應的段表項地址,通過查詢該段表項得出段基址,通過計算 段基址 + 段內偏移得出物理地址。

你可能發現了,在虛擬地址中,每個段的起始地址都是固定的,每個段的總大小都是固定的,其大小為:
$$
size = 2^p字節,p = 段內地址的位數
$$
如下圖所示,注意展示的是虛擬地址的空間:

image-20210912204532734

此外,棧地址是反向增長的,因此段表中必須維護一個比特位,描述是否為棧段。

段的另一個優點:很好的支持共享

隨着分段機制的不斷改進,系統設計人員很快意識到,通過再多一點的硬件支持,就能實現新的效率提升。具體來說,要節省內存,有時候在地址空間之間共享(share)某些內存段是有用的。尤其是,代碼共享很常見,今天的系統仍然在使用。

為了支持共享,需要一些額外的硬件支持,這就是保護位(protection bit)。基本為每個段增加了幾個位,標識程序是否能夠讀寫該段,或執行其中的代碼。通過將代碼段標記為只讀,同樣的代碼可以被多個進程共享,而不用擔心破壞隔離。

為什么頁不行?純頁式管理中,一個頁是比較大的,頁內毫無任何邏輯信息,因此可能放置任何代碼,因此我們必須還要確定哪些代碼是用於共享,這增加了成本。

因此我們常說,段式管理是符合用戶邏輯的,是利於保護和共享的。

虛擬地址翻譯太慢?

我們每次翻譯一個虛擬地址都需要去找尋段表中的段表項,相當於多義詞地址訪問,這太慢了!解決方案是為計算機設置一個小型的硬件設備,將虛擬地址直接映射到物理地址,而不必再訪問段表。這種設備稱為轉換檢測緩沖區 (Translation Lookaside Buffer,TLB),有時又稱為快表。

快表是一個小的高速緩存,現代操作系統無論是分段還是分頁中都利用了這種軟件技術,有關於快表地址翻譯的問題我們將在專門針對地址翻譯的文字講解。

段的缺點:過多的外部碎片

分段可以避免產生內部碎片(不是絕對的),但由於分段是離散的在主存內找到空閑的槽塊並插入,問題是物理內存很快充滿了許多空閑空間的小洞,因而很難分配給新的段,或擴大已有的段 —— 大量外部碎片。

例如4kb的空間裝入3kb的段,產生的1kb的空間無法在裝入任何段,產生碎片的主要原因是因為分段使用的大小是不確定的。

當然前面也提到過,外部碎片可通過緊湊的方式以合成較大的空閑空間,但這需要大量成本,操作系統難以維護。

這種情況下,分頁式管理應運而生。

image-20210912210836008


分頁式管理

分頁思想

對於分段式的管理,一段時間后主存上將會遍布大大小小的外部碎片,操作系統難以進行維護,分段的思想是將內存空間分割成不同長度的分片,由於長度不是固定的,產生外部碎片是必然的,之前提到的將整個程序一起裝入的方法雖然不會產生外部碎片,但會產生巨大的內部碎片,我們需要更細粒度的划分,以減少內部碎片的產生,解決這一問題的辦法是將空間分割成較小的、固定長度的分片,這就是分頁式管理。

分頁式管理將程序資源划分為固定大小的頁,將每一個虛擬頁映射到物理頁之中,由於每個頁是固定大小的,操作系統可以整齊的分配物理內存空間,避免產生了外部碎片,例如一個頁大小是4kb,而主存是40kb,操作系統稍加管理便能確保無論何時都能整齊的裝入10個頁面。

image-20210913101838707

要注意到頁在物理內存中也不是連續存在的,進程未使用的頁也沒必要為其分配內存,通過這種方式我們就解決了由分段產生大量外部碎片的問題,同時由於頁較小,只有在已使用的頁才會產生少量的內存碎片,這也是可以接受的,目前來看,分頁是一個良好的解決辦法。

image-20210913102317080

分頁地址轉換

正如同分段一樣,分頁地址轉換也需要基址+頁內偏移來完成,在分段中采用段表來存儲段基址,而在分頁中則采用頁表來存儲頁基址,頁基址表示頁在實際內存中的起始地址,那么實際的地址:
$$
addr = 頁基址 + 頁內偏移
$$
頁表是由操作系統維護的,操作系統知道頁表的起始未知,頁表項的大小是固定的,在32位地址空間中,通常是8字節,這64比特中不僅存儲了頁基址,還存放着一些其他重要的數據,如:有效位、可讀位、臟位等。

虛擬地址是由頁表號 + 頁內偏移組成的,這與分段中的虛擬地址類似,我們來進行一個簡單的計算以得出32位程序中頁表號所占用的位數,其中一個頁表的大小通常是4kb,那么:
$$
虛擬地址的總空間大小=2^{32}=4GB\
頁表項的個數=\frac{4GB}{4kb}=2^{20}\
4kb=2^{{12}}byte
$$
要能表示220個頁表項,我們必須分配20位地址,剩余12位代表頁內偏移,即下圖中p=12,n=32,我們通常稱虛擬頁號為VPN,而稱頁偏移量為VPO,如下圖所示:

image-20210913115545830

為什么直接取VPO就代表了頁偏移量?這也是很好理解的,因為:頁偏移量 = 虛擬地址 - 頁起始地址,而頁起始地址其實是固定的,即當VPO位全為0時,為對應頁的起始地址 ,此時 虛擬地址 - 頁起始地址 即為VPO表示數值。

現在操作系統取出虛擬地址,我們設其為vAddr,便可以通過如下步驟翻譯成物理地址:

  1. 獲取VPN與VPO,即VPN = vAddr & 0XFFFFF000;VPO = vAddr & 0X00000FFF;
  2. 獲取頁表項地址,$頁表項地址 = 頁表起始地址 + VPN × 頁表大小$;
  3. 從該頁表項內取出頁基址,即實際物理起始地址PPN(注意這里僅有20位,需要右移動12位才是真正的地址);
  4. 將PPN與VPO連接起來,即$真實地址 = (PPN << 12) | VPO$
image-20210913121551686

實際的頁表項還包含其他一些信息,如下圖便是酷睿I7操作系統中的頁表項:

image-20210913121745079

分頁的缺點:頁表過大怎么辦?

正如我們上面所計算的,對於32位操作系統而言,假定頁大小為4kb,我們得出大概需要220個頁表項,而每個頁表項大小通常是8字節,這意味着頁表的大小將是$2^{{20}} × 8byte = 4Mb$,這大的令人發指,然而大多數程序可能僅使用幾mb的大小,頁表的大小甚至比整個進程所需的所有資源還大,我們必須想辦法解決這個問題,但前提是,我們仍然要支持進程的虛擬大地址空間,盡管進程可能用不上這么多。

你可能會想到,對於進程未使用的空間,操作系統不為其分配頁表項以節省空間。

的確,這確實解決問題的辦法,但關鍵在於,操作系統根本不可能做到真正意義上的不分配頁表項,操作系統必須要確保每一個虛擬地址都具有意義。這句話也許優點拗口,讓我們來看一個例子:

我們假設地址空間是三位的,前兩位代表頁號,后一位代表頁偏移,那么進程虛擬地址共有8個,進程空間大小為8字節:
$$
\begin{cases}
000&頁號為0,偏移為0的地址\
001&頁號為0,偏移為1的地址\
010&頁號為1,偏移為0的地址\
011&頁號為1,偏移為1的地址\
100&頁號為2,偏移為0的地址\
101&頁號為2,偏移為1的地址\
110&頁號為3,偏移為0的地址\
111&頁號為3,偏移為1的地址\
\end{cases}
$$

那么我們必須准備四個頁表項,存放頁0 ~ 3的物理起始地址,現在該進程沒有使用頁0與頁1,我們假設操作系統沒有維護0 ~ 1的頁表項,現在對於頁表而言,頁號3是該頁表的第一個偏移量,這不對!頁號3無法被正確訪問!

即時你想出某個辦法使得頁3能被正確翻譯,但假設此時進程收到訪問地址為000的指令呢?這是可能的,由程序在運行時生成的。現在整個系統都將陷入苦惱,根本沒有任何關於地址000的信息。現在你可能理解了,操作系統必須要確保每一個虛擬地址都具有意義,當該虛擬地址為被使用時,也必須有一些信息來標識該地址未被進程使用,屬於非法地址。

所以直接上的方法是不管用的,解決這一問題的辦法是在加一層抽象。

多級頁表

在多級頁表中,上一級頁表存放的是對應的下一級頁表的起始地址,並至少存在一個有效位標識以標識下一級頁表是否存在。

image-20210913195940092

看一個例子,仍然假設3位地址的操作系統,第一位表示一級頁表,第二位表示二級頁表,第三位表示頁偏移:
$$
\begin{cases}
000&一級頁表頁號為0,二級頁表頁號為0,偏移為0的地址\
001&一級頁表頁號為0,二級頁表頁號為0,偏移為1的地址\
010&一級頁表頁號為0,二級頁表頁號為1,偏移為0的地址\
011&一級頁表頁號為0,二級頁表頁號為1,偏移為1的地址\
100&一級頁表頁號為1,二級頁表頁號為0,偏移為0的地址\
101&一級頁表頁號為1,二級頁表頁號為0,偏移為1的地址\
110&一級頁表頁號為1,二級頁表頁號為1,偏移為0的地址\
111&一級頁表頁號為1,二級頁表頁號為1,偏移為1的地址\
\end{cases}
$$
我們有一個一級頁表,一級頁表有兩項,一級頁表項至少存在一個有效位,如果確實有效則還要保存下一級頁表的起始地址。

我們仍然假設000;001;010;011這些地址進程未使用,現在假設進程訪問地址010,MMU(地址翻譯單元)取出一級頁號 0,並訪問一級頁表偏移為0的頁表項,此時操作系統發現該使用為設置為0(未使用),則無須訪問二級頁表,並立即返回,告知進程該地址非法,拋出異常或終止進程。

現在一級頁表中頁號為0對應的二級頁表無須再加載進來了,我們僅需要一級頁表的兩個表項和一級頁表頁號為1的兩個二級表項,共四個頁表表項,這個例子中我們所需頁表表項沒有改變,這是因為我們假設的頁表太小了,在實際中,一旦一級頁表使用未設置為0,可以有幾千個二級頁表項不被加載進來,極大的減小頁表大小,即時是在我們的示例中,一級頁表項實際上也比真實的頁表項要小,仍然可以減少頁表所占內存。

事實上,多級頁表中每一級頁表都可以設置的被恰好裝進一個頁,這樣將不會產生任何內部碎片或外部碎片。

例如在酷睿i7中采用4級頁表,每個頁表9位,每一級占9位,每個頁表項8字節,那么每一級頁表大小是$2^{9} × 8byte = 4kb$,剛好是一個頁的大小。

image-20210913202938886

應該指出,多級頁表是有成本的。在TLB未命中時,需要從內存加載多次,才能從頁表中獲取正確的地址轉換信息(一次用於頁目錄,其他用於PTE本身)。因此,多級表是一個時間—空間折中(time-space trade-off)的小例子。我們想要更小的表(並得到了),但不是沒代價。盡管在常見情況下(TLB命中),性能顯然是相同的,但TLB未命中時,則會因較小的表而導致較高的成本。

另一個明顯的缺點是復雜性。無論是硬件還是操作系統來處理頁表查找(在TLB未命中時),這樣做無疑都比簡單的線性頁表查找更復雜。通常我們願意增加復雜性以提高性能或降低管理費用。在多級表的情況下,為了節省寶貴的內存,我們使頁表查找更加復雜。

段頁式存儲

應該想到,在加一層抽象時,我們不僅僅可以加頁,還可以加段,多年前,Multics的創造者(特別是Jack Dennis)在構建Multics虛擬內存系統時,偶然發現了這樣的想法。具體來說,Dennis想到將分頁和分段相結合,以減少頁表的內存開銷。

現在,我們仍然將應用程序分段,但我們對於每一個段實施頁式管理,結合分段的思想,很容易可以理解為什么為什么這種想法可以減少內存開銷:由於段保存的僅僅是已使用的資源,那么對每個段分頁,其中每個頁都是被使用的!

舉個例子,假設程序分為代碼段、堆段、棧段,4GB的虛擬空間,程序僅僅使用了15kb,其中代碼段7kb,棧段4kb,堆段4kb,那么實際物理空間占用情況如圖所示:

image-20210913204619350

我們能夠確保所有頁面都被使用,僅僅每個段的最后一個頁可能會產生少許內部碎片!

我們來思考段頁式的地址轉換,這需要我們結合分段與分頁,此時段描述符(段表項)不再存放段基址和段長了,而是存放該段對應頁表的地址,段長頁存放頁表的長度:

image-20210913205438064

下圖地址向上增長:image-20210913205507730

那么此時的虛擬地址也應該表示為 段號 + 頁號 + 偏移量

image-20210913205645467

我們執行如下算法:

1)根據段號找到段描述符。

2)檢查該段的頁表是否在內存中。如果在,則找到它的位置;如果不在,則 產生一個段錯誤。如果訪問違反了段的保護要求就發出一個越界錯誤(陷阱)。

3)檢查所請求虛擬頁面的頁表項,如果該頁面不在內存中則產生一個缺頁中 斷,如果在內存就從頁表項中取出這個頁面在內存中的起始地址。

4)把偏移量加到頁面的起始地址上,得到要訪問的字在內存中的地址。

段頁式管理還使得操作系統對於某些保護或共享片段非常好管理,我們可以將一整個共享代碼作為一段而不必如分頁中標記頁內哪些代碼是共享的,此外可以發現這種管理還消除了分頁管理中可能存在的少許內部碎片(僅僅只有一個段的結尾存在內部碎片,可忽略不計),同時又如分頁一般,不含有任何外部碎片,易於管理,真是一個巧妙的思想。

總結:

段式管理:

優點:消除了內部碎片,提高了對物理內存的利用率;將應用按邏輯分段,人們可以編寫不同類型的代碼,可以方便的進行共享或保護。

缺點:會產生大量的外部碎片,使得操作系統難以分配空閑空間。

頁式管理

優點:消除了外部碎片,提高了對物理內存的利用率,利於操作系統管理空閑空間。

缺點:仍然會產生內部碎片,盡管每個頁碎片不超過頁的大小;頁表過大,占用大量空間,可以采用多級頁表思想解決。

段頁式管理

優點:同時具備段式和頁式的所有優點。

缺點:需要更多的硬件支持;當TLB未命中,需要更多的時間訪問內存。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM