1. 程式人生 > >每天3分鐘作業系統修煉祕籍(8):虛擬記憶體分段

每天3分鐘作業系統修煉祕籍(8):虛擬記憶體分段

點我檢視祕籍連載

程序的地址空間佈局:分段

Linux的虛擬地址空間採用“分段+分頁”結合的方式實現。先看分段,之後再介紹分頁。

分段是將記憶體劃分成各個段落(Segment),每個段落的長度可以不同,且虛擬地址空間中未使用的空間不會對映到實體記憶體中,所以作業系統不會為這段空間分配實體記憶體。這樣的話,核心為剛建立的程序分配的實體記憶體可以很小,隨著程序執行不斷使用記憶體,核心再為程序按需分配實體記憶體。也就是說,儘管地址空間的範圍和實體記憶體大小一樣,但不會將全部空間對映到實體記憶體。

對於Linux程序的虛擬地址空間來說,它的記憶體佈局如下圖。

虛擬空間分了如下幾個段:

  • 文字段(Text):也稱為程式碼段。程序啟動時會將程式的程式碼載入到實體記憶體中,文字段對映到這片實體記憶體。
  • 初始化資料:包含程式顯式初始化的全域性變數和靜態變數,這些資料是在程式真正執行前就已經確定的資料,所以可以提前載入到記憶體儲存好。
  • 未初始化資料(BSS):未初始化的全域性變數和靜態變數,這些變數的值是在程式真正執行起來併為其賦值後才能確定的,所以程式載入之初,只需要記錄它的記憶體地址和所需大小。出於歷史原因,這段空間也稱為BSS段。
  • 棧(Stack):是一個可以動態增長和收縮的記憶體段落,由棧幀(Stack Frames)組成,程序每呼叫一次函式,都將為該函式分配一個棧幀,棧幀中儲存了該函式的區域性變數、引數值和返回值。注意,編譯器會將函式引數放入暫存器來優化程式,只有暫存器放不下的引數才使用棧幀來儲存。
  • 堆(Heap):程式執行時,變數的值以及動態請求分配的記憶體都在這個記憶體段落中。
  • 核心段(Kernel):這部分是作業系統核心執行時所佔用記憶體在各程序虛擬地址空間中的對映。所有程序都有,且對映地址相同,因為都對映到核心使用的記憶體。這段記憶體只有核心能訪問,使用者程序無法訪問到該段落。

從上面的描述大概也能推測出,除了堆記憶體外,其它段落空間都是自動填充分配的,使用者無法控制這些記憶體的使用。而堆記憶體段是使用者能使用的自由記憶體區,絕大多數程式的使用者資料都丟在這裡面,算是一個大雜燴空間。

例如,下圖中是一段C程式碼和記憶體佈局之間的對應關係。

提示:其它語言的記憶體佈局
上面的佈局C程式的記憶體佈局,也是Linux下程序的記憶體佈局。其它語言(比如CPython)編寫的程式執行起來後,只要是在Linux下執行,其程序的佈局也會如此。只不過這些語言的程式中,全域性變數、區域性變數等可能和C的佈局不一樣,這和各語言的底層設計有關。比如C編寫的某動態語言,它不要求指定變數的資料型別,那麼在載入到記憶體的時候自然不知道該變數型別所需的空間大小,當它轉換成C後(儘管不會真的轉換成C程式碼),這個變數只能丟進堆記憶體作為動態資料。

使用分段的好處就是“各段自掃門前雪”,雖然在地址空間中每個分段的地址都是連續的,但實際上,每個分段對映到實體記憶體地址時是獨立的,段與段之間可以不連續。這是因為CPU為每個段都使用一對(即兩個)特殊的暫存器:基址暫存器和界限暫存器。

而界限暫存器中的值用來表示該段在實體記憶體中的大小,即已為該段分配了多少記憶體。當準備用虛擬地址加基址計算實體地址時,需要先根據界限暫存器中的值檢查將要訪問的實體記憶體地址是否超出了這個段的範圍。如果超出了,則表示訪問了不屬於該段的記憶體,也即記憶體的越界訪問,而使用者程序是沒有許可權訪問其它程序或未分配記憶體的地址的,這時會收到一個SIGSEGV(segmentation violation)訊號並提示:Segmentation Fault,即段錯誤或段異常。收到這個訊號後預設情況下會終止該程序,因為它訪問了非法地址,但是可以設定該訊號的訊號處理程式,從而做出其它處理。

例如,下圖中的程序訪問了Kernel段或者unallocated memory部分的記憶體,都會報錯。Kernel段除了核心程序,任何使用者程序都無法訪問,典型的地址是使用者程序想要訪問0x0地址時,而該地址屬於Kernel,所以報錯。而unallocated memory是還未分配的記憶體,界限暫存器會保護該段無法訪問。

再例如,C陣列的越界訪問時也會出現該問題。下圖直觀地顯示了在Windows中同樣的記憶體越界錯誤。

也就是說,基址暫存器是用來轉換地址的,界限暫存器是用來保護程序不越界訪問記憶體的。CPU藉助基址暫存器和界限暫存器管理並提供地址翻譯和記憶體保護的功能,通常稱為記憶體管理單元(Memory Management Unit,MMU)。

最後再說明一點,記憶體地址翻譯的任務既可以由作業系統來做,也可以由硬體CPU來做。但如果完全由作業系統來完成,就需要頻繁地陷入到核心態,這樣效率會非常低。所以,這項任務交給CPU硬體來完成,作業系統只需在必要的時候介入,比如分配記憶體、回收記憶體等