上一講溺蕉,我們看到了如何通過(guò)鏈接器某宪,把多個(gè)文件合并成一個(gè)最終可執(zhí)行文件福压。在運(yùn)行這些可執(zhí)行文件的時(shí)候掏秩,我們其實(shí)是通過(guò)一個(gè)裝載器或舞,解析 ELF 或者 PE 格式的可執(zhí)行文件。裝載器會(huì)把對(duì)應(yīng)的指令和數(shù)據(jù)加載到內(nèi)存里面來(lái)蒙幻,讓 CPU 去執(zhí)行映凳。
說(shuō)起來(lái)只是裝載到內(nèi)存里面這一句話(huà)的事兒,實(shí)際上裝載器需要滿(mǎn)足兩個(gè)要求邮破。
第一诈豌,可執(zhí)行程序加載后占用的內(nèi)存空間應(yīng)該是連續(xù)的,執(zhí)行指令的時(shí)候抒和,程序計(jì)數(shù)器是順序地一條一條指令執(zhí)行下去队询。這也就意味著,這一條條指令需要連續(xù)地存儲(chǔ)在一起构诚。
第二,我們需要同時(shí)加載很多個(gè)程序铆惑,并且不能讓程序自己規(guī)定在內(nèi)存中加載的位置范嘱。雖然編譯出來(lái)的指令里已經(jīng)有了對(duì)應(yīng)的各種各樣的內(nèi)存地址,但是實(shí)際加載的時(shí)候员魏,我們其實(shí)沒(méi)有辦法確保丑蛤,這個(gè)程序一定加載在哪一段內(nèi)存地址上。因?yàn)槲覀儸F(xiàn)在的計(jì)算機(jī)通常會(huì)同時(shí)運(yùn)行很多個(gè)程序撕阎,可能你想要的內(nèi)存地址已經(jīng)被其他加載了的程序占用了受裹。
要滿(mǎn)足這兩個(gè)基本的要求,我們很容易想到一個(gè)辦法虏束。那就是我們可以在內(nèi)存里面棉饶,找到一段連續(xù)的內(nèi)存空間,然后分配給裝載的程序镇匀,然后把這段連續(xù)的內(nèi)存空間地址照藻,和整個(gè)程序指令里指定的內(nèi)存地址做一個(gè)映射。
我們把指令里用到的內(nèi)存地址叫作虛擬內(nèi)存地址(Virtual Memory Address)汗侵,實(shí)際在內(nèi)存硬件里面的空間地址幸缕,我們叫物理內(nèi)存地址(Physical Memory Address)。
程序里有指令和各種內(nèi)存地址晰韵,我們只需要關(guān)心虛擬內(nèi)存地址就行了发乔。對(duì)于任何一個(gè)程序來(lái)說(shuō),它看到的都是同樣的內(nèi)存地址雪猪。我們維護(hù)一個(gè)虛擬內(nèi)存到物理內(nèi)存的映射表栏尚,這樣實(shí)際程序指令執(zhí)行的時(shí)候,會(huì)通過(guò)虛擬內(nèi)存地址浪蹂,找到對(duì)應(yīng)的物理內(nèi)存地址抵栈,然后執(zhí)行告材。因?yàn)槭沁B續(xù)的內(nèi)存地址空間,所以我們只需要維護(hù)映射關(guān)系的起始地址和對(duì)應(yīng)的空間大小就可以了古劲。
內(nèi)存分段
這種找出一段連續(xù)的物理內(nèi)存和虛擬內(nèi)存地址進(jìn)行映射的方法斥赋,我們叫分段(Segmentation)。這里的段产艾,就是指系統(tǒng)分配出來(lái)的那個(gè)連續(xù)的內(nèi)存空間疤剑。
分段的辦法很好,解決了程序本身不需要關(guān)心具體的物理內(nèi)存地址的問(wèn)題闷堡,但它也有一些不足之處隘膘,第一個(gè)就是內(nèi)存碎片(Memory Fragmentation)的問(wèn)題。
我們來(lái)看這樣一個(gè)例子杠览。我現(xiàn)在手頭的這臺(tái)電腦弯菊,有 1GB 的內(nèi)存。我們先啟動(dòng)一個(gè)圖形渲染程序踱阿,占用了 512MB 的內(nèi)存管钳,接著啟動(dòng)一個(gè) Chrome 瀏覽器,占用了 128MB 內(nèi)存软舌,再啟動(dòng)一個(gè) Python 程序才漆,占用了 256MB 內(nèi)存。這個(gè)時(shí)候佛点,我們關(guān)掉 Chrome醇滥,于是空閑內(nèi)存還有 1024 - 512 - 256 = 256MB。按理來(lái)說(shuō)超营,我們有足夠的空間再去裝載一個(gè)200MB 的程序鸳玩。但是,這 256MB 的內(nèi)存空間不是連續(xù)的演闭,而是被分成了兩段 128MB 的內(nèi)存怀喉。因此,實(shí)際情況是船响,我們的程序沒(méi)辦法加載進(jìn)來(lái)躬拢。
當(dāng)然,這個(gè)我們也有辦法解決见间。解決的辦法叫內(nèi)存交換(Memory Swapping)聊闯。
我們可以把 Python 程序占用的那 256MB 內(nèi)存寫(xiě)到硬盤(pán)上,然后再?gòu)挠脖P(pán)上讀回來(lái)到內(nèi)存里面米诉。不過(guò)讀回來(lái)的時(shí)候菱蔬,我們不再把它加載到原來(lái)的位置,而是緊緊跟在那已經(jīng)被占用了的 512MB 內(nèi)存后面。這樣拴泌,我們就有了連續(xù)的 256MB 內(nèi)存空間魏身,就可以去加載一個(gè)新的200MB 的程序。如果你自己安裝過(guò) Linux 操作系統(tǒng)蚪腐,你應(yīng)該遇到過(guò)分配一個(gè) swap 硬盤(pán)分區(qū)的問(wèn)題箭昵。這塊分出來(lái)的磁盤(pán)空間,其實(shí)就是專(zhuān)門(mén)給 Linux 操作系統(tǒng)進(jìn)行內(nèi)存交換用的回季。
虛擬內(nèi)存家制、分段,再加上內(nèi)存交換泡一,看起來(lái)似乎已經(jīng)解決了計(jì)算機(jī)同時(shí)裝載運(yùn)行很多個(gè)程序的問(wèn)題颤殴。不過(guò),你千萬(wàn)不要大意鼻忠,這三者的組合仍然會(huì)遇到一個(gè)性能瓶頸涵但。硬盤(pán)的訪(fǎng)問(wèn)速度要比內(nèi)存慢很多,而每一次內(nèi)存交換帖蔓,我們都需要把一大段連續(xù)的內(nèi)存數(shù)據(jù)寫(xiě)到硬盤(pán)上贤笆。所以,如果內(nèi)存交換的時(shí)候讨阻,交換的是一個(gè)很占內(nèi)存空間的程序,這樣整個(gè)機(jī)器都會(huì)顯得卡頓篡殷。
內(nèi)存分頁(yè)
既然問(wèn)題出在內(nèi)存碎片和內(nèi)存交換的空間太大上钝吮,那么解決問(wèn)題的辦法就是,少出現(xiàn)一些內(nèi)存碎片板辽。另外奇瘦,當(dāng)需要進(jìn)行內(nèi)存交換的時(shí)候,讓需要交換寫(xiě)入或者從磁盤(pán)裝載的數(shù)據(jù)更少一點(diǎn)劲弦,這樣就可以解決這個(gè)問(wèn)題耳标。這個(gè)辦法,在現(xiàn)在計(jì)算機(jī)的內(nèi)存管理里面邑跪,就叫作內(nèi)存分頁(yè)(Paging)次坡。
和分段這樣分配一整段連續(xù)的空間給到程序相比,分頁(yè)是把整個(gè)物理內(nèi)存空間切成一段段固定尺寸的大小画畅。而對(duì)應(yīng)的程序所需要占用的虛擬內(nèi)存空間砸琅,也會(huì)同樣切成一段段固定尺寸的大小。這樣一個(gè)連續(xù)并且尺寸固定的內(nèi)存空間轴踱,我們叫頁(yè)(Page)症脂。從虛擬內(nèi)存到物理內(nèi)存的映射,不再是拿整段連續(xù)的內(nèi)存的物理地址,而是按照一個(gè)一個(gè)頁(yè)來(lái)的诱篷。頁(yè)的尺寸一般遠(yuǎn)遠(yuǎn)小于整個(gè)程序的大小壶唤。在 Linux 下,我們通常只設(shè)置成 4KB棕所。你可以通過(guò)命令看看你手頭的 Linux 系統(tǒng)設(shè)置的頁(yè)的大小闸盔。
getconf PAGE_SIZE
由于內(nèi)存空間都是預(yù)先劃分好的,也就沒(méi)有了不能使用的碎片橙凳,而只有被釋放出來(lái)的很多4KB 的頁(yè)蕾殴。即使內(nèi)存空間不夠,需要讓現(xiàn)有的岛啸、正在運(yùn)行的其他程序钓觉,通過(guò)內(nèi)存交換釋放出一些內(nèi)存的頁(yè)出來(lái),一次性寫(xiě)入磁盤(pán)的也只有少數(shù)的一個(gè)頁(yè)或者幾個(gè)頁(yè)坚踩,不會(huì)花太多時(shí)間荡灾,讓整個(gè)機(jī)器被內(nèi)存交換的過(guò)程給卡住。
更進(jìn)一步地瞬铸,分頁(yè)的方式使得我們?cè)诩虞d程序的時(shí)候批幌,不再需要一次性都把程序加載到物理內(nèi)存中。我們完全可以在進(jìn)行虛擬內(nèi)存和物理內(nèi)存的頁(yè)之間的映射之后嗓节,并不真的把頁(yè)加載到物理內(nèi)存里荧缘,而是只在程序運(yùn)行中,需要用到對(duì)應(yīng)虛擬內(nèi)存頁(yè)里面的指令和數(shù)據(jù)時(shí)拦宣,再加載到物理內(nèi)存里面去截粗。
實(shí)際上,我們的操作系統(tǒng)鸵隧,的確是這么做的绸罗。當(dāng)要讀取特定的頁(yè),卻發(fā)現(xiàn)數(shù)據(jù)并沒(méi)有加載到物理內(nèi)存里的時(shí)候豆瘫,就會(huì)觸發(fā)一個(gè)來(lái)自于 CPU 的缺頁(yè)錯(cuò)誤(Page Fault)珊蟀。我們的操作系統(tǒng)會(huì)捕捉到這個(gè)錯(cuò)誤,然后將對(duì)應(yīng)的頁(yè)外驱,從存放在硬盤(pán)上的虛擬內(nèi)存里讀取出來(lái)育灸,加載到物理內(nèi)存里。這種方式昵宇,使得我們可以運(yùn)行那些遠(yuǎn)大于我們實(shí)際物理內(nèi)存的程序描扯。同時(shí),這樣一來(lái)趟薄,任何程序都不需要一次性加載完所有指令和數(shù)據(jù)绽诚,只需要加載當(dāng)前需要用到就行了。
通過(guò)虛擬內(nèi)存、內(nèi)存交換和內(nèi)存分頁(yè)這三個(gè)技術(shù)的組合恩够,我們最終得到了一個(gè)讓程序不需要考慮實(shí)際的物理內(nèi)存地址卒落、大小和當(dāng)前分配空間的解決方案。這些技術(shù)和方法蜂桶,對(duì)于我們程序的編寫(xiě)儡毕、編譯和鏈接過(guò)程都是透明的。這也是我們?cè)谟?jì)算機(jī)的軟硬件開(kāi)發(fā)中常用的一種方法扑媚,就是加入一個(gè)間接層腰湾。
通過(guò)引入虛擬內(nèi)存、頁(yè)映射和內(nèi)存交換疆股,我們的程序本身费坊,就不再需要考慮對(duì)應(yīng)的真實(shí)的內(nèi)存地址、程序加載旬痹、內(nèi)存管理等問(wèn)題了附井。任何一個(gè)程序,都只需要把內(nèi)存當(dāng)成是一塊完整而連續(xù)的空間來(lái)直接使用两残。
總結(jié)延伸
現(xiàn)在回到開(kāi)頭我問(wèn)你的問(wèn)題永毅,我們的電腦只要 640K 內(nèi)存就夠了嗎?很顯然,現(xiàn)在來(lái)看人弓,比爾·蓋茨的這個(gè)判斷是不合理的沼死,那為什么他會(huì)這么認(rèn)為呢?因?yàn)樗彩且粋€(gè)很優(yōu)秀的程序員啊!
在虛擬內(nèi)存、內(nèi)存交換和內(nèi)存分頁(yè)這三者結(jié)合之下崔赌,你會(huì)發(fā)現(xiàn)意蛀,其實(shí)要運(yùn)行一個(gè)程序,“必需”的內(nèi)存是很少的峰鄙。CPU 只需要執(zhí)行當(dāng)前的指令,極限情況下太雨,內(nèi)存也只需要加載一頁(yè)就好了吟榴。再大的程序,也可以分成一頁(yè)囊扳。每次吩翻,只在需要用到對(duì)應(yīng)的數(shù)據(jù)和指令的時(shí)候,從硬盤(pán)上交換到內(nèi)存里面來(lái)就好了锥咸。以我們現(xiàn)在 4K 內(nèi)存一頁(yè)的大小狭瞎,640K 內(nèi)存也能放下足足 160 頁(yè)呢,也無(wú)怪乎在比爾·蓋茨會(huì)說(shuō)出“640K ought to be enough for anyone”這樣的話(huà)搏予。
不過(guò)呢熊锭,硬盤(pán)的訪(fǎng)問(wèn)速度比內(nèi)存慢很多,所以我們現(xiàn)在的計(jì)算機(jī),沒(méi)有個(gè)幾 G 的內(nèi)存都不好意思和人打招呼碗殷。
那么精绎,除了程序分頁(yè)裝載這種方式之外,我們還有其他優(yōu)化內(nèi)存使用的方式么?下一講锌妻,我們就一起來(lái)看看“動(dòng)態(tài)裝載”代乃,學(xué)習(xí)一下讓兩個(gè)不同的應(yīng)用程序,共用一個(gè)共享程序庫(kù)的辦法仿粹。