本期上半部分將和網友一起聊聊I386體系結構,認識一下Intel系統中的內存尋址和虛擬內存的來龍去脈。下半部分將實現一個最最短小的可啟動內核,一是加深對i386體系的了解,再就是演示系統開發的原始過程。作為實例我們將分析計算機的啟動流程,然后著手學習開發一個基于I386體系的可啟動系統
內存尋址
曾經有一個叫“阿蘭.圖靈”的天才,它設想出了一種簡單但運算能力幾乎無限發達的理想機器——這可不是一個具體的機械玩藝,而是一個思想模型——用來計算能想象得到的所有可計算函數。這個有趣的機器由一個控制器,一個讀寫頭和一條假設兩端無限長的帶子組成。工作帶好比存儲器,被劃分成大小相同的格子,每格上可寫一個字母,讀寫頭可以在工作帶上隨意移動,而控制器可以要求讀寫頭讀取其下方工作帶上的字母。
你可千萬別覺得這個機器傻得可愛,它可是當代馮.諾依曼體系計算機的理論鼻祖。它帶來的“數據連續存儲和選擇讀取思想”可是目前我們使用的幾乎所有機器運行背后的靈魂。計算機體系結構中的核心問題之一就是如何有效地進行內存尋址,因為所有運算的前提都是先要從內存中取得數據,所以內存尋址技術從某種程度上代表了了計算機技術。
下面就開始一起聊聊關于尋址的故事。
馮.諾依曼體系計算機系統由運算器、存儲器、控制器、輸入設備、輸出設備五大部件組成。運算器就是我們熟知的CPU中的AUL(算術邏輯單元),存儲器是內存,控制器是CPU中的控制單元;輸入設備就是我們的鼠標鍵盤等;輸出設備就是顯示器,打印機等。
歷史回顧
計算機的內存尋址技術和世界上的其它事物一樣都經歷了由簡單到復雜,由笨拙到優雅的過程。自我聽說計算機到今天,內存尋址方法發生了幾次決定性的變革(“史前”的內存尋址方法我連資料都沒由找到,真是無據可查了!),而每次變革都帶來了軟件技術的發展注入了新鮮血液。 讓我們沿著Intel公司的腳步來回顧一下歷史吧!(我實在沒機會接觸除Intel以外的處理器!!!)
石器時代
20年前intel推出了一款8位處理器——8080,它有1個主累加器(寄存器A)和6個次累加器(寄存器B,C,D,E,H和L),幾個次累加器可以配對(如組成BC, DE或HL)使用來訪問16位的內存地址,也就是說8080可訪問到64K內的地址空間。另外那時還沒有段的概念,訪問內存都要通過絕對地址,因此程序中的地址必須進行硬編碼,而且也難以重定位,因此當時的軟件大都是些可控性弱,結構簡陋,數據處理量小的工控程序。
人類從來都是不斷前進的,很快幾年后intel就開發出了16位的新處理器——8086,這便是內存尋址的第一次飛躍。
青銅時代
8086處理器引入了一個重要概念——段。段描述了一塊有限的內存區域,區域的起始位置存在專門的寄存器(段寄存器)中。另外8086處理器可以尋址到
系統所需要作的僅僅是:把16位的段地址左移動4位后,再與16位的偏移量相加便可獲得一個20位的內存地址,見圖1 摘要:本期上半部分將和網友一起聊聊I386體系結構,認識一下Intel系統中的內存尋址和虛擬內存的來龍去脈。下半部分將實現一個最最短小的可啟動內核,一是加深對i386體系的了解,再就是演示系統開發的原始過程。作為實例我們將分析計算機的啟動流程,然后著手學習開發一個基于I386體系的可啟動系統
內存尋址
曾經有一個叫“阿蘭.圖靈”的天才,它設想出了一種簡單但運算能力幾乎無限發達的理想機器——這可不是一個具體的機械玩藝,而是一個思想模型——用來計算能想象得到的所有可計算函數。這個有趣的機器由一個控制器,一個讀寫頭和一條假設兩端無限長的帶子組成。工作帶好比存儲器,被劃分成大小相同的格子,每格上可寫一個字母,讀寫頭可以在工作帶上隨意移動,而控制器可以要求讀寫頭讀取其下方工作帶上的字母。
你可千萬別覺得這個機器傻得可愛,它可是當代馮.諾依曼體系計算機的理論鼻祖。它帶來的“數據連續存儲和選擇讀取思想”可是目前我們使用的幾乎所有機器運行背后的靈魂。計算機體系結構中的核心問題之一就是如何有效地進行內存尋址,因為所有運算的前提都是先要從內存中取得數據,所以內存尋址技術從某種程度上代表了了計算機技術。
下面就開始一起聊聊關于尋址的故事。
馮.諾依曼體系計算機系統由運算器、存儲器、控制器、輸入設備、輸出設備五大部件組成。運算器就是我們熟知的CPU中的AUL(算術邏輯單元),存儲器是內存,控制器是CPU中的控制單元;輸入設備就是我們的鼠標鍵盤等;輸出設備就是顯示器,打印機等。
歷史回顧計算機的內存尋址技術和世界上的其它事物一樣都經歷了由簡單到復雜,由笨拙到優雅的過程。自我聽說計算機到今天,內存尋址方法發生了幾次決定性的變革(“史前”的內存尋址方法我連資料都沒由找到,真是無據可查了!),而每次變革都帶來了軟件技術的發展注入了新鮮血液。
讓我們沿著Intel公司的腳步來回顧一下歷史吧!(我實在沒機會接觸除Intel以外的處理器!!!)
石器時代 20年前intel推出了一款8位處理器——8080,它有1個主累加器(寄存器A)和6個次累加器(寄存器B,C,D,E,H和L),幾個次累加器可以配對(如組成BC, DE或HL)使用來訪問16位的內存地址,也就是說8080可訪問到64K內的地址空間。另外那時還沒有段的概念,訪問內存都要通過絕對地址,因此程序中的地址必須進行硬編碼,而且也難以重定位,因此當時的軟件大都是些可控性弱,結構簡陋,數據處理量小的工控程序。
人類從來都是不斷前進的,很快幾年后intel就開發出了16位的新處理器——8086,這便是內存尋址的第一次飛躍。
青銅時代8086處理器引入了一個重要概念——段。段描述了一塊有限的內存區域,區域的起始位置存在專門的寄存器(段寄存器)中。另外8086處理器可以尋址到1M大的內存空間,因為它的地址線擴展到了20位。可是制造20位的寄存器來存放內存地址在當時顯然要比制造20位的地址線難得多。為了克服困難,intel的工程師們想出了個好辦法:將內存分為數個64k大小的段,然后利用兩個16位值——一個是段地址,另一個是段內偏移量——巧妙組合產生20位的內存地址。換句話說就是把1M大的空間分成數個64k的段來管理(化整為零了)。
系統所需要作的僅僅是:把16位的段地址左移動4位后,再與16位的偏移量相加便可獲得一個20位的內存地址,見圖1
![]()
圖 1
Intel內存地址的描述形式也很貼近上圖,采用了“段地址:偏移量”的形式來描述內存地址,比如A815:CF2D就代表段首地址在A815,段內偏移位CF2D。
為了支持段機制,8086為程序使用的代碼段,數據段,堆棧段分別提供了專門的16位寄存器CS,DS和SS,此外還給內存和字符串拷貝操作留下了一個目的段寄存器:ES。
段式內存管理帶來了顯而易見的優勢——程序的地址不再需要硬編碼了,調試錯誤也更容易定位了,更可貴的是支持更大的內存地址。程序員開始獲得了自由。
白銀時代
人們的欲望在繼續膨脹。intel的80286處理器于1982年問世了,它的地址總線位數增加到了24位,因此可以訪問到
為了和過去兼容,80286內存尋址可以以兩種方式進行,一種是先進的保護模式,另一種是老式的8086方式,被成為實模式。啟動時候處理器處于實模式只能訪問
黃金時代
真正的第二次內存尋址飛躍發生在80386身上,它近乎完美的設計將計算機技術推向了一個新高度.
80386的地址總線擴展到了32位,可尋址空間一下擴充為
保護模式
保護模式真得是太精妙了,我恨不得用專門用一本書來討論它,但即使那樣我也擔心不能真正觸其精華。不過還是借用那句老話”簡單就是美麗”,我爭取用最小的篇幅揭示保護模式的真實面目。
實模式和保護模式 保護模式和實模式好比一對孿生兄弟,它們外貌很像,從程序角度來看幾乎看不出什么區別,它們都是通過段寄存器去訪問內存地址,都通過中斷和設備驅動程序來操作硬件,表面上能感覺得到的差異就是保護模式能訪問的空間是
但實際上保護模式和實模式采用了兩種截然不同的思路,保護模式帶來的最可貴的優點不是單純的擴大了內存尋址范圍,而是對內存尋址從機制上提供了保護,將系統的執行空間按權限進行了化分。
這種劃分到底會帶來那些好處啦? 我們來推敲一下吧。
為什么需要保護?如果你的機器只允許一個任務使用系統資源,比如說系統內存,那么你完全不需要保護資源,因為系統中再沒有什么值得你去偷窺的東西了,更別說去破壞什么了。
可惜那樣的時代已經一去不復返了,如今的系統需要支持多個用戶同時運行多個任務。為了防止你去偷看別人的任務,或惡意或由于你的荒唐行為而破壞別人的任務,系統有責任將每個任務隔離開來,讓大家各自運行在不同的空間,免收干擾。這就是保護的第一個方面——任務間保護,要做到任務間保護需要借助虛擬內存技術(我們后面分析它),其基礎之一就是保護模式。
除了任務間保護外,另一個必須保護的東西就是操作系統本身,它可是資源調配的首腦呀!決不能讓你有機可承,擅自進入。必須有一道鐵絲網,將你和操作系統隔離開,使你不得越雷池一步。要想拉起這道鐵絲網,就需要借助保護模式中的特權級機制。操作系統放在高特權級里,你的任務被放在低特權級里。你沒有權利去偷看操作系統的內容。有什么要求只能請示“領導”(就是保護機制),獲得拼準后才能給你提供服務。這點可謂是保護模式的最直接應用。
誰賦予它保護能力?80386之所以能有變化多端的保護手段,追其根本源自保護模式下內存尋址方式發生革命。傳統上我們知道段方式尋址時,是直接從段寄存器中取得的段的首地址,但是在保護模式中是要多經過一次檢查手續才能獲得想要的段地址。
這里可千萬別再說“簡單就是美了”,多了這一次中間倒手過程可是保護模式下尋址的關鍵技術所在呀。倒手的原因我想大概是因為,雖然80386有的通用寄存器(EAX,EDI等等)被擴充倒了32位,但是其中的段寄存器(DS,ES等)仍然只有16位,顯然不可能再用16位的段寄存器直接存放 線性地址屬于中間地址,它還需要一次轉換才能映射到實際的物理地址上(下面會看到)。線性地址長成的空間稱為線性空間,它和物理地址空間結構想同,都為32位,最大可達
這個索引指針被稱作是段選擇子(見圖2),它共有16位,其中14位用來作為索引,另外2位(RPL)用來作描述請求特權級。通過索引從表中獲得的信息,被稱為段描述符,它含有段的相關地址信息等。
改變尋址方法的另一個原因主要是為了完成保護使命。多用戶多任務環境下,內存尋地工作不再是簡單地取得32位的內存地址就可以直接不假思索地放到地址總線上去了讀寫內存了,此刻必須先要對需訪問的地址進行合法性檢查,看看訪問者是不是有權利去訪問它要求的地址。如果發現有非法訪問企圖,則立刻阻止(CPU會產生一個一異常)這種危險行為。讀到這里,多數的朋友一定要問,靠什么進行檢查請求的合法性呢?更細心的朋友還會繼續問,檢查需要什么信息?這些信息放在那里?
考慮到尋址過程和合法性檢測過程需要在同一現場一起進行,所以最理想是能把段地址信息和檢測合法性用到的屬性信息能放在一起(需要的空間更大了),于是系統設計師門便把屬性信息和段的基地址和界限都柔和在了一起,形成了一個新的信息單元——段描述符號,它整整占用了8個字節。顯然寄存器太小,不夠存放段描述符,所以段描述符都被統一存在專門的系統段描述符號表中(GTD或LDT)保存。
說到這里,聰明的朋友可能已經能大概猜出段描述符表中的內容是什么了。內容里一定包含了段基地址、短的大小信息、段的屬性信系,而且在屬性信息里包含了還有和訪問權限有關的信息。的確如此,下面圖示描述了段描述符的詳細信息,其中和保護關心最大的信息要數DPL了(見圖3)。
這種間接尋址方式不僅體現在普通任務尋址上,而且對于中斷處理同樣適用。傳統上中斷處理查詢方法是在中斷產生后,CPU會在中斷向量表中搜索中斷服務例程(ISR)的地址,地址形式還是段+偏移量。在保護模式中中斷產生后,CPU會從中斷描述符表(IDT)中根據中斷號取得中斷服務例程的段選擇子和偏移量,然后通過段選擇子從段描述附表(GDT)中獲得ISR的段信息再結合偏移量得到需要的實際物理地址。
中斷尋址過程如圖4 特權等級 計算機世界和人類世界一樣最初是沒有等級之分的,但當人類社會物質文明逐步發達后,等級也隨之而來了;同樣當計算機上的應用軟件越來月豐富后,這個虛擬世界也逐漸形成了級別等級。我們不去評價人類社會等級制度,我們只來看看計算機世界中的等級制度,而且只陷于保護模式中的等級制度。
80386中共規定有4個特權級,由0到3。0級別權限最高,3級最小。標準的作法是將操作系統核心運行在0級,應用程序運行其它幾個低級別。不過為了簡化操作,往往只會用到0和3兩個級別。80386中的每個段描述符號中都有DPL字段,它規定了訪問該段的最低特權級,只用高于次特權級別的程序能有權訪問它。所以在訪問內存地址時要將當前特權級(CPL,一般來說就是當前代碼段的特權級別)和被訪問段的特權級別比較,如果大于等于才允許訪問。
處理當前特權級別和段的特權級別外,有時還需要使用請求特權級別(RPL),這個子段出自段選擇字,主要用來輔助特權保護。比如可以在訪問某個段時,指定其請求特權級,那么特權檢查時,規則變為將RPL和CPL中特權更高的那個和被訪問段的DPL比較。例如,操作系統中的某個例程會把一些資料寫到用戶段中。若沒有特別檢查,那么用戶可以把一個 DPL為 0 的 段(用戶程序不能存取它)傳到操作系統處理例程中,因為系統例程有全權寫入DPL為0的段,因此用戶程序就可以破壞該段中的資料了。為了避免這個問題,系統 API 在存取用戶傳入的段時,可以先把該段選擇子的 RPL設定成和用戶程序的 CPL 相同,就不會意外寫入原先用戶無權存取的段了。 (但RPL在linux好像沒被怎么用到)
虛擬內存
虛擬內存可是個怎么強調也不過分的概念,它的存在極大地方便了程序設計任務,徹底解放了程序員的手腳。下面我們就看看虛擬內存的作用以及如何在存儲管理機制的基礎上實現它。
什么是虛存?為什么需要它? 我們知道程序代碼和數據必須駐留在內存中才能得以運行,然而系統內存數量很有限,往往不能容納一個完整程序的所有代碼和數據,更何況在多任務系統中,可能需要同時打開子處理程序,畫圖程序,瀏覽器等很多任務,想讓內存駐留所有這些程序顯然不大可能。因此我們能首先能想到的就是將程序分割成小分,只讓當前系統運行它所有需要的那部分留在內存,其它部分都留在硬盤。當系統處理完當前任務片段后,再從外存中調入下一個待運行的任務片段。的確老式系統的確這樣處理大任務,而且這個工作是由程序員自行完成。但是隨之程序語源越來越高級,程序員對系統體系的依賴程度降低了,很少有程序員能非常清楚的駕馭系統體系了,因此放手讓程序員負責將程序片段化和按需調入輕則降低效率,重則使得機器崩潰;再一個原因是隨程序越來越豐富,程序行為幾乎無法準確預測,程序員自己都很難判斷下一步需要載入那段程序。因此很難再靠預見性靜態分配固定大小的內存,然后再機械地輪換程序片進內存執行。系統必須采取一種能按需分配,不要程序員干預地新技術。
虛擬內存[1]技術就是一種由操作系統接管的按需動態內存分配方法,它允許程序不知不覺種使用大于實際物理的存儲空間(其實是將程序需要的存儲空間以頁的形式分散存儲在物理內存和磁盤上),所以說虛擬內存徹底解放了程序員,從此程序員不用過分關心程序大小和載入,可以自由編寫程序了,繁瑣的事情都交給操作系統去作吧。
實現虛擬內存 虛擬內存是將系統硬盤空間和系統實際內存聯合在一起為進程使用,給進程提供了一個比內存大的多的虛擬空間。在程序運行時,只把虛擬地址空間的一小部分映射到內存,其余都存儲在硬盤上(也就是說程序虛擬空間就等于實際物理內存加部分硬盤空間)。當訪問被訪問的虛擬地址的不在內存時,則說明該地址未被映射到內存,而是被存貯在硬盤中,因此需要的虛擬存儲地址被隨即調入到內存;同時當系統內存緊張時,也可以把當前不用的虛擬存儲空間換出到硬盤,來騰出物理內存空間。系統如此周而復始地運轉——換入、換出,而用戶幾乎無法查覺,這都是拜虛擬內存機制所賜。
Linux的swap分區就是硬盤專門為虛擬存儲空間預留的空間。經驗大小應該是內存的兩倍左右。有興趣的話可以使用 swapon -s 查看交換分區大小,還可以用vmstat 查看當前每秒換入換出的數據大小(在si/so字段下)
大道理很好理解,無非是用內存和硬盤空間合成為虛擬內存空間。但是這一過程中反復運行的地址映射(虛擬地址映射到物理地址)和虛擬地址換入換出卻值得仔細推敲。系統到底是怎么樣吧虛擬地址映射到物理地址上的呢?內存又如何能不斷的和硬盤之間換入換出虛擬地址呢?
利用段機制能否回答上述問題呢?我們上面提到過邏輯地址通過段機制后變為一個32位的地址,足以覆蓋
因為使用頁機制的原因,通過段機制轉換得到的地址僅僅是作為一個中間地址——線性地址了,該地址不代表實際物理地址,而是代表整個進程的虛擬空間地址。在線性地址的基礎上,頁機制接著會處理線性地址映射:當需要的線性地址(虛擬空間地址)不在內存時,便以頁為單位從磁盤中調入需要的虛擬內存;當內存不夠時,又會以頁為單位把內存中虛擬空間的換出到磁盤上。可見利用頁來管理內存和磁盤(虛擬內存)大大方便了內存管理的工作。毫無疑問頁機制是虛擬內存管理簡直是“天配”。
使用頁機制,
前面我們提到了線性地址是32位。它其中高20位是對頁表的索引,低12位則給出了頁面中的偏移。線性地址經過頁表找到頁框基地址后和低12位偏移量相加就形成了最終需要的物理地址了。
在實際使用中,并非所有頁表項都是被存放在一個大頁表里,因為每個頁表項4字節,如果要在一個表中存放2的20次方個頁表項,就需要
兩級頁表搜索如同看章回小說,先找到在那一章里,然后在找在該章下那一節。具體過程看看下圖5。
[1] .之所以稱為虛擬內存是和系統中的邏輯內存和物理內存而言的,邏輯內存是站在進程角度看到的內存,因此是程序員關心的內容。而物理內存是站在處理器角度看到的內存,由操作系統負責管理。虛擬內存可以說是這映射這兩種不同視角內存的一個技術手段。
綜上所述。地址轉換工作需要兩種技術,一是段機制,二是頁機制。段機制處理邏輯地址向線性地址映;頁機制則負責把線性地址映射為物理地址。兩級映射一同完成了從程序員看到的邏輯地址轉換到處理器看到的物理地址這一艱巨任務。
你可以將這兩種機制分別比作一個地址轉換函數,段機制的變量是邏輯地址,函數值是線性地址;頁機制的變量是線性地址,函數值是物理地址。地址轉換過程如下所示。
邏輯地址——(段函數)——>線性地址——(頁函數)——>物理地址。
雖然段機制和頁機制都參與映射,但它們分工不同,而且相互獨立互不干擾,彼此之間不必知道對方是否存在。
說了這么多道理,下面我們結合Linux實例簡要地看看段頁機制如何使用。
Linux分段段策略
段機制在Linux里用得有限,并沒有被完全利用。每個任務并未分別安排各自獨立的數據段,代碼段,而是僅僅最低限度的利用段機制來隔離用戶數據和系統數據——Linux只安排了四個范圍一樣的段,內核數據段,內核代碼段,用戶數據段,用戶代碼段,它們都覆蓋0
每個用戶進程都可以看到
說到特權切換,就離不開任務門,陷阱門/中斷門等概念。陷阱門和中斷門是在發生陷阱和中斷時,進入內核空間的通道。調用門是用戶空間程序相互訪問時所需要的通道,任務門比較特殊,它不含如何地址,而是服務于任務切換(但linux任務切換時并未真正采用它,它太麻煩了)。
對于各種門系統都會有對應的門描述符,和段描述符結構類似,門描述符也是由對應的門選擇字索引,并且最終會產生一個指向特定段內偏移地址的指針。這個指針就指向的是將要進入的入口。利用門的目的就是保證入口可控,不至于進入到內核中不該訪問的位置(回憶前面講到的中斷服務程序尋址,其中從中斷描述符號表中獲得的就是中斷門的描述符,而描述符則制定了具體的入口位置)。
Linux中的分頁策略
我們前面大概談了談為什么要使用分頁,這里看看linux中如何使用分頁。
Linux中每個進程都會有個自的不同的頁表,也就是說進程的映射函數互不相同,保證每個進程虛擬地址不會映射到相同的物理地址上。這是因為進程之間必須相互獨立,各自的數據必須隔離,防止信息泄漏。
另外需要注意的是,內核作為必須保護的單獨部分,它有自己獨立的頁表來映射內核空間(并非全部空間,僅僅是物理內存大小的空間),該頁表(swapper_pg_dir)被靜態分配,它只來映射內核空間(swapper_pg_dir只用到768項以后的項——768個頁目錄可映射
那么在用戶進程需要訪問內核空間時如何作呢?
Linux采用了個巧妙的方法:用戶進程頁表的前768項映射進程空間(<
|
|
來自: skywood > 《Daily Study》