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