天天看點

重學計算機組成原理(八)- 程式的裝載1 程式裝載的挑戰2 記憶體分段3 記憶體分頁4 總結5 推薦閱讀6 思考參考

比爾·蓋茨在上世紀80年代說的“640K ought to be enough for anyone”

也就是“640K記憶體對哪個人來說都夠用了”

那個年代,微軟開發的還是DOS作業系統,程式員們還在絞盡腦汁,想要用好這極為有限的640K記憶體

而現在,我手頭的Mac Book Pro已經是16G記憶體了,上升了一萬倍還不止。

那比爾·蓋茨這句話在當時也是完全的無稽之談麼?有沒有哪怕一點點的道理呢?這一講裡,我就和你一起來看一看。

1 程式裝載的挑戰

在運作這些可執行檔案的時候,我們其實是通過一個裝載器,解析ELF或者PE格式的可執行檔案

裝載器會把對應的指令和資料加載到記憶體裡面來,讓CPU去執行。

裝載到記憶體,裝載器需要滿足兩個要求

  • 可執行程式加載後占用的記憶體空間應該是連續的

    執行指令的時候,程式計數器是順序地一條一條指令執行。這意味着,這一條條指令需要連續地存儲在一起

  • 需要同時加載很多個程式,并且不能讓程式自己規定在記憶體中加載的位置

    雖然編譯出來的指令裡已經有了對應的各種各樣的記憶體位址,但是實際加載的時候,我們其實沒有辦法確定,這個程式一定加載在哪一段記憶體位址上

因為現在的計算機通常會同時運作很多個程式,可能你想要的記憶體位址已經被其他加載了的程式占用

要滿足這兩個基本的要求,我們很容易想到一個辦法。那就是我們可以在記憶體裡面,找到一段連續的記憶體空間,然後配置設定給裝載的程式,然後把這段連續的記憶體空間位址,和整個程式指令裡指定的記憶體位址做一個映射。

指令裡用到的記憶體位址叫作虛拟記憶體位址(Virtual Memory Address)

實際在記憶體硬體裡面的空間位址,我們叫實體記憶體位址(Physical Memory Address)

程式裡有指令和各種記憶體位址,我們隻需要關心虛拟記憶體位址就行了

對于任何一個程式來說,它看到的都是同樣的記憶體位址。我們維護一個虛拟記憶體到實體記憶體的映射表,這樣實際程式指令執行的時候,會通過虛拟記憶體位址,找到對應的實體記憶體位址,然後執行。因為是連續的記憶體位址空間,是以我們隻需要維護映射關系的起始位址和對應的空間大小就可以了。

2 記憶體分段

這種找出一段連續的實體記憶體和虛拟記憶體位址進行映射的方法,我們叫分段(Segmentation)。

這裡的段,就是指系統配置設定出來的那個連續的記憶體空間。

分段的辦法很好,解決了程式本身不需要關心具體的實體記憶體位址的問題,但它也有一些不足之處,第一個就是記憶體碎片(Memory Fragmentation)

舉個例子

電腦有1GB的記憶體

先啟動一個圖形渲染程式,占用了512MB的記憶體

接着啟動一個Chrome浏覽器,占用了128MB記憶體

再啟動一個PY程式,占用了256MB記憶體

這個時候,我們關掉Chrome,于是空閑記憶體還有1024 - 512 - 256 = 256MB

按理來說,我們有足夠的空間再去裝載一個200MB的程式。但是,這256MB的記憶體空間不是連續的,而是被分成了兩段128MB的記憶體

是以,實際情況是,我們的程式沒辦法加載進來。

當然了,有辦法解決 --- 記憶體交換(Memory Swapping)

我們可以把Python程式占用的256MB記憶體寫到硬碟,再從硬碟上讀回來到記憶體裡面

不過讀回來的時候,我們不再把它加載到原來的位置,而是緊緊跟在那已經被占用了的512MB記憶體後面

這樣,我們就有了連續的256MB記憶體空間,就可以去加載一個新的200MB的程式。如果你自己安裝過Linux作業系統,你應該遇到過配置設定一個swap硬碟分區的問題

這塊分出來的磁盤空間,其實就是專門給Linux作業系統進行記憶體交換用的。

虛拟記憶體、分段,再加上記憶體交換

看起來似乎已經解決了計算機同時裝載運作很多個程式的問題

不過三者的組合仍然會遇到一個性能瓶頸

  • 硬碟的通路速度要比記憶體慢很多
  • 而每一次記憶體交換,我們都需要把一大段連續的記憶體資料寫到硬碟上

是以,如果記憶體交換的時候,交換的是一個很占記憶體空間的程式,這樣整個機器都會顯得卡頓。

3 記憶體分頁

既然問題出在記憶體碎片和記憶體交換的空間太大上,那麼解決問題的辦法就是,少出現一些記憶體碎片。

另外,當需要進行記憶體交換的時候,讓需要交換寫入或者從磁盤裝載的資料更少一點,這樣就可以解決這個問題。

這個辦法,在現在計算機的記憶體管理裡面,就叫作記憶體分頁(Paging)

**和分段這樣配置設定一整段連續的空間給到程式相比

分頁則是把整個實體記憶體空間切成一段段固定尺寸的大小**

而對應的程式所需要占用的虛拟記憶體空間,也會同樣切成一段段固定尺寸的大小。

這樣一個連續并且尺寸固定的記憶體空間,我們叫頁(Page)。

從虛拟記憶體到實體記憶體的映射,不再是拿整段連續的記憶體的實體位址,而是按照一個個頁來的。

頁的尺寸一般遠遠小于整個程式的大小。

在Linux下,我們通常隻設定成4KB。你可以通過指令看看你手頭的Linux系統設定的頁的大小。

由于記憶體空間都是預先劃分好的,也就沒有不能使用的碎片,而隻有被釋放出來的很多4KB的頁。

即使記憶體空間不夠,需要讓現有的、正在運作的其他程式

通過記憶體交換釋放出一些記憶體的頁出來,一次性寫入磁盤的也隻有少數的一個頁或者幾個頁,不會花太多時間,讓整個機器被記憶體交換的過程給卡住。

分頁的方式使得加載程式的時候,不再需要一次性把程式加載到實體記憶體中

可以在進行虛拟記憶體和實體記憶體的頁之間的映射後,并不真的把頁加載到實體記憶體裡,而是隻在程式運作中,需要用到對應虛拟記憶體頁裡面的指令和資料時,再加載到實體記憶體裡面去。

實際上,我們的作業系統,的确是這麼做的

當要讀取特定的頁,卻發現資料并沒有加載到實體記憶體裡的時候,就會觸發一個來自于CPU的缺頁錯誤(Page Fault)

作業系統會捕捉到這個錯誤,然後将對應的頁,從存放在硬碟上的虛拟記憶體裡讀取出來,加載到實體記憶體裡。這種方式,使得我們可以運作那些遠大于我們實際實體記憶體的程式。同時,這樣一來,任何程式都不需要一次性加載完所有指令和資料,隻需要加載目前需要用到就行了。

通過虛拟記憶體、記憶體交換和記憶體分頁這三個技術的組合,我們最終得到了一個讓程式不需要考慮實際的實體記憶體位址、大小和目前配置設定空間的解決方案。

這些技術和方法,對于我們程式的編寫、編譯和連結過程都是透明的。這也是我們在計算機的軟硬體開發中常用的一種方法,就是加入一個間接層。

通過引入虛拟記憶體、頁映射和記憶體交換,我們的程式本身,就不再需要考慮對應的真實的記憶體位址、程式加載、記憶體管理等問題了。任何一個程式,都隻需要把記憶體當成是一塊完整而連續的空間來直接使用。

4 總結

電腦隻要640K記憶體就夠了嗎?很顯然,現在來看,比爾·蓋茨的這個判斷是不合理的,那為什麼他會這麼認為呢?因為他也是一個很優秀的程式員啊!

在虛拟記憶體、記憶體交換和記憶體分頁這三者結合之下,你會發現,其實要運作一個程式,“必需”的記憶體是很少的。CPU隻需要執行目前的指令,極限情況下,記憶體也隻需要加載一頁就好了。再大的程式,也可以分成一頁。每次,隻在需要用到對應的資料和指令的時候,從硬碟上交換到記憶體裡面來就好了。以我們現在4K記憶體一頁的大小,640K記憶體也能放下足足160頁呢,也無怪乎在比爾·蓋茨會說出“640K ought to be enough for anyone”這樣的話。

不過呢,硬碟的通路速度比記憶體慢很多,是以我們現在的計算機,沒有個幾G的記憶體都不好意思和人打招呼。

那麼,除了程式分頁裝載這種方式之外,我們還有其他優化記憶體使用的方式麼?下一講,我們就一起來看看“動态裝載”,學習一下讓兩個不同的應用程式,共用一個共享程式庫的辦法。

5 推薦閱讀

想要更深入地了解代碼裝載的詳細過程,推薦你閱讀《程式員的自我修養——連結、裝載和庫》的第1章和第6章。

6 思考

在Java這樣使用虛拟機的程式設計語言裡面,我們寫的程式是怎麼裝載到記憶體裡面來的呢?它也和我們講的一樣,是通過記憶體分頁和記憶體交換的方式加載到記憶體裡面來的麼?

jvm已經是上層應用,無需考慮實體分頁,一般更直接是考慮對象本身的空間大小,實體硬體管理統一由承載jvm的操縱系統去解決吧

參考

深入淺出計算機組成原理