Linux 内存管理 | 地址映射:分段、分页、段页
文章目錄
- 分段
- 分頁
- 多級頁表
- 快表(TLB)
- 段頁式
- Linux
Linux 內(nèi)存管理 | 物理內(nèi)存管理:內(nèi)存碎片、伙伴系統(tǒng)、slab分配器
Linux 內(nèi)存管理 | 虛擬內(nèi)存管理:虛擬內(nèi)存空間、虛擬內(nèi)存分配
在前兩篇博客中,我介紹了虛擬內(nèi)存與物理內(nèi)存的管理方式,那么對于操作系統(tǒng)來說,它是如何管理它們兩個之間的關(guān)系的呢?如何進(jìn)行地址的映射呢?
在早期的計算機中,程序是直接運行在物理內(nèi)存上的,所以其通常都會面臨以下幾種問題
為了解決這些問題,我們又引入了虛擬內(nèi)存這個概念,但是虛擬內(nèi)存是如何解決這個問題的呢?它與物理內(nèi)存又是如何進(jìn)行映射的呢?這就是本篇博客所要講的內(nèi)容。
對于操作系統(tǒng)來說,通常解決這個問題的方式有三種,一種是內(nèi)存分頁,另一種是內(nèi)存分段,以及兩者相結(jié)合的段頁式。
分段
在最開始時,人們采用的是分段的方法,為了簡化地址管理,所以將虛擬內(nèi)存空間中的虛擬內(nèi)存按照其邏輯劃分為代碼段、數(shù)據(jù)段、堆段、棧段幾部分
通過段寄存器中的段表,來將虛擬地址與物理地址進(jìn)行映射。段表中存儲了每一個邏輯段的段號對應(yīng)的物理內(nèi)存的起始地址。
對于每一個在虛擬內(nèi)存中存儲的數(shù)據(jù),其虛擬地址都以其所在的段號以及段內(nèi)偏移組成。
因此虛擬地址與物理地址的轉(zhuǎn)換方式如下
如上圖,例如變量A段號為2,段內(nèi)偏移為500。首先根據(jù)段號查詢段表,得知物理內(nèi)存起始地址位于3000的位置,接著找到對應(yīng)的起始地址,加上段內(nèi)偏移500,此時3500的位置即為其對應(yīng)的物理地址。
通過分段的方式,我們解決了上面所說的問題1和問題3,但是對于內(nèi)存的使用效率,分段仍然存在以下兩個問題
為什么會存在內(nèi)存碎片的問題呢?
在上面的講解中可以看出,在分段存儲中,一個段內(nèi)可能保存有多個變量,而這些變量都是從同一個物理地址起始位置開始偏移。因此在物理內(nèi)存中,同一個段中的數(shù)據(jù)使用了連續(xù)的地址空間。
例如我們有1G的物理內(nèi)存,倘若我們運行了512M的程序A,接著運行了128M的程序B,128M的程序C。剩余內(nèi)存為256M
倘若我們此時結(jié)束程序B,釋放內(nèi)存,此時總剩余空間為384M
倘若我們此時需要運行300M的進(jìn)程D,但是這時候就會因為剩余空間不連續(xù),導(dǎo)致我們的程序無法運行,這也就是我們常說的內(nèi)存外碎片問題。
那么如何解決這個問題呢?這就會使用到內(nèi)存交換。例如上面那種情況,我們就會將程序C寫入硬盤的SWAP分區(qū)(交換分區(qū),用于內(nèi)存和硬盤的空間交換)。緊接著再將其從硬盤中讀取回來,讓其緊挨著程序A的那塊內(nèi)存,這樣就能保證后面的空閑內(nèi)存都是連續(xù)的了。
為什么內(nèi)存交換的效率低呢?
由于分段對物理內(nèi)存的映射是以程序為單位,按照其邏輯進(jìn)行分段映射,如果我們的內(nèi)存不足,那么被換入換出到硬盤中的都是整個程序,這樣就必然會造成大量的磁盤訪問操作,總所周知,磁盤IO的速度特別慢,因此就會嚴(yán)重影響我們的訪問速度。
根據(jù)程序的局部性原理,當(dāng)一個程序在運行時,在某個時間段內(nèi),它只是頻繁地用到了一小部分?jǐn)?shù)據(jù),也就是說程序中的很多數(shù)據(jù)其實在一個時間段內(nèi)都是不會被用到的。
而我們分段的最大問題就在于其以程序為單位進(jìn)行映射,因此我們只需要使用更小粒度的存儲單位,就可以解決這個問題,大大的提升內(nèi)存的使用率。因此在后續(xù)的設(shè)計中,就以頁作為基本單位,這也就是分頁機制的由來
分頁
分頁就是將內(nèi)存空間人為地劃分成固定大小的頁,每一頁地大小由硬件決定,在Linux中,一頁是4KB
與段表類似,虛擬地址與物理地址的映射是通過MMU(內(nèi)存管理單元)中的頁表來完成的。
頁表中不僅保存了頁號,物理內(nèi)存地址,還保留了該物理頁的訪問權(quán)限,用以實現(xiàn)對頁的訪問控制
在分頁機制下,虛擬地址由頁號以及頁內(nèi)偏移組成
因此在分頁機制下,虛擬地址與物理地址的轉(zhuǎn)換方式如下
如下圖
當(dāng)進(jìn)程需要訪問物理地址時,此時CPU就會通過MMU中的頁表,來找到對應(yīng)的物理地址。
講了這么多,再次回到之前的問題,分頁是如何解決分段的內(nèi)存利用率低的問題的呢?
主要就是依靠以下兩方面來完成的
1、使用更低粒度的內(nèi)存單位
分段所面臨的最大問題,無非就是內(nèi)存碎片以及交換效率低。
導(dǎo)致內(nèi)存碎片最大的原因就是各個邏輯段的數(shù)據(jù)需要連續(xù)存儲,而邏輯段又過大,導(dǎo)致我們需求大量的連續(xù)空間。而當(dāng)我們所有的內(nèi)存分配釋放都以頁為單位時,就能夠很好的解決這個問題了。
而當(dāng)內(nèi)存空間不夠時,我們需要進(jìn)行將內(nèi)存中的數(shù)據(jù)暫時寫入到硬盤中,之后再重新寫回來這樣的換入換出操作。而使用頁為單位后,即使我們還是需要進(jìn)行磁盤IO,但是由于我們交換的容量僅僅只有幾個頁,所以也不會花費過多的時間。
2、不需要將程序一次性加載進(jìn)內(nèi)存,什么時候需要,什么時候加載。
按照前面說的,為了滿足程序的局部性原理。所以為了能夠盡可能提高內(nèi)存的利用率,在建立了虛擬內(nèi)存空間后并不會直接分配物理內(nèi)存,而是在我們程序運行中需要用到的時候,再將其加載進(jìn)內(nèi)存中。
所以如果在頁表中查找不到時,此時就會由內(nèi)核的請求分頁機制產(chǎn)生缺頁中斷,然后進(jìn)入內(nèi)核態(tài)中分配物理內(nèi)存、更新進(jìn)程頁表,最后再返回用戶態(tài),恢復(fù)進(jìn)程的運行。
在上面所介紹的頁表中,有一個非常致命的缺點,就是空間占用大。
在 Linux中,可以并發(fā)的執(zhí)行多個進(jìn)程,而每個進(jìn)程都有其自己的虛擬內(nèi)存空間,那么也自然都有自己獨有的頁表。在32位Linux系統(tǒng)下,我們的虛擬內(nèi)存空間的大小為4G,而每頁的大小為4K,這也就意味著我們至少有2^20個內(nèi)存頁,倘若每個頁表項為4Byte,那么每個頁表大小也至少為4M。
倘若我們此時并發(fā)了兩百個進(jìn)程,那么占用則高達(dá)800M,即使是在現(xiàn)在,這個數(shù)字也是非常龐大的,因為并發(fā)數(shù)百個進(jìn)程是非常常見的情況,更別提64位的操作系統(tǒng),隨著尋址范圍的增加,頁表將更為龐大。
為了解決這個問題,就引入了多級頁表。
多級頁表
我們將一級頁表再進(jìn)行分頁,分成1024個二級頁表,并且每個二級頁表中存有1024個頁表項,形成如下的二級分頁的結(jié)構(gòu)。
雖然分級乍一看花費的物理內(nèi)存變多了,但是實際上對于大多數(shù)程序來說,其使用到的空間遠(yuǎn)未達(dá)到 4G,所以會存在部分對應(yīng)的頁表項都是空的,根本沒有分配。而對于已分配的頁表項,如果存在最近一定時間未訪問的頁表,在物理內(nèi)存緊張的情況下,操作系統(tǒng)會將頁面換出到硬盤,也就是說不會占用物理內(nèi)存。
如果某個一級頁表的頁表項沒有被用到,也就不需要創(chuàng)建這個頁表項對應(yīng)的二級頁表了,即可以在需要時才創(chuàng)建二級頁表。假設(shè)每個二級頁表大小為4M(1024 * 4K),而我們用到的一級頁表只有20%
在這種情況下,頁表所占用的物理內(nèi)存就只有4K + 20% * 4M,即0.804M,比起只用了一級頁表的4M,大大的節(jié)約了內(nèi)存。
而在64位系統(tǒng)中,兩級頁表是肯定不夠用的,因此又演變成了四級目錄
- 全局頁目錄項 PGD
- 上層頁目錄項 PUD
- 中間頁目錄項 PMD
- 頁表項 PTE
結(jié)構(gòu)如下圖所示
快表(TLB)
多級頁表雖然解決了空間占用大的問題,但是由于其復(fù)雜化了地址的轉(zhuǎn)換,因此也帶來了大量的時間開銷,使得地址轉(zhuǎn)換速度減慢。
如果要解決這個問題,那么最簡單的方式就是降低查詢頁表的頻率,那么如何實現(xiàn)呢?這時候就需要用到緩存的技術(shù)
與我之前在Redis系列博客中所提到的,對于熱點資源,我們可以將其提前緩存下來,到以后使用時就可以直接到緩存中查找。對于操作系統(tǒng)來說,也是這么一個道理。
在操作系統(tǒng)中,這個緩存就是CPU中的TLB,也就是我們通常所說的快表。我們將最常訪問的幾個頁表項存儲到TLB中,在之后進(jìn)行尋址時,CPU就會先到TLB中進(jìn)行查找,如果沒有找到,這時才會去查詢頁表。
段頁式
雖然分段和分頁各有優(yōu)缺點,但他們直接并不是對立的,所以如今大部分的內(nèi)存管理方式,都是將分段與分頁相結(jié)合,也就是我們常說的段頁式
它的原理非常簡單,就是先對虛擬內(nèi)存空間進(jìn)行分段管理,然后再對每一個段進(jìn)行分頁管理。如下圖
所以此時的虛擬地址結(jié)構(gòu),就由段號、段內(nèi)頁號、頁內(nèi)偏移所組成。此時對于每個進(jìn)程來說,都會建立一個段表,而對于段表中的每一個段,又會再分別建立一個頁表,如下圖
所以此時的虛擬地址轉(zhuǎn)換為物理地址,就需要以下三個步驟
這種方法雖然增加了系統(tǒng)開銷以及硬件成本,但是內(nèi)存的利用率得到了巨大的提升。
Linux
由于硬件問題的限制,Linux 內(nèi)存主要采用的是頁式內(nèi)存管理,但同時也不可避免地涉及了段機制。
在往常的機制中,地址的轉(zhuǎn)換流程如下
但是在Linux中,并沒有邏輯地址這一說(所有段起始地址相同),因為其將段機制進(jìn)行了弱化,此時段只用于進(jìn)行訪問控制以及內(nèi)存保護(hù)
Linux 系統(tǒng)中的每個段都是從 0 地址開始的整個 4GB 虛擬空間(32 位環(huán)境下),也就是所有的段的起始地址都是一樣的。
這意味著,Linux系統(tǒng)中的代碼,包括操作系統(tǒng)本身的代碼和應(yīng)用程序代碼,所面對的地址空間都是線性地址空間(虛擬地址),這種做法相當(dāng)于屏蔽了處理器中的邏輯地址概念,段只被用于訪問控制和內(nèi)存保護(hù)。
總結(jié)
以上是生活随笔為你收集整理的Linux 内存管理 | 地址映射:分段、分页、段页的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 趣谈设计模式 | 桥接模式(Bridge
- 下一篇: Redis 缓存常见问题:缓存一致性的解