虛擬內存技術
虛擬內存技術是操作系統(tǒng)實現(xiàn)的一種高效的物理內存管理方式,具有以下作用:
- 使得進程間彼此隔離 :通過將物理內存和虛擬地址空間聯(lián)系起來,并將虛擬地址空間與進程一一對應,每個進程都認為自己擁有了整個物理內存,使得進程之間彼此隔離。這讓操作系統(tǒng)在運行多個進程的同時也保障了內存訪問的安全。
- 使得進程共享物理內存 :將物理內存進行虛擬化后,多個進程可以同時共享同一塊物理內存。
- 提升物理內存利用率 :有了虛擬地址空間后,操作系統(tǒng)只需要將進程當前正在使用的部分數(shù)據或指令加載入物理內存,沒有在使用的部分數(shù)據或指令則可以存儲在外存中,而不需要將整個進程的數(shù)據或指令全部載入物理內存。這使得操作系統(tǒng)可以在相對較小的物理內存下運行更多的進程,從而提升了物理內存的利用率。
- 方便內存管理 :虛擬內存技術將物理內存分割成若干塊,每塊都可以分配給不同的進程使用,這使得操作系統(tǒng)可以更加靈活地管理內存。如物理內存分配、回收等。
頁式內存管理技術
頁式內存管理技術是 Linux 實現(xiàn)的一種虛擬內存技術,其基本思想是將物理內存和虛擬內存都分割成多個固定大小的 Page(頁),然后對這些 Pages 進行編址,并通過 Page Table(頁表)將它們一一映射起來。當 CPU 訪問虛擬地址空間時,Linux 會通過 Page Table 將虛擬地址轉換為物理地址。
- Virtual Address(虛擬地址) :操作系統(tǒng)和應用程序使用的虛擬內存地址。
- Physical Address(物理地址) :實際的物理內存地址。
Linux 通過頁式內存管理技術,除了能夠高效地管理物理內存之外,還提供了許多額外的虛擬內存功能,例如:進程隔離、內存保護、共享物理內存等。
虛擬地址格式與頁表(32bit 系統(tǒng))
在 x86 32bit Linux 系統(tǒng)中,虛擬地址(也稱為線性地址,Linear Address)的格式由 3 部分組成,總長度為 32bit,尋址范圍為 2^32,最大可描述空間為 4G。
- Page Table Directory(10bit)
- Page Table Entry(10bit)
- Offset(12bit)
在 Kernel 中用于對虛擬地址進行尋址的數(shù)據結構稱為 Kernel Page Table(內核頁表),包括:
- 頁表目錄(Page Directory):可包含 1024 個目錄項。
- 目錄項(Directory Entry):每個目錄項指向一個頁表,即有 1024 個頁表。
- 頁表(Page Table):大小為 4KB,頁表項的大小為 4B,即一個頁表可包含 1024 個頁表項。
- 頁表項(Page Table Entry):每個頁表項指向一個頁。
- 屬性標記:用于將每個頁表項標記為只讀、可寫、只執(zhí)行等,以控制內存的訪問權限和緩存行為。
可見,32bit 系統(tǒng)中的 2 級頁表結構,最多可以映射 1024*1024 個 Pages。而對于大于 4GB 的物理地址空間,則需要使用多級級頁表結構,以支持更大的物理內存空間。
虛擬地址格式與頁表(64bit 系統(tǒng))
在 x86 64bit 系統(tǒng)中,可以描述的最長地址空間為 2^64(16EB),遠遠超過了目前主流內存卡的規(guī)格,所以在 Linux 中只使用了 48bit 長度,尋址空間為 2^48(256TB),User Space 和 Kernel Space 各占 128T。尋址空間分別為:
- User Space :0x0000 0000 0000 0000~0x0000 7FFF FFFF F000,高 16bit 全 0。
- Canonical Address Space :0x0000 7FFF FFFF F000 - 0xFFFF 8000 0000 0000,無效地址空間。
- Kernel Space :0xFFFF 8000 0000 0000~0xFFFF FFFF FFFF FFFF,高 16bit 全 1。
由于內存空間的擴大,x86 64bit 系統(tǒng)中的虛擬地址格式由 5 部分組成,占 64bit 中的 48bit。相應的,Linux Kernel 在 v2.6.10 中實現(xiàn)了四級頁表,后來又在 v4.11 中引入了五級的頁表結構。
就四級頁表而言,虛擬內存空間被劃分成了 4 個層次結構,每一級都有一個頁表來記錄該層次的映射關系。
- PGD(Page Global Directory,全局頁目錄)
- PUD(Page Upper Directory,上級頁目錄)
- PMD(Page Middle Directory,中間頁目錄)
- PTE(Page Table Entry,頁表項)
當 CPU 需要訪問一個虛擬地址時,會執(zhí)行以下頁表遍歷流程:
- 首先根據虛擬地址的 GLOBAL DIR(9bit) ,確定在 PGD 中的頁表項,開始尋址 PUD。
- 再根據虛擬地址中的 UPPER DIR(9bit) ,確定 PUD 中的頁表項,開始尋址 PMD。
- 再根據虛擬地址中的 MIDDLE DIR(9bit) ,確定 PMD 中的頁表項,開始尋址 PTE。
- 再根據虛擬地址中的 TABLE ID(9bit) ,確定 PTE 中的頁表項,開始尋址 Physical Page(物理內存頁框)。
- 最后根據虛擬地址中的 OFFSET(12bit) ,確定 Physical Page 中的 Page Lane(頁條)。
在頁表遍歷的過程中,如果找到對應的物理頁框,則可以進行對應的內存讀寫操作。反之,如果遇到了尋址失敗的情況,則說明對應的物理頁框沒有被分配或者被換出到外存了,此時需要進行相應的頁表調度和頁表交換操作。
四級頁表的優(yōu)點是它可以映射非常大的虛擬內存空間,并且每個進程的頁表都是獨立的,相互干擾。缺點是每次訪問內存都需要遍歷四級頁表,這會導致一定的性能損失。并且,當 Linux 設定的虛擬頁大小越小時,單個進程中的頁表項和虛擬頁也就越多,頁表的層級也可能越多,查詢性能就越低。同時也需要注意,頁面并非是越大越好,因為過大的頁面會造成內存碎片,降低了內存的利用率。
因此,Linux 采用了一些優(yōu)化措施,如 TLB(Translation Look-aside Buffer)緩存等,來加速頁表遍歷的過程。
CPU MMU 虛實地址轉換
Linux 虛實地址轉換功能,除了需要由 Kernel 實現(xiàn)的內核頁表(Page Table)數(shù)據結構之外,還需要硬件層面的 CPU MMU(Memory Management Unit,存儲管理單元)支持。
MMU(Memory Management Unit,內存管理單元)內嵌在 CPU 芯片上,它是一個專用的硬件,利用存放在 Main Memory 中的 Page Table 來輔助完成虛實地址之間的動態(tài)翻譯,而 Page Table 的內容就交由 Kernel 來統(tǒng)一管理。
當 CPU 訪問虛擬地址時,MMU 首先將虛擬地址的高位部分作為頁表的索引,查找對應的頁表項。頁表項中存儲了與虛擬頁對應的物理頁的起始地址以及一些標志位,如是否可讀、可寫等。然后,MMU 將虛擬地址的低位部分作為偏移量,加上物理頁的起始地址,得到實際的物理地址。
TLS 快表轉換
TLB(Translation Look-aside Buffer,翻譯旁路緩沖器)同樣是內嵌在 CPU 芯片上的一個專用硬件,作為緩存,旁掛在 MMU 上,緩存了最近訪問過的虛擬地址與物理地址之間的映射關系,以便在下次訪問時快速地進行翻譯。TLB 的空間非常有限,一般只可緩存幾十個到數(shù)百個條目。
通過 TLB,操作系統(tǒng)可以旁路掉多級頁表遍歷的流程,只需要在 TLB 中執(zhí)行一次高速訪問即可,前提是沒有 TLB Miss(緩存失效)。如果 Miss 的話,就會回到常規(guī)的頁表遍歷流程,然后再利用局部性原理去更新 TLB。
虛擬地址空間與 CPU 運行模式
為了保障多任務實時操作系統(tǒng)運行的安全性和穩(wěn)定性,Intel x86 CPU 提供了 Ring0-3 這 4 種不同的運行模式,而 Linux 只使用了其中的 Ring0(特權指令模式)和 Ring3(非特權指令模式),為虛擬地址空間提供了 2 級保護機制。
相應的,在 32bit Linux 系統(tǒng)中,大小為 4G 的虛擬地址空間,被分成了 2 個部分:
- User Space(0x0~0xBFFF FFFF,0~3G) :每個 User Process 都有自己的 User Space 且互相隔離。運行在 CPU Ring3(用戶模式)模式,User Process 的代碼被限制了可以執(zhí)行的操作以及可以訪問的資源范圍;
- Kernel Space(0xC000 0000~0xFFF FFFF,3~4G) :屬于 Kernel 的 Kernel Space。運行在 CPU Ring0(內核模式)模式,Kernel 代碼沒有被限制,可以執(zhí)行任何操作并且可以訪問任何資源。
所以,Linux 中的 Page Table 也可以被分為 2 種:
- 內核頁表區(qū) :用于 Kernel Space 高端內存映射區(qū)與物理地址空間之間的映射。
- 進程頁表區(qū) :每個 User Process 擁有自己的頁表,用于進程虛擬地址空間與物理地址空間之間的映射。
另外,User Space 可以通過 SCI(系統(tǒng)調用接口)來訪問或操作 Kernel Space 的代碼和數(shù)據,同時也會觸發(fā) CPU 運行模式的切換。例如:C 標準庫中的 malloc() 函數(shù)底層調用了 sbrk() 或 brk() SCI 來分配堆內存;printf() 函數(shù)底層調用了 wirte() SCI 來輸出字符串等等。
虛擬地址空間的布局(32bit 系統(tǒng))
從上圖可以看出 Linux 對虛擬地址空間作了復雜的分段布局,主要是為了實現(xiàn)更加高效的內存管理和保護機制。
以 User Space 為例:
- 持久化的程序流代碼(順序程序流、條件程序流、循環(huán)程序流)存儲在 Test Segment 中。
- 持久化的、且初始化的常量、全局變量、靜態(tài)變量(包括靜態(tài)全局變量、靜態(tài)局部變量)存儲在 Data Segment 中。
- 持久化的、但未初始化的全局變量、靜態(tài)變量(包括靜態(tài)全局變量、靜態(tài)局部變量)存儲在 BSS Segment 中。
- 臨時的函數(shù)局部變量存儲在 Stack Segment 中。
- 臨時由用戶自主申請的數(shù)據存儲 Heap Segment 或 MMAP Segment 中。
劃分不同的存儲空間更有助于針對不同的數(shù)據內容進行合理的訪問和存儲規(guī)劃。例如:
- 提高 CPU Cache 利用率 :將指令區(qū)、持久數(shù)據區(qū)、動態(tài)數(shù)據區(qū)進行分離,有利于應用局部性原理來發(fā)揮出 CPU Instruction Cache(指令緩存)和 Data Cache(數(shù)據緩存)的優(yōu)勢。
- 節(jié)省內存空間 :如果系統(tǒng)中運行多個該程序的副本時,指令區(qū)中的 Read Only 數(shù)據可被共享,物理內存實際上只需要存儲一份。
User Space
在 User Space 中,每個 User Process 都有一個 task_struct(進程描述符)。
struct task_struct {
pid_t pid; // User Process ID
pid_t tgid; // Kernel Thread ID
struct files_struct *files; // 文件描述符
struct mm_struct *mm; // 內存映射描述符
...
}
其中,除了 Environment Variables(程序運行時環(huán)境變量)和 Command-line arguments(程序運行指令行參數(shù))之外,進程虛擬地址空間的內存布局都通過 mm_struct 結構體來進行描述。
Stack Segment(用戶棧)
User Process 下屬的每個 User Thread 都有屬于自己的用戶線程棧。主要用于存儲以下信息:
- 存儲函數(shù)調用信息(Procedure Activation Record,過程活動記錄)或棧幀(Stack Frame)。
- 存儲函數(shù)內部的非靜態(tài)(Non-static)變量;
- 提供臨時存儲區(qū),使用 C 標準庫 alloca() 函數(shù)可動態(tài)申請棧內內存。
Stack Segment 的空間具有 “靜態(tài)分配" 和 “動態(tài)分配” 這 2 種使用形勢。其中,靜態(tài)分配由 C 編譯器自動分配和管理,主要應用在函數(shù)處理流程。而動態(tài)分配則由程序通過 alloca() 函數(shù)主動申請和釋放。
例如:在一次函數(shù)調用中,C 編譯器依次入棧的數(shù)據包括:
- 主調函數(shù)下一條語句(指令);
- 被調函數(shù)的返回值地址;
- 被調函數(shù)的實際參數(shù);
- 被調函數(shù)局部變量等。
通過先進后出(FILO)的數(shù)據結構,使得被調函數(shù)退出后,可以繼續(xù)執(zhí)行主調函數(shù)的語句。
Stack Segment 是一塊連續(xù)的空間,運行時大小可以由 Kernel 動態(tài)調整(向下增長),且最大容量 RLIMIT_STACK(8M)由系統(tǒng)預先定義,用戶也可以通過 ulimit -s 指令來查看和設定棧的最大值。
$ ulimit -s
8192
當程序入棧數(shù)據超出容量之后,就會觸發(fā) Stack Overflow(溢出)錯誤,此時程序收到一個 Segmentation Fault(段錯誤)異常。
函數(shù)調用棧的工作原理
程序每執(zhí)行一次函數(shù)調用都會在 Stack 中生成一個棧幀(Stack Frame),對應著一個未運行完的主調函數(shù),用于存儲被調函數(shù)的執(zhí)行環(huán)境信息,包括:函數(shù)實際參數(shù)、函數(shù)局部變量、函數(shù)返回值地址等等。
棧幀主要通過兩個指針寄存器來實現(xiàn):
- ebp(幀指針) :指向幀底,作為基址指針,不會移動。
- esp(棧指針) :指向棧頂,可以移動,通過移動 esp 來訪問棧幀中的數(shù)據。
ebp 到 esp 之間的地址空間就是用于存儲當前被調函數(shù)執(zhí)行環(huán)境信息的空間。
另外,Stack Segment 很可能會同時存在多個棧幀(函數(shù)嵌套調用),此時多個棧幀會根據函數(shù)調用順序在 Stack Segment 中先入后出。
例如:雖然 esp 會隨著當前函數(shù)的入棧和出棧而不斷移動,但由于 ebp 的存在,所以當前函數(shù)棧幀的邊界始終是清晰的。當被調函數(shù)退出后,ebp 就會跳到主函數(shù)棧幀的底部,esp 也會隨其自然的來到主函數(shù)棧幀的頭部。
Memory Mapping Segment(內存映射段)
Memory Mapping Segment(內存映射段)的空間通過 mmap() SCI(系統(tǒng)調用接口)來使用,用于將外存(e.g. 硬盤)中的一個文件、或一段物理內存直接映射到 Memory Mapping Segment 中,而后 User Process 就可以采用指針的方式來訪問一段內存,而不必再調用 read() / write() 等 SCI。mmap() 是一種高效的 I/O 方式。
Memory Mapping Segment 主要有 2 類應用場景:
- File mappings(文件映射) :在程序裝載過程中,將程序所需要 #include 的 .so 動態(tài)共享庫文件(Dynamic share libraries)加載到 Memory Mapping Segment 內存空間。
- Anonymous mappings(匿名內存映射) :C 標準庫 malloc() 函數(shù)的底層實現(xiàn)方式之一就是對大于 MMAP_THRESHOLD(默認為 128KB)的空間申請,會調用 mmap() SCI 從 Memory Mapping Segment 中分配,而不是調用 sbrk() 或 brk() SCI 從 Heap 申請。
Memory Mapping Segment 的空間大小同樣可以由 Kernel 動態(tài)調整(向上增長)。
Heap Segment(運行時堆)
Heap Segment(運行時堆)的空間由程序自行使用,包括分配和釋放。例如:開發(fā)者可通過 C 標準庫 malloc() 函數(shù)申請并返回 void*(無類型指針),且無名稱,只能通過指針訪問。
在 Kernel 層面通過堆管理器來管理 Heap Segment 的空間。堆管理器通過鏈表存儲結構來記錄 Heap Segment 空間的使用情況,記錄了包括:空閑的內存地址、已使用的內存地址等。
當程序申請一塊內存時,堆管理器會遍歷鏈表尋找第一個空間大于所申請空間的節(jié)點,并返回地址給程序,然后將該節(jié)點從空閑鏈表中刪除。所以 Heap 空間中的多個內存塊之間很可能是不連續(xù)的。
當目前的 Heap Segment 已經沒有足夠的空間時(可能由于內存碎片太多導致的),那么堆管理器可能會通過 brk() 或 sbrk() SCI 進行動態(tài)調整(向上增長),實際上是通過調整 Heap Segment 末端的 break 指針來實現(xiàn)。
Heap Segment 的空間總大小受到 CPU 架構和操作系統(tǒng)位數(shù)影響,例如:32bit 架構的 Heap Segment 最大可達 2.9G 空間。
應用程序裝載與數(shù)據段
當開發(fā)者經過編碼、編譯、匯編、鏈接一個 C 程序后就得到了一個可執(zhí)行程序的文件。然后,就需要通過程序裝載器(Loader)將可執(zhí)行文件加載到 User Space 中并啟動一個 User Process。在 Linux 上,可執(zhí)行文件采用的是 ELF(Executable and Linkable File Format,可執(zhí)行與可鏈接文件格式)格式。
ELF 文件由 4 部分組成,分別是:
- ELF Header
- Program Header Table(程序頭表)
- Sections(節(jié))
- Section Header Table(節(jié)頭表)
其中,位于 Program Header Table 和 Section Header Table 之間的都是 Sections,這些 Sections 中的數(shù)據會在程序啟動時被加載到相應的進程虛擬地址空間中。
關鍵的 Sections 包括以下幾個:
- .bss :存儲未初始化的全局變量和靜態(tài)變量。
- .data :存儲已初始化的全局變量和靜態(tài)變量。
- .rodata :存儲只讀數(shù)據(e.g. 常量)。
- .text :存儲已編譯程序的機器指令代碼。
- .debug :調試符號表,調試器用此段的信息幫助調試。
數(shù)據段(BSS Segment 和 Data Segment)
BSS Segment 和 Data Segment 常被合并稱為 “數(shù)據段”,都用于存儲全局變量和靜態(tài)變量,區(qū)別于存儲在 Stack Segment 中的函數(shù)局部變量。
- BSS(Block Started by Symbol,未初始化的數(shù)據段) :可讀寫,主要存儲了從 ELF .bss section 中加載的數(shù)據,包括:1)已定義,但未初始化的全局變量和靜態(tài)變量;2)已定義,且初始值為 0 的全局變量和靜態(tài)變量,例如 C 編譯器中的空指針。
- Data Segment(已初始化的數(shù)據段) :可讀寫,主要存儲了從 ELF .data section 中加載的數(shù)據,包括:已定義、且已初始化、且初值不為 0 的全局變量、靜態(tài)變量和常量。所以,Data Segment 也被稱為 “靜態(tài)存儲區(qū)(Static data area)”。
ELF .bss section 的特別之處在于沒有具體的數(shù)值,所以只需要記錄下全局變量和靜態(tài)變量所需要的內存空間大小即可,但并不會分配真實的內存空間,即:只記錄了全局變量和靜態(tài)變量在虛擬地址空間中的開始和結束地址。
當程序加載器(Loader) 將 ELF .bss section 加載到 BSS Segment 后,這些數(shù)據會被 C 編譯器自動的初始化為 0 或 NULL。這樣可以有效的減少了 C object file 的體積。
例如:對于 int arr0[10000] = {1, 2, 3, …}
和 int ar1[10000]
這兩個數(shù)組而言:
- arr0 存儲在 Data Segment,記錄了每個數(shù)組元素的數(shù)值。
- arr1 存儲在 BSS Segment,只記錄了 arr1 的起始和結束地址,直到程序啟動時才被編譯器在相應的虛擬地址空間中刷 0。顯著的減少了可執(zhí)行文件的大小。
Text Segment(代碼段)
Text Segment(代碼段)主要存儲了從 ELF .text section 中加載的機器指令。
Text Segment 中的數(shù)據只能讀不能寫,但可以被執(zhí)行,即:Text Segment 中的數(shù)據是可共享的,可以被其他的進程執(zhí)行。例如:機器中有數(shù)個進程運行相同的一個程序,那么它們就可以使用同一個代碼段。
可見,User Space 劃分了明確的 “數(shù)據區(qū)” 和 “指令區(qū)",且數(shù)據區(qū)對于進程而言可讀寫,而指令區(qū)對于進程只讀,以防止程序指令被誤改。
內存缺頁中斷
基于 Linux 虛擬內存管理技術,每個 User Process 都擁有自己獨立的虛擬地址空間,當一個 User Process 被 Kernel 加載并運行時,無需要一次性將 User Process 所有數(shù)據都加載到 Main Memory 中,而是當通過 Page Table 缺頁中斷的方式來動態(tài)加載。
虛擬地址的頁表遍歷過程中,當訪問到某個頁面時,通過頁表項中的有效位,可以得知此頁面是否在內存中,如果不存在,則通過缺頁異常,將磁盤對應的數(shù)據拷貝到內存中,如果沒有空閑內存,則選擇犧牲頁面,替換掉其他頁面。在這個時候,被內存映射的文件實際上成了一個分頁交換文件。
Kernel Space
Kernel Space 與物理地址空間的映射關系
區(qū)別于 User Space 只擁有虛擬地址空間。Kernel Space 除了虛擬地址空間之外,還直接擁有一部分的物理地址空間。也就是說 Kernel Space 具有 2 種地址映射關系,如下圖所示。
- 直接映射(Linear Mapped) :Kernel Space 虛擬地址空間中的 3G~3G+896M 與物理地址空間的 0~896M 直接一對一映射,擁有最高的效率。
- 動態(tài)映射(Dynamic Mapped) :Kernel Space 虛擬地址空間中的高端內存映射區(qū)(0xF800 0000 ~ 0xFFFF FFFF)動態(tài)的與物理地址空間 896M~4G 中的某塊物理頁面建立映射(通過 Page Table),即:在有需要的時候才建立映射,待使用完之后就釋放映射,以供其它物理頁面映射。
物理地址空間布局
基于不同的用途,Linux 將物理內存劃分為 3 個 ZONEs,從地址低到高為:
- ZONE_DMA(0~16M) :是 Kernel 直接映射的物理地址空間。
- ZONE_NORMAL(16M~896M) :是 Kernel 直接映射的物理地址空間,Kernel 將需要頻繁使用的數(shù)據存放于此。
- ZONE_HIGHMEM(896M~4G) :是 Kernel 動態(tài)映射到物理地址空間,Kernel 將不常用數(shù)據存放于此,只在要訪問這些數(shù)據時才建立映射關系。
最終,通過結合兩種映射方式,Linux Kernel 可以完全接管整個 4G 物理內存空間。如下圖所示,藍色區(qū)域為直接映射空間,綠色區(qū)域為動態(tài)映射空間,棕色區(qū)域為動態(tài)映射頁面。
物理直接映射區(qū)
根據 User Process 對 Kernel Space 訪問權限的不同,還可以將 Kernel Space 分為 “進程私有” 和 “進程共享” 這 2 塊區(qū)域。
- 進程私有區(qū)域 :每個進程都有單獨的內核棧、頁表、task 結構以及 mem_map 結構等。
- 進程共享區(qū)域 :屬于所有進程共享的內存區(qū)域,包括:物理存儲器、內核數(shù)據和內核代碼區(qū)域。
Kernel Space 中的物理直接映射區(qū),屬于 “進程共享區(qū)域",是為了讓 Kernel Space 或者 User Space 可以直接訪問某些特殊的物理內存區(qū)域。這些物理內存區(qū)域包括:
- ZONE_DMA :NIC、GPU 此類 I/O 外設所提供的 DMA Controller 只能對內存的前 16M 進行尋址。為了方便設備驅動程序的實現(xiàn),Linux 通過物理直接映射區(qū),使得 Kernel 和 User Process 可以直接訪問這些外設的存儲器。此外,在 ZONE_DMA 中還劃分了用于 BIOS ROM 和 VGA 適配器的區(qū)域,地址為 640K~1M 。
- ZONE_NORMAL :常規(guī)內存區(qū)域,沒有特殊的使用限制,主要用于 User Process 和 Kernel 之間的交互,以及作為文件緩存加快文件系統(tǒng)的訪問速度,避免頻繁地讀寫磁盤。例如:存放 Kernel Image(內核代碼)、mem_map 數(shù)組等數(shù)據。
可見,物理直接映射區(qū)使得 Kernel 和 User Process 得以更方便地訪問一些特殊的物理內存區(qū)域,從而簡化了操作系統(tǒng)和設備驅動程序的編寫。
DMA 直接內存訪問
DMA(Direct Memory Access,直接內存訪問)指的是主機的 I/O 外設對 Main Memory 的直接訪問。有了 DMA 機制之后,外設跟主存之間的數(shù)據交互主要由 DMA Controller 來完成的,從而避免了 CPU(包括 MMU)的參與。
高端內存映射區(qū)
物理內存中的 ZONE_NORMAL(高端內存區(qū)域)大小為 4G - 896M = 3200M,遠遠大于 Kernel Space 剩余的 1G - 896M = 128M 虛擬地址空間。所以 Kernel 對 ZONE_NORMAL 的訪問需要采用動態(tài)映射的方式。
Kernel Space 中的 128M 統(tǒng)稱為 “高端內存映射區(qū)”,主要有以下 3 個部分組成:
- Fixing Kernel Mapping(固定映射區(qū))/ Temporary Kernel Mapping(臨時映射區(qū))
- Persistent Kernel Mapping(持久映射區(qū))
- Vmalloc Area / Loremap Area(動態(tài)映射區(qū))
Fixing Kernel Mapping(固定映射區(qū))/ Temporary Kernel Mapping(臨時映射區(qū))
Fixing Kernel Mapping(固定映射區(qū))通常用于靜態(tài)分配內存,例如:驅動程序需要一段固定大小的內存來存儲數(shù)據結構或緩沖區(qū),它可以使用 Fixing Kernel Mapping 將一段物理內存空間映射到內核虛擬地址空間中,并在整個生命周期中保持映射關系。
Temporary Kernel Mapping(臨時映射區(qū))通常用于動態(tài)分配內存,例如:驅動程序需要在運行時創(chuàng)建一個臨時緩沖區(qū)來存儲數(shù)據,它可以使用 Temporary Kernel Mapping 來創(chuàng)建一個臨時的虛擬內存區(qū)域,映射到物理內存空間中,并在使用完畢后釋放虛擬內存空間。
Persistent Kernel Mapping(持久映射區(qū))
Persistent Kernel Mapping(持久映射區(qū))用于在 Kernel Space 中維護一組持久性的映射關系。這些映射通常是針對硬件設備或驅動程序的,允許 Kernel 直接訪問這些設備或驅動程序的存儲器區(qū)域。例如:用于 PCI I/O 外設進行內存映射的區(qū)域,大小由 PCI 規(guī)范決定。
Vmalloc Area(動態(tài)映射區(qū))
Vmalloc Area(動態(tài)映射區(qū))用于 Kernel 動態(tài)分配內存,例如:當 Kernel 需要訪問 I/O 外設的存儲空間時,就會使用 ioremap() SCI 將位于物理地址空間中的 MMIO 內存映射到 Kernel Space 的 vmalloc area 中,并在使用完之后釋放映射關系。
vmalloc() 和 kmalloc() 內存分配函數(shù)
vmalloc() 和 kmalloc() 函數(shù)都用于從 Kernel Space 中申請內存,但兩者有很大的不同:
- vmalloc() 用于從動態(tài)內存映射區(qū)申請非連續(xù)的物理內存(通過內核頁表映射),可以最大限度的使用高端物理內存空間。通常應用于為活動的 Swap 交換分區(qū)分配數(shù)據結構、或為某些 I/O 驅動程序分配緩沖區(qū)、或為內核模塊分配空間。
- kmalloc() 用于從直接內存映射區(qū)申請連續(xù)的物理內存(通過 Slab 分配器分配)。
與 User Space 中的 malloc() 不同,在 Kernel Space 進行內存申請是直接分配的,區(qū)別于 User Space 的延遲分配(通過缺頁機制來反饋)方式。一旦 vmalloc() 和 kmalloc() 申請內存,那么 Kernel 就必須立刻滿足。
評論
查看更多