深入理解Linux内核之内存寻址
說明:?本文基于第三版《深入理解 Linux 內核》,該部分以 80x86 處理器為基準進行介紹,并且略過了原文中詳細介紹32位擴展分頁部分。
https://xcraft.tech/2021/07/10/OS/MemAddressing
目錄
一、內存地址
二、內存分段
三、內存分頁
3.1 硬件分頁
3.2 Linux 分頁
3.2.1 分頁機制的優勢
?3.2.2 進程頁表
?3.2.3 內核頁表
一、內存地址
1.1 邏輯地址 (logic address)
在機器語言指令中用來指定一個操作數或一條指令的地址,每一個邏輯地址都由以下兩部分組成
段 (segment)指明段位置
偏移量 (offset)指明段開始處到實際地址的距離
1.2 虛擬地址 (virtual address)
根據機器的位數不同而不同,32位機器即32位無符號整數、64位即64位無符號整數,這里取32位為例。
可用于表達 即 4GB 的地址空間
通常用16進制表示,值的范圍從 0x00000000 ~ 0xffffffff
1.3 物理地址 (physical address)
內存芯片級的內存單元尋址,從CPU的地址引腳發送到內存總線上的電信號相對應,由 32 位或 36 位無符號整數表示
1.4 內存控制單元 MMU
內存控制單元以下簡稱MMU,其集成在CPU上進行地址翻譯,轉換過程為兩階段
分段:由邏輯地址到虛擬地址
分頁:由虛擬地址到物理地址
1.5 內存仲裁器 MA
在多核系統中,所有的CPU核心共享同一內存,則代表著CPU可以并發的訪問內存。而內存的讀寫必須是串行執行,所以需要專用元器件對內存訪問進行排序,其稱為內存仲裁器。
內存仲裁器是在內存總線和RAM芯片之間的硬件電路
若內存空閑:允許訪問
若內存被占用:延遲CPU訪問
注:由于單處理器上存在一個叫做DMA控制器的特殊處理器,因此其實單處理器上也有內存仲裁器
1.6 分段和分頁的意義
分段和分頁是用于劃分進程的物理地址空間的
分段:每個進程分配不同的虛擬地址空間
分頁:把同一虛擬地址空間映射到不同的物理地址
Linux更多使用分頁的方式
不同進程共享同一組虛擬地址空間,內存管理簡單
跨平臺,因為RISC體系結構對分段支持有限
二、內存分段
2.1 硬件分段
2.1.1?實模式和保護模式
從 80286 模型開始,Intel處理器采用兩種不同方式進行地址轉換,稱為實模式(real mode)和保護模式(protected mode)
實模式
其作用是為維持處理器和早期模型的兼容,因為早期寄存器位數太少,物理地址有20位,最多1MB的內存空間。而段基址寄存器有16位,最多只能訪問64kb。為了訪問64kb以上的空間,需要對內存進行分段,使用段基址+段偏移的模式尋址。
通過?物理地址 = 段基址 << 4 + 段內偏移?的方式表示物理地址。這個實模式的 “實” 體現在其反應的是真實物理地址。
但是由于實模式沒有區分代碼和數據,如果用戶程序的一個指針如果指向了系統程序區域或其他用戶程序區域,并修改了內容,那么后果就很可能是災難性的。
保護模式
隨著寄存器硬件的擴展,地址位數和寄存器位數都變成了32/64位,現代CPU已經不需要使用上述實模式了,當然為了兼容老版本所以還是得支持實模式。
同時由于實模式不安全,我們通過一些手段來實現比較安全的尋址,這也是保護模式的命名的由來。
地址保護:程序內部的地址(虛擬地址)要由操作系統轉化為物理地址去訪問,程序對此一無所知
邊界保護:?段寄存器中不再儲存的是段地址而是段索引。我們將數據放在一個叫做全局描述符表(GDT) 的結構中,其中表項稱為段描述符,段描述符存放了段基址、段界限、內存段類型屬性,用來索引段地址和標記段邊界。
2.1.2?段選擇符和段寄存器
段選擇符
邏輯地址 = 段標識符 (16位) + 段偏移量 (32位),我們又將段標識符稱為段選擇符,其結構如下圖所示
index 描述符的入口,在2.1.4節中會詳細講解
TI (Table Indicator)標明段在GDT還是LDT中,在GDT中為0,LDT中為1
RPL 請求特權級,cs寄存器改變時指示出CPU當前特權級
段寄存器
段寄存器存放段選擇符,段寄存器共有 cs,ss,ds,es,fs和gs六個,其作用如圖三所示。
注:cs寄存器中還有一個兩位的字段,指明CPU當前特權級別(CPL)0~3,Linux只用0和3,代表內核態和用戶態??
2.1.3 段描述符
每個段被一個8字節的段描述符(Segment Descriptor)表示,描述了段的特征。
其放在?全局描述符表(GDT - Global Descriptor Table)?或?局部描述符表 (LDT - Local Descriptor Table)?中
全局描述符表(GDT - Global Descriptor Table)
- 特點:進程共享- 存放:gdtr寄存器(基址+大小)局部描述符表 (LDT - Local Descriptor Table)
特點:進程獨享
存放:ldtr寄存器(基址+大小)
段描述符字段
| Base | 包含段首字節的虛擬地址 |
| G | 粒度標志,如果為0,則以字節為單位,否則以4096字節的倍數計算 |
| Limit | 存放段中最后一個內存單元的偏移量,來決定段的長度。G為0則段在1到1MB,否則在4KB到4GB |
| S | 系統標志,如果為0,則代表該段為系統段,儲存LDT或其他關鍵數據結構,否則則是普通text或data段 |
| Type | 描述段的類型特征和存取權限 |
| DPL | 描述符特權級(DPL)字段;限制對該段的存取。表示訪問該段需要的最小 CPL (Linux 0/3) |
| P | Segement-Present標志,0代表不在主存中,Linux總將此設為1,因為整個段一直都會在主存中 |
| D or B | 取決于是代碼段還是數據段 |
| AVL 標志 | 可被操作系統使用,但Linux忽略該標志 |
描述符段分類
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?
代表任務狀態段(TSS)用于保存寄存器內容,僅在GDT中
代碼段描述符
數據段描述符
任務狀態段描述符 (TSSD)
2.1.4 快速訪問段描述符
段描述符的索引規則:
段基址 + 段選擇符 index [13位] << 3
因此描述符最大數目為2^13-1
2.1.5 分段單元
圖六已經較為清楚的展示了分段單元把邏輯地址轉為虛擬地址的過程,段選擇符在段寄存器中,offset存儲在ip寄存器中
邏輯地址翻譯
2.2 Linux 分段
2.2.1 Linux中的段結構
2.6版的Linux只有在 80x86 結構下才進行分段,下圖為Linux的分段結構
Linux分段
所有段都從0x00000000開始,所以在Linux下,邏輯地址和虛擬地址相同
相應端選擇符由宏?__USER_CS、__USER_DS、__KERNEL_CS、__KERNEL_DS?定義
CPU的CPL存儲在 cs 寄存器的 RPL 字段中,特權級別改變,某些段寄存器必須更新
例如當CPL由 3 變為 0 時 ds寄存器必須從含有用戶態數據段的段選擇符變為含有內核數據段的段選擇符,ss類似
2.2.2 Linux GDT
每個CPU對應一個GDT,所有的GDT都存放在 cpu_gdt_table 里,所有的GDT地址和大小被存放在 cpu_gdt_descr數組中。
這些符號在 arch/i386/kernel/head.S 中被定義
每個GDT包含 18個段描述符和14個空的保留項,保留項保證了常用的描述符可以在同一個32字節的 Cache 中,防止 Cache 抖動。
Linux GDT結構
三、內存分頁
3.1 硬件分頁
分頁單元(Paging Unit)是將虛擬地址轉化為物理地址
?關鍵任務:是將所請求的訪問類型和虛擬地址訪問權限相比較,如果訪問無效,則產生缺頁異常
?頁:一組虛擬地址,又指包含在這組地址中的數據。把RAM分成固定長度的頁框(Page Frame)每個頁框(結構)包含一個頁(數據)。
?頁表:將虛擬地址映射到物理地址的數據結構
?cr0寄存器:PG標志為0,虛擬地址就解釋為物理地址,否則如果 PG = 1 代表啟用分頁。
3.2 Linux 分頁
Linux采用4級分頁模式,節省內存空間花費,頁表基址寄存器cr3
頁全局目錄 (Page Global Directory)
頁上級目錄(Page Upper DIrectory)
頁中間目錄(Page Middle Directory)
頁表 (Page Table)
Linux多級分頁
如圖所示,虛擬地址翻譯過程,其將虛擬地址分為五部分,標準頁大小4kb,所以offset占12位,剩下 52 位 每 13 位代表相應目錄偏移量,先取出cr3寄存器中頁全局目錄的基址,和偏移量相加,索引到下級頁上級目錄的極致,如此重復,直到索引到頁表取出 PPN,由于物理地址偏移量和虛擬地址相同,所以直接和虛擬地址偏移量 VPO 拼接得到物理地址.
為什么要使用多級頁表來完成映射呢?
用來將虛擬地址映射到物理地址的數據結構稱為頁表, 實現兩個地址空間的關聯最容易的方式是使用數組, 對虛擬地址空間中的每一頁, 都分配一個數組項. 該數組指向與之關聯的頁幀, 但這會引發一個問題, 例如, IA-32體系結構使用4KB大小的頁, 在虛擬地址空間為4GB的前提下, 則需要包含100萬項的頁表. 這個問題在64位體系結構下, 情況會更加糟糕. 而每個進程都需要自身的頁表, 這回導致系統中大量的所有內存都用來保存頁表.
設想一個典型的32位的X86系統,它的虛擬內存用戶空間(user space)大小為3G, 并且典型的一個頁表項(page table entry, pte)大小為4 bytes,每一個頁(page)大小為4k bytes。那么這3G空間一共有(3G/4k=)786432個頁面,每個頁面需要一個pte來保存映射信息,這樣一共需要786432個pte!
如何存儲這些信息呢?一個直觀的做法是用數組來存儲,這樣每個頁能存儲(4k/4=)1K個,這樣一共需要(786432/1k=)768個連續的物理頁面(phsical page)。而且,這只是一個進程,如果要存放所有N個進程,這個數目還要乘上N! 這是個巨大的數目,哪怕內存能提供這樣數量的空間,要找到連續768個連續的物理頁面在系統運行一段時間后碎片化的情況下,也是不現實的。
假設每個進程都占用了4G的線性地址空間,頁表共含1M個表項,每個表項占4個字節,那么每個進程的頁表要占據4M的內存空間。為了節省頁表占用的空間,我們使用兩級頁表。每個進程都會被分配一個頁目錄,但是只有被實際使用頁表才會被分配到內存里面。一級頁表需要一次分配所有頁表空間,兩級頁表則可以在需要的時候再分配頁表空間,而Linux根據實際情況(64位CPU,內存大小,查詢效率),選擇4級頁表。
3.2.1 分頁機制的優勢
虛擬地址到物理地址的自動轉換使得下述設計目標變得現實
給每個進程分配不同的物理地址空間,防止尋址錯誤
區別頁和頁框不同,允許頁被裝入不同的頁框中,是虛擬內存機制的基本要素
每個進程有自己的頁全局目錄和頁表集合,每次進行進程上下文切換時,Linux內核把前一個進程的cr3寄存器值存入在前一個進程的進程描述符中,并載入新進程的全局目錄基址進入cr3寄存器中。
3.2.2 進程頁表
進程的虛擬內存空間被分為兩部分
用戶態尋址部分:0x00000000 ~ 0xbfffffff
內核態尋址部分:0xc0000000 ~ 0xffffffff
進程運行在用戶態時,其產生的線性地址小于 0xc0000000,在內核態則隨意
3.2.3 內核頁表
內核有自己的一組頁表,存放在主內核頁全局目錄中。主內核頁全局目錄的最高目錄項部分作為參考模型,為系統中每個普通進程對應的頁全局目錄項提供參考模型。
- END -
看完一鍵三連在看,轉發,點贊
是對文章最大的贊賞,極客重生感謝你
推薦閱讀
深入理解Linux內存子系統
如何掌握和精通一門技術
深入理解Cache工作原理
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 求點贊,在看,分享三連
總結
以上是生活随笔為你收集整理的深入理解Linux内核之内存寻址的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 2022新年重磅技术分享|深入理解Lin
- 下一篇: 如何从0搭建公司的后端技术栈