一、課程核心主題引入
這一講,我要給大家講的是真正的內存管理,也就是段和頁結合在一起的內存管理方式。之前提到過,我們先學習了分段管理內存的工作原理,知道操作系統采用分段的方式,讓用戶程序能以分段的結構進行編寫;后來又學習了分頁管理內存,明白操作系統在管理物理內存時,通過分頁機制更高效地利用內存。分段對用戶和應用程序友好,分頁對物理內存管理高效,所以這兩種機制必須結合起來。
這一講的關鍵,就在于探究段和頁這兩種機制如何結合,以及結合后實際的物理內存管理是什么樣的。程序員希望用段來編寫程序,物理內存希望用頁來進行管理,而操作系統作為中間橋梁,既要讓上層用戶滿意,又要高效管理下層物理內存資源,所以必須將分段和分頁兩種機制融合。接下來,我先給大家講講段頁結合的思路,再深入探討這種結合方式的具體實現。
二、段頁結合的思路解析
- 回顧分段與分頁工作原理
- 先來回顧一下分段是怎么工作的。想象一下,有一個程序和一塊內存,我們在內存中采用分區的方法,根據程序分段的數量劃分出相應的區域。比如程序分成兩段,就割出兩個區域,然后將用戶程序中的段和這些區域建立映射,把代碼段放在一個區域,數據段放在另一個區域,這樣應用程序就放到內存的段中了。
- 再看看分頁的工作方式。物理內存會被打散成一頁一頁固定大小的片,我們的程序同樣也會被打散。假設程序有兩個段,將這兩個段也打散成若干頁,然后把這些頁映射到物理內存的空閑頁上,分頁機制的工作就完成了。
- 段頁結合的具體方式
-
現在要把段和頁結合起來,該怎么做呢?仔細觀察分段和分頁的工作過程圖,我們可以發現一個巧妙的方法。把程序中的段先映射到一個類似物理內存的地方,但它不是真正的物理內存,只是一個地址空間。這個地址空間可以劃分成一塊一塊的,就像物理內存一樣,不過劃分出來的只是地址,還不能直接訪問真實的物理內存,我們把它叫做虛擬內存。
-
具體來說,從應用程序角度出發,在虛擬內存給定的地址空間上,割出一個區域給程序中的段,這樣就實現了對段的支持,段有了對應的映射。但是虛擬內存本身不能直接使用,它只是一個虛擬的地址空間,所以還需要把虛擬內存中的段再分割成固定大小的頁,然后將每頁映射到物理內存上。經過這兩次映射,就形成了既有段又有頁的內存管理模式。
-
從用戶的角度看,程序對應到虛擬內存上,感覺就像在使用段,他們不用關心底層的復雜映射過程,只知道自己的程序段在一段地址空間上,并且可以像使用段一樣連續地進行尋址訪問。而從物理內存的角度,是把虛擬內存中的段映射到物理內存頁上,實現了分頁管理。通過引入虛擬內存這個概念,完美地將段和頁結合在了一起,這就是段頁式內存管理的核心輪廓。
-
三、段頁同時存在時的重定位過程
-
重定位的必要性
我們已經知道段頁是如何結合工作的了,但要讓程序在內存中順利運行,還需要進行重定位,也就是地址翻譯。因為用戶發出的邏輯地址,像程序中使用的段號加上段的偏移地址,需要經過轉換,才能找到真正的物理地址,這樣程序才能在內存中正確地取指執行。 -
重定位的具體步驟
- 當用戶發出邏輯地址(段號 + 段偏移)時,首先要根據段表找到一個地址,這個地址是虛擬地址。在只有分段的情況下,這個地址就是物理內存地址,但在段頁結合的模式下,它還不是真正的物理地址,只是虛擬內存中的地址。
- 得到虛擬地址后,操作系統還要根據分頁機制再進行一次映射。通過虛擬地址算出頁號,再結合頁內偏移,組合形成物理地址。最后,操作系統把這個物理地址發送到地址總線上,程序就能訪問到真正的物理內存單元,取出數據或執行指令了。整個過程需要經過兩層地址翻譯,第一層基于分段,第二層基于分頁,這樣既支持了分段,又支持了分頁,用戶感覺自己在使用段,而物理內存則按照頁的方式進行管理和分配。
四、段頁式內存下程序載入內存的過程
-
程序載入內存的總體步驟
- 我們的目標是讓操作系統管理內存,使用戶程序能夠放入內存并正常執行。在段頁式內存管理下,程序載入內存的過程可以分為幾個關鍵步驟。第一步,要在虛擬內存上割出一段區域,分配給用戶程序的代碼段、數據段等,然后建立段表,記錄虛擬內存區域和程序段的對應關系,這一步相當于“假裝”把程序段放入了內存。
- 第二步,雖然在虛擬內存中有了映射,但程序還沒有真正放到物理內存中,所以接下來要在物理內存中找到空閑頁,建立頁表,將虛擬內存中的區域和物理內存的頁關聯起來。通過磁盤讀寫操作,把程序真正載入到物理內存中。
- 第三步,完成前面的操作后,程序已經在內存中了,但要能正常使用內存,還需要進行重定位,也就是前面講的地址翻譯過程,讓程序中的地址能夠正確對應到物理內存單元,這樣程序就能順利執行了。
-
結合代碼分析具體操作(以fork為例)
-
我們從
fork
函數開始分析程序載入內存的過程。fork
函數用于創建子進程,在這個過程中會涉及到內存的分配和映射。copy_process
函數會進行一系列操作,其中copy_mem
函數中的代碼實現了關鍵步驟。 -
set_base
等代碼用于設置段表,這對應著第一步在虛擬內存上分配區域并建立段表的操作。通過計算,給每個進程分配一定大小的虛擬內存空間(如這里每個進程分配64兆),并將虛擬內存的起始地址賦給段表中的基址,完成對虛擬地址的分割和段表的初始化。 -
接下來是分配物理內存和建立頁表。由于子進程是通過復制父進程創建的,所以在
copy_page_tables
函數中,子進程可以共用父進程已經分配好的物理內存頁,只需要拷貝頁表即可。通過一系列代碼操作,先確定父進程和子進程的頁目錄,為子進程分配新的頁目錄項(如果需要),然后將父進程頁表中的內容拷貝到子進程的頁表中,并將共享的頁設置為只讀,同時更新內存使用計數等信息。這樣,父子進程就都有了自己的虛擬內存、物理內存,并且段表和頁表都已正確建立,程序成功載入內存。
-
五、程序使用內存的過程及多進程地址分離
- 程序使用內存的操作
當操作系統把段表和頁表做好后,程序就可以使用內存了。以*p = 7
為例,假設p
的邏輯地址經過編譯后是300,首先根據邏輯地址(段號 + 段偏移)和段表中的基址算出線性地址(虛擬地址),然后內存管理單元(MMU)會根據虛擬地址和頁表算出物理地址。由于這個地址轉換過程如果用軟件實現會比較慢,所以CPU設計了MMU這個硬件來自動完成轉換功能。MMU算出物理地址(如7300)后,將其打到地址總線上,就把7存儲到對應的物理內存單元中,實現了*p = 7
的操作。 - 多進程地址分離與相互影響消除
- 當有多個進程時,比如父子進程執行同樣的代碼,子進程執行
*p = 8
,p
還是300。在執行過程中,由于父子進程的段表基址不同(子進程的段表基址是根據其自身分配的虛擬內存計算的),所以算出的虛擬地址不同。 - 又因為之前將共享的頁設置為只讀,當子進程要寫入數據(如寫入8)時,就會觸發寫時復制機制。此時會新申請一個內存頁,修改頁表,建立新的映射,將新的物理地址(如8300)與虛擬地址關聯起來。這樣,父進程和子進程就通過各自的映射表實現了地址的分離,避免了相互影響,每個進程都能獨立地訪問和使用自己的內存空間,就像我們之前講過多進程在內存中相互影響的問題,通過段表和頁表的配合,得到了完美解決。
- 當有多個進程時,比如父子進程執行同樣的代碼,子進程執行