當我們運行各類程序在現(xiàn)代操作系統(tǒng)諸如Windows、linux上時胁附,往往無需操心操作系統(tǒng)及硬件如何在內存上“管理”程序的代碼及數(shù)據(jù)酒繁,作為一名程序員,無需擔心像“我該在哪里存儲這個變量”這樣的問題汉嗽,操作系統(tǒng)已經(jīng)暗地幫你做好了一切欲逃,如果不是,那么將十分痛苦饼暑。
為了實現(xiàn)進程間內存隔離稳析,兼顧訪問效率、空間碎片等因素的影響弓叛,操作系統(tǒng)也相應衍生出了多種不同的內存虛擬化機制彰居,耳熟能詳?shù)木陀蟹侄巍⒎猪撟辍⒍雾摮露琛⒍嗉夗摫淼龋挛尼槍@幾種虛擬化機制進行較為詳細的闡述毕籽。
在開始正文前抬闯,我們需要簡單了解一下地址重定位相關知識以及邏輯地址、線性地址和物理地址之間的區(qū)別
操作系統(tǒng)為了實現(xiàn)內存虛擬化关筒,確保應用程序只訪問自己的內存空間溶握,利用了基于硬件的地址轉換技術,基于地址轉換蒸播,硬件每次訪問內存(指令獲取睡榆、數(shù)據(jù)讀取或寫入),都會將程序內存引用地址重定位到內存中實際的地址袍榆。
如圖展示了一個進程16KB大小的地址空間胀屿,可以看到代碼及數(shù)據(jù)都位于進程地址空間中,操作系統(tǒng)希望將這個地址空間放到物理內存中的其他位置進行管理包雀,并不一定從地址0開始
如圖所示宿崭,操作系統(tǒng)將第一塊物理內存留給了自己,并將上面進程16KB的地址空間重定位到了從32KB開始的物理內存地址處
現(xiàn)在的問題是才写,操作系統(tǒng)如何實現(xiàn)地址的重定位劳曹,程序編譯鏈接成二進制碼后奴愉,其引用的內存地址已經(jīng)確定了,如何將引用的地址進行轉換呢铁孵?
主要有兩種方法:
早期的系統(tǒng)采用純軟件的重定位方式,其被稱為靜態(tài)重定位房资,使用一個加載程序(loader)接手將要運行的可執(zhí)行程序蜕劝,將它的地址重寫到物理內存中期望的偏移位置,沒有操作系統(tǒng)的管理轰异,進程中的錯誤地址可能會導致loader重定位后對其他進程或操作系統(tǒng)的內存進行非法訪問岖沛,且由于loader的重定位規(guī)則固定,很難將內存空間重定位到其它位置
而現(xiàn)代重定位方式是基于硬件的地址轉換搭独,具體來說婴削,每個CPU需要兩個硬件寄存器,基址寄存器(base)和界限寄存器(bound)牙肝,進程產(chǎn)生的所有內存引用唉俗,都會被處理器通過下列方式轉換為物理地址:physical address = virtual address + base 進程使用的虛擬地址,硬件將虛擬地址加上基址寄存器的內容得到物理地址配椭,再對內存進行訪問虫溜,界限寄存器確保了進程產(chǎn)生的所有地址都在進程的地址‘界限’中 ,這種在CPU中負責地址轉換的部分被稱為內存管理單元(MMU)
使用硬件的地址重定位股缸,也衍生出了后來基于分段分頁的內存虛擬化技術衡楞,相應的,也就有了邏輯地址敦姻、線性地址瘾境、物理地址的概念
邏輯地址由兩部份組成,段標識符: 段內偏移量(如cs:77000000,代碼段77000000偏移)镰惦,可以認為是分段機制轉換前的地址迷守,通過段描述符找到該段的線性地址基址,加上段內偏移量即可得到線性地址陨献。
線性地址可以認為是分段機制轉換后裂逐、硬件頁式內存的轉換前的地址
物理地址是線性地址經(jīng)分頁轉換后的地址(采用分頁)姑蓝,若不采用分頁,則線性地址即物理地址(實模式下)
分段
使用基址及界限寄存器可以很好地將虛擬地址重定位到物理地址(內存)上,但是如上面進程虛擬地址空間圖例展示的肋殴,在整個虛擬地址空間中,存在大塊的空閑空間耘成,如棧和堆之間的地址空間
將整個進程的虛擬空間都重定位到內存上谓传,將會存在大量的內部碎片,且物理內存是有限的聘殖,在32位地址尋址的機子上晨雳,每個進程可尋址的虛擬空間就有4G行瑞,基于基址加界限的方式將會導致物理內存無法提供足夠的空間來放置所有進程地址空間,進程便無法運行餐禁,其靈活性較差
為了解決這個問題血久,分段機制應運而生,其主要做法是在MMU中引入不止一個基址跟界限寄存器對帮非,而是給每個邏輯段一對氧吐,一個段只是地址空間一個連續(xù)定長的區(qū)域,典型的地址空間有三個邏輯段:代碼末盔、堆和棧筑舅。分段機制可以讓操作系統(tǒng)將不同的段放到不同的物理內存地址處,通過更為細顆粒度的管理虛擬地址陨舱,避免了虛擬地址空間中未使用的部分占用內存
從圖中可以看到翠拣,只有已使用的內存才會在物理內存中分配空間
現(xiàn)在的問題是,使用分段機制游盲,在進行地址轉換時误墓,如何判斷使用哪個段寄存器、段內的偏移量是多少背桐?
如圖所示优烧,常見的做法是將虛擬地址分成兩部分,一部分標識使用哪個段寄存器链峭,一個標識段內偏移量
由于有三個段畦娄,分兩位標識段寄存器,剩下的標識偏移量弊仪,如圖熙卡,如果前兩位是00,硬件就知道這是代碼段的地址,因此使用代碼段的基址跟界限重定位到正確的物理地址励饵,其轉換過程偽代碼如下所示
先通過虛擬地址得到段寄存器索引驳癌,再獲取段內偏移量,根據(jù)段界限寄存器判斷偏移量是否在合法地址空間中役听,若非法颓鲜,則觸發(fā)段錯誤,否則使用段基址寄存器加偏移量得到實際物理地址典予,并進行訪問
特殊的棧
由于棧的地址空間增長方向為高地址往低地址甜滨,所以硬件(MMU)進行地址轉換時,還需要知道棧的增長方向瘤袖,因此段寄存器需要使用一位來區(qū)分方向(1代表自小而大
增長衣摩,0反之)
假設要訪問虛擬地址11 0100 0000 0000 硬件使用前兩位指定棧段,得出段內偏移量1KB捂敌,用1KB減去段大邪纭(2KB)得到-1KB的反向偏移量既琴,加上基址28KB就得到了正確的物理地址27KB
支持共享
有時候在地址空間共享某些內存段是非常有用的,比如多個進程間代碼段共享泡嘴,共享某些段甫恩,就需要在程序每個段增加幾個位,用來標識程序是否能夠讀寫該段或執(zhí)行其中的代碼酌予,這樣同樣的代碼被共享填物,且不用擔心其被破壞
這樣,訪問內存時除了要檢查虛擬地址是否越界霎终,硬件還需檢查特定訪問是否被允許,如果程序嘗試寫入只讀段升薯,或從非執(zhí)行段執(zhí)行指令莱褒,硬件就會觸發(fā)異常,操作系統(tǒng)便處理出錯進程
分段解決了一些問題涎劈,幫助我們實現(xiàn)了更高效的虛擬內存广凸。不只是動態(tài)重定位,通過避免地址空間的邏輯段之間的大量潛在的內存浪費蛛枚,分段能更好地利用物理內存空間谅海,其支持共享可以保證多個運行的程序共享某些段如代碼段不會出現(xiàn)問題。
但是由于段的大小不一蹦浦,隨著程序越來越多扭吁,物理內存會被分割成各種奇怪的大小(外部碎片)盲镶,因此內存分配請求會變得更難侥袜。
一種解決方法是緊湊物理內存,重寫安排原有的所有段溉贿,操作系統(tǒng)先終止運行的進程枫吧,將他們的數(shù)據(jù)復制到連續(xù)的內存區(qū)域中,改變段寄存器的值宇色,指向新的段基址九杂,從而解決外部碎片,但是拷貝段是內存密集型的宣蠕,會占用大量cpu處理時間
另一種做法是利用空閑列表管理算法例隆,試圖保留大的內存塊進行分配,如使用最優(yōu)匹配植影、最壞匹配裳擎、首次匹配、伙伴算法等方式思币,有興趣的同學可以繼續(xù)深入了解鹿响。
分頁
實際上羡微,分段機制還是不足以支持更一般化的稀疏地址空間,對虛擬地址空間細化管理到段為單位惶我,盡管可以使用各類機制(內存管理算法)但還是會存在外部碎片妈倔,假想有一個很大的堆,處于邏輯段中绸贡,整個堆還是需要完整地加載到內存中盯蝴,因此,我們需要更一般化的虛擬機制來細化管理內存
為了解決外部碎片的問題听怕,分片機制應運而生捧挺,其將空間分割為固定長度的分片,不再將一個進程的地址空間分割成幾個長度不同的邏輯段(代碼尿瞭、堆闽烙、棧等),其分割后的每個單元稱為一頁,這時声搁,物理內存可以看成是定長槽塊的陣列黑竞,每個槽塊叫做頁幀,一個頁幀包含一個虛擬內存頁疏旨,通過這種機制很魂,我們可以對內存進行更一般化的管理
現(xiàn)在的問題是,使用分頁機制檐涝,如何進行地址轉換遏匆,空間和時間開銷如何
前文所述使用分段單位是段,通過段寄存器獲取基址跟界限后進行地址轉換骤铃,分頁使用更一般化的管理拉岁,單位是頁,那么怎么完成虛擬頁到物理頁的轉換呢惰爬,使用寄存器這時變得不切實際喊暖,因為一個進程虛擬空間采用分頁的話將產(chǎn)生成千上萬的頁,而寄存器數(shù)量是有限的撕瞧,因此需要有一種新的機制完成虛擬頁到物理頁的轉換陵叽,為了記錄地址空間的每個虛擬頁放在物理內存中的位置,操作系統(tǒng)通常為每個進程保存一個數(shù)據(jù)結構(存儲于物理內存中)丛版,稱為頁表
如圖巩掺,對應VPN0->PFN3(虛擬頁0->物理幀3)、VPN1->PFN7页畦、VPN2->PFN5胖替、VPN3->PFN2
進程的地址空間也變成了VPN標識+頁內偏移量兩部分組成
可以看到該地址對應虛擬頁VPN1,偏移為該頁第5個字節(jié)處,通過頁表查詢物理頁為PFN7独令,這樣根據(jù)物理頁跟頁內偏移即可得到物理地址端朵。下面利用分頁機制訪問內存流程
首先根據(jù)虛擬地址獲取虛擬頁號VPN,通過每個進程的頁表基址寄存器加上VPN頁表內偏移量得到頁表項地址燃箭,訪問頁表項冲呢,判斷頁表項有效位,若無效招狸,拋出頁錯誤敬拓,若有效,繼續(xù)判斷頁表項保護位裙戏,若無法訪問乘凸,拋出錯誤,否則根據(jù)虛擬地址獲取的頁內偏移量加上頁表項的物理頁地址得到完整的物理地址并訪問內存累榜。
頁表中有什么
頁表是一個數(shù)據(jù)結構翰意,用于將虛擬頁號映射到物理幀號,在最簡單的一級分頁機制中信柿,就是一個數(shù)組,操作系統(tǒng)通過虛擬頁號來檢索數(shù)組醒第,獲取數(shù)組中的一項渔嚷,我們稱之為頁表項(PTE),根據(jù)PTE得到PFN等內容
其包含一個存在位(P)稠曼,確定是否允許寫入該頁面的讀/寫位(R/W) 形病,確定用戶模式進程是否可以訪問該頁面的用戶/超級用戶位(U/S),(PWT霞幅、PCD漠吻、PAT和G)確定硬件緩存如何為這些頁面工作,一個訪問位(A)和一個臟位(D)司恳,最后是頁幀號(PFN)本身途乃。
其中存在位表示該頁是在物理存儲器還是在磁盤上(即它已被換出),讀/寫位(R/W)以這些位不允許的方式訪問頁扔傅,會陷入操作系統(tǒng)耍共,臟位表明頁面被帶入內存后是否被修改過,有效位通常用于指示特定地址轉換是否有效猎塞。當一個程序開始運行時试读,它的代碼和堆在其地址空間的一端,棧在另一端荠耽。所有未使用的中間空間都將被標記為無效(invalid)钩骇,如果進程嘗試訪問這種內存,就會陷入操作系統(tǒng),可能會導致該進程終止倘屹。因此银亲,有效位對于支持稀疏地址空間至關重要。通過簡單地將地址空間中所有未使用的頁面標記為無效唐瀑,我們不再需要為這些頁面分配物理幀群凶,從而節(jié)省大量內存。
實際上頁表項跟段寄存器實現(xiàn)功能大同小異哄辣,存儲轉換的目標基址地址及相應的管理單位的各類狀態(tài)位
頁表存在的問題
前文所述请梢,進程的虛擬地址空間采用分頁機制會產(chǎn)生成千上萬的頁,而此時頁表也會變得非常大力穗,一個典型的32位地址空間毅弧,頁大小為4KB,虛擬地址這樣就會分割成12位的偏移量(4KB)和20位的VPN(尋址虛擬頁號)当窗,也就是說頁表需要最大能尋址到20位的虛擬頁號够坐,即頁表需包含2的20次方個頁表項,每個頁表項需要4字節(jié)(32位)崖面,那么一個進程的頁表就有4MB大小元咙,如果操作系統(tǒng)運行100個進程,那么內存中光頁表就需要存儲400MB內存巫员,如果是64位地址尋址空間庶香,那么頁表的大小將十分恐怖
段頁機制
為了解決分頁機制帶來的頁表體積太大的問題,出現(xiàn)了較多的解決方案简识,比如使用更大的頁赶掖,這樣尋址頁表項的壓力就會變小,頁表體積也相應變衅呷拧(更少的頁表項)奢赂,但是更大的頁不可避免地會產(chǎn)生內部碎片,大的內存頁可能會只被使用一部分(總會有這樣的內存頁)颈走,隨著系統(tǒng)運行膳灶,內存很快就會充滿這些較大的頁,因此立由,大多數(shù)操作系統(tǒng)在常見情況下使用較小的頁袖瞻,4KB(x86)或8KB。
混合的方法 分頁和分段
與分頁機制不同的是拆吆,我們不再為整個進程的地址空間提供單一頁表(那會有很多頁表項)聋迎,而是每個邏輯分段提供一個,如上圖枣耀,可能有三個頁表霉晕,地址空間的代碼庭再、堆、棧各有一個頁表牺堰,與分段機制不同的是拄轻,每一個段寄存器跟界限寄存器現(xiàn)在存儲的不再是段在物理內存的地址跟段的偏移,而是每個邏輯分段的頁表地址已經(jīng)每個頁表實際有效頁表項數(shù)伟葫,此時的頁表(每個段的頁表)存儲的不再是該段所有的頁恨搓,而是該段所有的有效頁,如上圖所示的物理頁PFN4筏养、PFN10斧抱、PFN23、PFN28渐溶,對應于虛擬頁VPN0(棧段內虛擬頁)辉浦、VPN0(代碼段內虛擬頁)、VPN0(堆段內虛擬頁)茎辐、VPN1(棧段內虛擬頁)宪郊,堆頁表有效頁表項數(shù)為1,棧頁表有效頁表項數(shù)為2拖陆,代碼頁表有效頁表項數(shù)為1弛槐。
如圖,段頁虛擬地址被分割成3部分依啰,分別是段標識位丐黄,段頁表項偏移,頁表內偏移
先根據(jù)虛擬地址獲取段號跟段頁表項偏移孔飒、頁表內偏移,根據(jù)段號尋找段寄存器得到段頁表基址艰争,根據(jù)段頁表基址加上段頁表項偏移得到段頁表項坏瞄,最后訪問頁表項得到頁基址加上頁表內偏移得出最后的物理地址
段頁機制解決了單一頁表太大的問題(分成多個段頁表,且僅僅保留有效的頁面)甩卓,但是還是沒有從根本上解決頁表大的問題鸠匀,試想這樣一種情況,存在一個大而稀疏的堆(大量內存分配逾柿、釋放結果)缀棍,對應的堆頁表還是可能會變得很大,當系統(tǒng)中遍布這樣的進程時机错,外部碎片又產(chǎn)生了爬范,因此,需要有一種更好地方式來實現(xiàn)更小的頁表
多級頁表
多級頁表的基本思想很簡單弱匪。首先青瀑,將頁表分成頁大小的單元(如果頁表項很多,那么內存上頁表將占用較多頁)。然后斥难,使用了名為頁目錄(page directory)的新結構統(tǒng)一管理頁表分成的每個單元枝嘶,如果一個單元內的頁表項(PTE)全部無效,就完全不分配該單元的頁表哑诊。
圖的左邊是經(jīng)典的線性頁表群扶。即使地址空間的大部分中間區(qū)域無效(有較多的無效頁表項),我們仍然需要為這些區(qū)域分配頁表空間(即頁表的中間兩頁)镀裤。右側是一個多級頁表竞阐。頁目錄僅將頁表的兩頁標記為有效(第一個和最后一個);因此淹禾,頁表的這兩頁就駐留在內存中(另外兩頁不分配內存)馁菜。因此,多級頁表讓線性頁表的一部分消失(釋放這些幀用于其他用途)铃岔,并用頁目錄來記錄頁表的哪些頁被分配汪疮。
在一個簡單的兩級頁表中,頁目錄為每頁頁表包含了一項毁习。它由多個頁目錄項(Page Directory Entries智嚷,PDE)組成。PDE(至少)擁有有效位(valid bit)和頁幀號(page frame number纺且,PFN)盏道,類似于PTE。但是载碌,正如上面所暗示的猜嘱,這個有效位的含義稍有不同:如果PDE項是有效的,則意味著該項指向的頁表(通過PFN)中頁表項至少有一項是有效的嫁艇,即在該PDE所指向的頁中朗伶,至少一個PTE,其有效位被設置為1步咪。如果PDE項無效(即等于零)论皆,則PDE的其余部分沒有定義。
與單一頁表相比猾漫,多級頁表對頁表進行單元管理点晴,若某個單元內(一頁)不存在有效頁表項,則在頁目錄內不記錄該單元的物理頁悯周,且頁表中也不會分配該頁粒督,頁表變得緊湊(去除了部分無效的單元),并且此時頁表由于被分割成一個個單元禽翼,更支持稀疏的地址空間坠陈,更容易管理萨惑,不再需要像單一頁表機制那樣在物理內存中尋找4MB的連續(xù)內存
多級頁表轉換示例
如圖展示了大小為16KB(14位)的小地址空間,頁的大小64字節(jié)仇矾,因此庸蔼,虛擬地址頁內偏移量為6位,虛擬頁號VPN有8位贮匕,即時在虛擬地址空間中只有一小部分空間被使用姐仅,線性頁表(單一頁表)也會有2的8次方(256)項頁表項,那么刻盐,如何構建二級頁表呢掏膏,首先頁表的總大小為256 * 4(字節(jié)) = 1KB,一個物理頁為64B敦锌,那么頁表總共需要1KB/64B = 16個物理頁馒疹,我們把頁表分成16個單元,每個單元由頁目錄管理乙墙,總共有16個頁目錄項颖变,需要4位尋址,而每個單元頁表項也是16個(64B/4B)听想,頁目錄索引+頁表索引(Page-Table Index,PTIndex) = VPN
頁目錄表索引加頁表索引得到頁表項腥刹,通過頁表項獲取頁基址加上頁內偏移最后得到物理地址。
多級頁表存在的問題
多級頁表采用的是時間換空間的做法汉买,在TLB(快速地址轉換)未命中時衔峰,需要從內存中加載至少兩次,才能從頁表中獲取正確的地址轉換信息(一次用于頁目錄蛙粘,另一次用于頁表項)垫卤,而線性頁表只需要一次加載,我們實現(xiàn)了更小的表(頁表單元管理)出牧,為了節(jié)省寶貴的內存穴肘,使頁表更加復雜。
分段與多級頁表結合
Linux作為現(xiàn)代通用操作系統(tǒng)崔列,使用了分頁機制(X86叫保護模式,arm叫MMU機制)來對用戶態(tài)與內核態(tài)進行隔離旺遮,也對進程與進程之前進行隔離赵讯。但是在X86 cpu架構下,使用分頁機制前耿眉,必須打開分段機制边翼。所以Linux采用了討巧的辦法,就是繞過分段機制鸣剪,直接使用分頁機制组底。 那Linux是怎么繞過分段機制的呢丈积? 很簡單,就是每個段都是0~4G的地址空間(相當于什么也沒有做一樣)债鸡,剩下的管理全由分頁機制來實現(xiàn)江滨。所以Linux內核在啟動時,自從開始使用了分頁機制時厌均,都先打開分段機制唬滑,并具這兩個功能一直使用。
而windows其實也是只靠分頁來隔離的棺弊,windows下虛擬地址=線性地址晶密,其CS,DS對應的段描述符實際指向的是同一個段(平坦模型)模她,只是使用了別名技術稻艰,順便規(guī)定了訪問權限,有興趣的同學可以深入探索侈净。