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