虛擬地址空間的初始化
缺頁中斷
缺頁中斷的概念
缺頁中斷(Page Fault Interrupt) 是指當程序訪問的虛擬地址在頁表中不存在有效映射(即該頁未加載到內存中)時,CPU 會發出一個中斷信號,請求操作系統加載所需的頁面到內存。
這是現代操作系統實現 虛擬內存管理 的重要機制之一。
缺頁中斷的觸發條件
缺頁中斷通常在以下情況下觸發:
- 虛擬地址對應的頁面不在內存中:
- 頁表中找不到對應的物理頁幀(頁表條目為空或無效)。
- 常見于程序訪問未加載到內存的代碼段或數據段。
- 訪問非法地址:
- 程序試圖訪問一個不存在的虛擬地址(如超出地址空間范圍)。
- 操作系統會判斷訪問是否合法,非法訪問將觸發異常。
缺頁中斷的處理流程
以下是缺頁中斷的詳細處理流程:
- 程序訪問虛擬地址:
- CPU 將虛擬地址拆分為頁號和頁內偏移量,根據頁號查詢頁表。
- 檢測頁表:
- 如果頁表中沒有找到對應的物理頁幀(即頁表項無效),觸發缺頁中斷。
- 陷入內核:
- 缺頁中斷引發陷入操作系統內核,操作系統負責處理。
- 檢查頁面是否合法:
- 操作系統檢查虛擬地址是否屬于當前進程的合法地址范圍:
- 如果地址非法(如訪問未分配的堆空間),操作系統會終止進程,拋出段錯誤(Segmentation Fault)。
- 如果合法,進入下一步。
- 操作系統檢查虛擬地址是否屬于當前進程的合法地址范圍:
- 分配頁面:
- 操作系統為該虛擬頁分配物理頁幀。
- 如果內存不足,則觸發頁面置換算法(如LRU、FIFO),將某些頁面換出到硬盤(即交換分區或頁面文件)。
- 加載頁面:
- 如果訪問的頁面是磁盤文件的一部分(如代碼或數據),則將頁面從磁盤加載到內存。
- 將分配的物理頁幀的地址填入頁表,并將頁表項標記為有效。
- 恢復程序執行:
- 操作系統返回用戶態,重新執行導致缺頁的指令。
- 由于頁面已加載到內存,訪問可以正常完成。
缺頁中斷的示例
假設一個程序試圖訪問數組元素,但數組較大,未全部加載到內存。以下是可能的情景:
int arr[100000]; // 數組在內存中未完全加載
for (int i = 0; i < 100000; i++) {arr[i] = i; // 順序訪問
}
- 程序首次訪問
arr[i]
時,CPU 查詢頁表,發現對應的虛擬頁面未映射到物理內存,觸發缺頁中斷。 - 操作系統加載對應頁面到內存,并更新頁表。
- 下一次訪問已加載的頁面時,程序可以直接讀取,無需觸發缺頁中斷。
示例總結
當一個應用程序數據過大的話,不會立即將所有的數據全部從硬盤上加載到物理內存中,會先加載一部分。但是在進程的虛擬地址空間中會將所有的數據對應的地址全部建立。于是當需要使用一個虛擬地址空間的時候會在頁表中進行查找映射的物理地址,但是沒有物理地址,還未加載進內存中。操作系統會動態加載數據,當需要的時候再申請物理空間,加載數據,然后建立映射關系。
即使加載到物理內存的數據是亂序存儲的,通過頁表的映射關系也可以進行有序的管理。
虛擬地址空間初始化
虛擬地址空間就是mm_struct
,在task_struct
中。作為對象,需要被初始化。
當一個新進程被創建(例如通過 fork
或 exec
系統調用)時,操作系統會:
- 創建虛擬地址空間:
- 為進程分配獨立的虛擬地址空間,確保不同進程之間的地址空間隔離。
- 設置頁表:
- 頁表初始狀態為“未映射”(即頁面不在物理內存中),以支持按需加載。
- 加載程序的基礎信息:
- 通過程序文件(如 ELF 文件)中的頭部信息,劃分代碼段、數據段等區域。
- 堆區和棧區只初始化元信息(如起始地址),實際分配時動態增長。
- 所以大部分會在程序加載的時候就被初始化。
為什么要有進程地址空間
將地址從無需變有序
數據從磁盤加載到物理內存是動態加載的,順序會變得無規則,甚至亂序。但是有了虛擬地址空間和頁表,虛擬地址空間中各個區域的地址是有序的,然后通過頁表進行映射,找到無序的物理內存地址,從而將物理地址進行有序管理。
地址轉換時的合法性判定
當地址轉換的時候,通過虛擬地址空間和頁表可以對地址和操作進行合法性判定,防止直接操作物理內存造成損壞,進而保護物理內存。
頁表中對于每一個映射關系也會有權限(rwx...
)存在,當進程又不合法操作的時候,操作系統會拒絕地址映射轉換,甚至殺死進程。
野指針
從操作系統層面理解野指針:
- 未初始化指針與頁表:當一個指針未初始化時,它指向的虛擬地址是隨機的。這個隨機地址很可能在頁表中沒有對應的映射項。因為正常的內存分配(如通過
malloc
、new
等操作)會由操作系統分配一段合法的虛擬地址,并在頁表中建立映射。而未初始化的指針所指向的地址沒有經過這樣的分配過程,所以在頁表中找不到對應的物理地址。 - 已釋放指針與頁表:指針所指向的內存被釋放后,操作系統會將這塊內存對應的頁表項標記為未使用或者分配給其他進程。如果繼續使用這個指針訪問內存,操作系統在查找頁表時會發現這個虛擬地址對應的頁表項已經不再有效。例如,一個動態分配的內存塊被
delete
或free
后,它所占用的虛擬地址范圍在頁表中的映射會被撤銷或者改變,再次訪問這個地址就會引發錯誤。 - 越界指針與頁表:當指針越界時,它可能會指向虛擬地址空間中未分配的區域。比如,一個數組指針越界后指向了數組之外的地址,這個地址可能超出了操作系統為該數組分配的合法虛擬地址范圍。在頁表中,這個越界的地址沒有合法的物理地址映射,因為操作系統只會在合法的內存分配范圍內建立頁表映射。
- 錯誤賦值指針與頁表:如果指針被錯誤地賦值為一個非法的地址,這個地址在頁表中很可能沒有對應的映射。因為操作系統在進行正常的內存分配時,會確保分配的虛擬地址在頁表中有合法的映射。而人為錯誤地給指針賦一個非法地址,打破了這種正常的映射關系。
查頁表失敗后,會反饋給操作系統,操作系統會處理進程,所以野指針會導致操作系統殺死進程,導致進程崩潰。
字符串常量為什么無法修改
常量字符串字面量(如<font style="color:rgb(6, 6, 7);">char* ptr = "Hello, World!"</font>
)通常被存儲在代碼段(Text Segment)中。這是因為常量字符串在程序的整個運行過程中不需要修改,將它們放在代碼段可以利用代碼段的只讀特性來保護這些字符串不被意外修改。
這就是在地址轉換的時候權限拒絕了對數據的寫操作,所以無法修改。
解耦合!
簡單回顧一下程序加載進內存的過程:
- 在虛擬地址空間申請指定大小的空間(調整區域劃分)。
- 加載程序,申請物理空間。
- 在頁表中進行虛擬地址和物理地址的映射構建。
完成后,此時的物理地址就轉化為了虛擬地址。提供給上層使用,用戶無需關心底層的物理地址是什么,物理內存中是如何加載的,只是使用虛擬地址就可以了。
作為進程也只是關心對于虛擬地址的使用,而不關心實際物理內存的存儲。
如圖所示。task_struct
和其中管理的mm_struct
所形成的關系對于進程來說只是負責進程的調度,而不關心如何管理調度的數據存儲和加載。
而對于物理內存部分來說,只進行內存的管理,加載物理內存。
二者通過頁表的映射關系來解耦合,讓進程管理和內存管理進行一定程度的解耦合!
Tips
- 可以不加載代碼和數據,只加載
task_struct
,mm_struct
(只拿到main
代碼的起始地址),頁表。
CPU在拿到起始地址后,當訪問虛擬地址時,會有標識證明沒有加載過物理內存,所以會缺頁中斷,然后開始加載物理內存。
- 創建進程的時候,先有
task_struct
,mm_struct
等,還是先加載代碼和數據?
先有內核數據結構,然后再陸續加載物理內存。
- 如何理解進程掛起?
進程進入掛起狀態時,操作系統找到對應的進程,清空頁表的物理地址部分,將物理地址對應的數據全部換入磁盤swap
分區。只保留虛擬地址空間中的虛擬地址和頁表的虛擬地址部分。
當掛起狀態結束時,將swap
分區的數據全部換出加載到物理內存中,然后再頁表中建立映射。這就是解耦的好處,將進程調度與內存管理完全解耦。
vm_area_struct
對于在程序中動態申請的空間來說,一般會申請在堆區中。但是在程序中可能會頻繁的申請,難道對于虛擬地址空間中的堆區來說,不只有一個起始地址嗎?可能是離散分布的嗎?
虛擬空間的組織?式有兩種:
- 當虛擬區較少時采取單鏈表,由mmap指針指向這個鏈表;
- 當虛擬區間多時采取紅?樹進?管理,由mm_rb指向這棵樹。
mm_struct
并不是直接就是整個虛擬地址空間,而是包含了一個指向虛擬內存區域(VMA)列表的指針,這個列表是由 vm_area_struct
組成的。每個 vm_area_struct
表示進程地址空間中的一個連續區域,具有相同的權限和映射類型。所以離散申請的堆空間可以用vm_area_struct
進行管理,并且所有的區域都可以統一使用vm_area_struct
進行管理。
為了高效查找區域,所以用紅黑樹來進行管理——
struct rb_root mm_rb
。
struct mm_struct {// ... 其他成員 ...struct vm_area_struct *mmap; // 指向虛擬內存區域列表的指針struct rb_root mm_rb; // 紅黑樹,用于快速查找虛擬內存區域// ... 其他成員 ...
};
struct vm_area_struct {struct mm_struct *vm_mm; // 指向所屬進程的mm_structunsigned long vm_start; // 虛擬內存區域的起始地址unsigned long vm_end; // 虛擬內存區域的結束地址pgprot_t vm_page_prot; // 頁面保護標志unsigned long vm_flags; // 標志位,如VM_READ, VM_WRITE等// ... 其他成員,如鏈表指針、紅黑樹節點等 ...
};
因為存在vm_area_struct
這個數據結構,即使堆區頻繁申請,但是每一段申請的空間都可以使用vm_area_struct
來進行管理,最后用建表進行管理所有的vm_area_struct
,在mm_struct
中用*mmap
作為鏈表的頭指針。
進程地址空間管理(總結)
關于進程地址空間整體的管理結構如上圖所示(虛擬區間較少情況下)。
task_struct
管理整體進程,其中包括管理進程地址空間的mm_struct
。在虛擬區間較少的情況下用在mm_struct
中的vm_area_struct *mmap;
指向由vm_area_struct
鏈接而成的鏈表。每個vm_area_struct
對應一段虛擬區間,而對于堆這種可能頻繁申請的來說,堆這個區間會由多個vm_area_struct
組成,而其他的區域使用一個vm_area_struct
管理即可。
為什么要有進程地址空間(總結)
在早期的計算機中,程序直接操作物理內存,所有內存地址都是物理地址。當同時運行多個程序時,內存管理必須確保程序使用的內存總量小于物理內存。然而,這種直接操作物理內存的方式存在以下問題:
安全風險
- 直接操作物理內存允許每個進程訪問任意內存空間。
- 惡意程序(如木馬病毒)可以隨意修改系統內存區域,導致設備癱瘓。
地址不確定性
- 編譯完成的程序存儲在硬盤上,運行時需加載到內存。
- 由于內存分配動態變化,程序的物理內存地址在不同運行時可能不同。
- 例如:第一次運行時,程序加載到地址
0x00000000
; - 第二次運行時,內存已被占用,加載地址可能變為其他位置。
- 例如:第一次運行時,程序加載到地址
效率低下
- 使用物理內存時,進程以整體內存塊操作。
- 當物理內存不足時,將進程從內存拷貝到磁盤(交換分區)需要拷貝整個進程,耗時較長,效率低下。
虛擬地址空間與分頁機制的優勢
1. 內存安全
- 地址空間和頁表由操作系統創建和維護。
- 所有地址空間和頁表映射必須經過操作系統監管。
- 保護物理內存中的合法數據,防止非法訪問。
2. 地址靈活性
- 地址空間和頁表映射機制允許物理內存中的數據以任意位置加載。
- 內存分配與進程管理解耦,進程不再直接依賴物理內存地址。
3. 延遲分配
- 在C/C++中,通過
new
或malloc
申請的空間實際上分配在虛擬地址空間。 - 物理內存分配延遲到真正訪問內存時執行。
- 操作系統自動完成內存分配和頁表映射,用戶或進程無需感知。
4. 提高效率
- 程序在物理內存中的加載可以是任意位置,通過頁表實現虛擬地址與物理地址的映射。
- 虛擬地址空間使得進程內存分布在邏輯上保持有序,簡化了程序管理。
總結
虛擬地址空間通過操作系統的地址空間管理和頁表機制,解決了直接操作物理內存帶來的安全性、靈活性和效率問題,使得內存管理更安全、更高效,同時簡化了程序開發與運行。