操作系統(tǒng)是如何一步步發(fā)明虛擬內(nèi)存的?
那時引以為傲的System/360大型機(jī)雖然配備了豪華的256KB物理內(nèi)存(價格相當(dāng)于今天的數(shù)百萬美元),但在引入多進(jìn)程后內(nèi)存相關(guān)的問題開始出現(xiàn),因?yàn)槎鄠€進(jìn)程可以同時運(yùn)行在內(nèi)存中。
你面臨的核心問題是:如何保證多進(jìn)程能夠高效共享有限的物理內(nèi)存?
最初的嘗試:固定分區(qū)
你的第一個嘗試是最直觀的方法,將物理內(nèi)存劃分為幾個固定大小的區(qū)域,每個區(qū)域分配給一個程序:
圖片
這就是所謂的固定分區(qū)(Fixed Partitioning),這個想法很簡單,你很快實(shí)現(xiàn)了這個機(jī)制:
// 固定分區(qū)內(nèi)存管理的簡單實(shí)現(xiàn)
struct memory_partition {
void* start_address; // 分區(qū)起始地址
size_t size; // 分區(qū)大小
bool is_occupied; // 是否被占用
int process_id; // 占用進(jìn)程ID
};
這個簡單的分區(qū)系統(tǒng)確實(shí)解決了一些問題。它允許多個程序同時駐留在內(nèi)存中,并提供了基本的內(nèi)存隔離。然而,它很快就暴露出了嚴(yán)重的缺陷。
問題出在內(nèi)存利用率上:一個只需要10KB內(nèi)存的小程序占用了整個64KB的分區(qū),而一個需要70KB的程序卻無法運(yùn)行,因?yàn)闆]有任何一個分區(qū)足夠大,盡管系統(tǒng)中空閑內(nèi)存超過了70KB!
你意識到,固定分區(qū)雖然簡單,但極其浪費(fèi)內(nèi)存資源。
它無法適應(yīng)程序大小的變化,也無法解決運(yùn)行大型程序的問題。
這個方案本質(zhì)就是吃大鍋飯,不管你可執(zhí)行程序本身有多大都給你固定內(nèi)存,打破大鍋飯的最佳方法就是按勞分配。
動態(tài)分區(qū):按需分配
既然是按勞分配那就不能預(yù)先劃分內(nèi)存,而是根據(jù)程序的實(shí)際需求動態(tài)分配內(nèi)存塊,用多少給多少:
// 動態(tài)分區(qū)內(nèi)存管理
struct memory_block {
void* start_address; // 內(nèi)存塊起始地址
size_t size; // 內(nèi)存塊大小
bool is_free; // 是否空閑
struct memory_block* next; // 鏈表中的下一個塊
};
struct memory_block* free_list; // 空閑內(nèi)存塊鏈表
這就是你在數(shù)據(jù)結(jié)構(gòu)課上學(xué)到的鏈表。
動態(tài)分區(qū)確實(shí)提高了內(nèi)存利用率,程序可以獲得剛好滿足其需求的內(nèi)存量,這種內(nèi)存分配方法開始流行起來。
然而,隨著系統(tǒng)運(yùn)行時間的增長,大量用戶開始反饋物理內(nèi)存很快耗盡導(dǎo)致程序崩潰,一通debug后你發(fā)現(xiàn)了問題:內(nèi)存碎片。
只需要幾周的運(yùn)行,系統(tǒng)中就會出現(xiàn)了大量的小內(nèi)存塊,它們分散在各處,雖然總和足夠大,但沒有一個連續(xù)的塊能滿足新程序的需求。
更糟糕的是,即使使用動態(tài)分區(qū),仍然無法運(yùn)行那些需要超過物理內(nèi)存總量的程序。
因?yàn)樵?0世紀(jì)60-80年代,雖然計算機(jī)物理內(nèi)存有限(如KB級別),但程序規(guī)模卻在逐漸增大(如大型科學(xué)計算、數(shù)據(jù)庫系統(tǒng)),這是一個根本性的限制,你需要一種全新的思路...
覆蓋技術(shù):程序員的自我管理
面對內(nèi)存不足的問題,你開始思考,既然內(nèi)存一次性裝不下大型程序,那么為什么不把這個大型程序拆開了、用到哪些就裝哪些呢?
看上去好像能解決問題,你進(jìn)一步思考,程序其實(shí)可以被劃分為多個獨(dú)立的功能模塊,一些核心的模塊可能需要始終駐留在內(nèi)存(如主控制邏輯、核心函數(shù)),而非核心的功能模塊可以按需動態(tài)加載到共享內(nèi)存區(qū)域,覆蓋前一個模塊。
假設(shè)可執(zhí)行程序A劃分為一個核心模塊和4個功能模塊,那么當(dāng)需要運(yùn)行模塊1時就把模塊1加載到共享內(nèi)存區(qū)域,當(dāng)需要運(yùn)行模塊2時就把模塊2加載到共享內(nèi)存中覆蓋掉原來的模塊1:
圖片
這樣就能實(shí)現(xiàn)在有限的物理內(nèi)存中運(yùn)行超大程序的目的,這就是早期操作系統(tǒng)中的"覆蓋技術(shù)"(Overlay)。
這種方法要求程序員手動將程序分割成多個模塊,并在運(yùn)行時根據(jù)需要將不同模塊加載到同一塊內(nèi)存區(qū)域。
// 程序員使用覆蓋技術(shù)的偽代碼
void main() {
// 主模塊始終在內(nèi)存中
// 需要模塊A時
load_module("module_A", OVERLAY_REGION);
execute_module_A();
// 需要模塊B時,覆蓋同一內(nèi)存區(qū)域
load_module("module_B", OVERLAY_REGION);
execute_module_B();
// 再次需要模塊A時
load_module("module_A", OVERLAY_REGION);
execute_module_A_again();
}
覆蓋技術(shù)確實(shí)突破了物理內(nèi)存限制,可以在有限的物理內(nèi)存上運(yùn)行大型程序,是一種非常聰明的方法。
但它有嚴(yán)重的缺點(diǎn):
- 程序員必須手動管理內(nèi)存,這極其復(fù)雜且容易出錯
- 程序必須預(yù)先知道哪些模塊可以共享內(nèi)存區(qū)域
- 頻繁的模塊加載會導(dǎo)致性能下降
這讓你開始認(rèn)識到:內(nèi)存管理太重要了,絕不能完全依賴程序員自己手動管理。
因此你需要一個系統(tǒng)級的解決方案,能夠自動管理內(nèi)存,對程序員透明,同時允許程序使用超過物理內(nèi)存的地址空間,這就是后來的虛擬內(nèi)存技術(shù)。