|
下面是網(wǎng)上看到的一些關(guān)于內(nèi)存和CPU方面的一些很不錯(cuò)的文章. 整理如下: 轉(zhuǎn): CPU的等待有多久? 原文標(biāo)題:What Your Computer Does While You Wait [注:本人水平有限,只好挑一些國(guó)外高手的精彩文章翻譯一下。一來(lái)自己復(fù)習(xí),二來(lái)與大家分享。] 本文以一個(gè)現(xiàn)代的、實(shí)際的個(gè)人電腦為對(duì)象,分析其中CPU(Intel Core 2 Duo 3.0GHz)以及各類子系統(tǒng)的運(yùn)行速度——延遲和數(shù)據(jù)吞吐量。通過(guò)粗略的估算PC各個(gè)組件的相對(duì)運(yùn)行速度,希望能給大家留下一個(gè)比較直觀的印象。本文中的數(shù)據(jù)來(lái)自實(shí)際應(yīng)用,而非理論最大值。時(shí)間的單位是納秒(ns,十億分之一秒),毫秒(ms,千分之一秒),和秒(s)。吞吐量的單位是兆字節(jié)(MB)和千兆字節(jié)(GB)。讓我們先從CPU和內(nèi)存開(kāi)始,下圖是北橋部分:
第一個(gè)令人驚嘆的事實(shí)是:CPU快得離譜。在Core 2 3.0GHz上,大部分簡(jiǎn)單指令的執(zhí)行只需要一個(gè)時(shí)鐘周期,也就是1/3納秒。即使是真空中傳播的光,在這段時(shí)間內(nèi)也只能走10厘米(約4英寸)。把上述事實(shí)記在心中是有好處的。當(dāng)你要對(duì)程序做優(yōu)化的時(shí)候就會(huì)想到,執(zhí)行指令的開(kāi)銷對(duì)于當(dāng)今的CPU而言是多么的微不足道。 當(dāng)CPU運(yùn)轉(zhuǎn)起來(lái)以后,它便會(huì)通過(guò)L1 cache和L2 cache對(duì)系統(tǒng)中的主存進(jìn)行讀寫(xiě)訪問(wèn)。cache使用的是靜態(tài)存儲(chǔ)器(SRAM)。相對(duì)于系統(tǒng)主存中使用的動(dòng)態(tài)存儲(chǔ)器(DRAM),cache讀寫(xiě)速度快得多、造價(jià)也高昂得多。cache一般被放置在CPU芯片的內(nèi)部,加之使用昂貴高速的存儲(chǔ)器,使其給CPU帶來(lái)的延遲非常低。在指令層次上的優(yōu)化(instruction-level optimization),其效果是與優(yōu)化后代碼的大小息息相關(guān)。由于使用了高速緩存技術(shù)(caching),那些能夠整體放入L1/L2 cache中的代碼,和那些在運(yùn)行時(shí)需要不斷調(diào)入/調(diào)出(marshall into/out of)cache的代碼,在性能上會(huì)產(chǎn)生非常明顯的差異。 正常情況下,當(dāng)CPU操作一塊內(nèi)存區(qū)域時(shí),其中的信息要么已經(jīng)保存在L1/L2 cache,要么就需要將之從系統(tǒng)主存中調(diào)入cache,然后再處理。如果是后一種情況,我們就碰到了第一個(gè)瓶頸,一個(gè)大約250個(gè)時(shí)鐘周期的延遲。在此期間如果CPU沒(méi)有其他事情要做,則往往是處在停機(jī)狀態(tài)的(stall)。為了給大家一個(gè)直觀的印象,我們把CPU的一個(gè)時(shí)鐘周期看作一秒。那么,從L1 cache讀取信息就好像是拿起桌上的一張草稿紙(3秒);從L2 cache讀取信息則是從身邊的書(shū)架上取出一本書(shū)(14秒);而從主存中讀取信息則相當(dāng)于走到辦公樓下去買(mǎi)個(gè)零食(4分鐘)。 主存操作的準(zhǔn)確延遲是不固定的,與具體的應(yīng)用以及其他許多因素有關(guān)。比如,它依賴于列選通延遲(CAS)以及內(nèi)存條的型號(hào),它還依賴于CPU指令預(yù)取的成功率。指令預(yù)取可以根據(jù)當(dāng)前執(zhí)行的代碼來(lái)猜測(cè)主存中哪些部分即將被使用,從而提前將這些信息載入cache。 看看L1/L2 cache的性能,再對(duì)比主存,就會(huì)發(fā)現(xiàn):配置更大的cache或者編寫(xiě)能更好的利用cache的應(yīng)用程序,會(huì)使系統(tǒng)的性能得到多么顯著的提高。如果想進(jìn)一步了解有關(guān)內(nèi)存的諸多信息,讀者可以參閱Ulrich Drepper所寫(xiě)的一篇經(jīng)典文章《What Every Programmer Should Know About Memory》。 人們通常把CPU與內(nèi)存之間的瓶頸叫做馮·諾依曼瓶頸(von Neumann bottleneck)。當(dāng)今系統(tǒng)的前端總線帶寬約為10GB/s,看起來(lái)很令人滿意。在這個(gè)速度下,你可以在1秒內(nèi)從內(nèi)存中讀取8GB的信息,或者10納秒內(nèi)讀取100字 節(jié)。遺憾的是,這個(gè)吞吐量只是理論最大值(圖中其他數(shù)據(jù)為實(shí)際值),而且是根本不可能達(dá)到的,因?yàn)橹鞔婵刂齐娐窌?huì)引入延遲。在做內(nèi)存訪問(wèn)時(shí),會(huì)遇到很多零 散的等待周期。比如電平協(xié)議要求,在選通一行、選通一列、取到可靠的數(shù)據(jù)之前,需要有一定的信號(hào)穩(wěn)定時(shí)間。由于主存中使用電容來(lái)存儲(chǔ)信息,為了防止因自然 放電而導(dǎo)致的信息丟失,就需要周期性的刷新它所存儲(chǔ)的內(nèi)容,這也帶來(lái)額外的等待時(shí)間。某些連續(xù)的內(nèi)存訪問(wèn)方式可能會(huì)比較高效,但仍然具有延時(shí)。而那些隨機(jī) 的內(nèi)存訪問(wèn)則消耗更多時(shí)間。所以延遲是不可避免的。 圖中下方的南橋連接了很多其他總線(如:PCI-E, USB)和外圍設(shè)備:
令人沮喪的是,南橋管理了一些反應(yīng)相當(dāng)遲鈍的設(shè)備,比如硬盤(pán)。就算是緩慢的系統(tǒng)主存,和硬盤(pán)相比也可謂速度如飛了。繼續(xù)拿辦公室做比喻,等待硬盤(pán)尋道的時(shí)間相當(dāng)于離開(kāi)辦公大樓并開(kāi)始長(zhǎng)達(dá)一年零三個(gè)月的環(huán)球旅行。這就解釋了為何電腦的大部分工作都受制于磁盤(pán)I/O,以及為何數(shù)據(jù)庫(kù)的性能在內(nèi)存緩沖區(qū)被耗盡后會(huì)陡然下降。同時(shí)也解釋了為何充足的RAM(用于緩沖)和高速的磁盤(pán)驅(qū)動(dòng)器對(duì)系統(tǒng)的整體性能如此重要。 雖然磁盤(pán)的'連續(xù)'存取速度確實(shí)可以在實(shí)際使用中達(dá)到,但這并非故事的全部。真正令人頭疼的瓶頸在于尋道操作,也就是在磁盤(pán)表面移動(dòng)讀寫(xiě)磁頭到正確的磁道上,然后再等待磁盤(pán)旋轉(zhuǎn)到正確的位置上,以便讀取指定扇區(qū)內(nèi)的信息。RPM(每分鐘繞轉(zhuǎn)次數(shù))用來(lái)指示磁盤(pán)的旋轉(zhuǎn)速度:RPM越大,耽誤在尋道上的時(shí)間就越少,所以越高的RPM意味著越快的磁盤(pán)。這里有一篇由兩個(gè)Stanford的研究生寫(xiě)的很酷的文章,其中講述了尋道時(shí)間對(duì)系統(tǒng)性能的影響:《Anatomy of a Large-Scale Hypertextual Web Search Engine》 當(dāng) 磁盤(pán)驅(qū)動(dòng)器讀取一個(gè)大的、連續(xù)存儲(chǔ)的文件時(shí)會(huì)達(dá)到更高的持續(xù)讀取速度,因?yàn)槭∪チ藢さ赖臅r(shí)間。文件系統(tǒng)的碎片整理器就是用來(lái)把文件信息重組在連續(xù)的數(shù)據(jù)塊 中,通過(guò)盡可能減少尋道來(lái)提高數(shù)據(jù)吞吐量。然而,說(shuō)到計(jì)算機(jī)實(shí)際使用時(shí)的感受,磁盤(pán)的連續(xù)存取速度就不那么重要了,反而應(yīng)該關(guān)注驅(qū)動(dòng)器在單位時(shí)間內(nèi)可以完 成的尋道和隨機(jī)I/O操作的次數(shù)。對(duì)此,固態(tài)硬盤(pán)可以成為一個(gè)很棒的選擇。 硬盤(pán)的cache也有助于改進(jìn)性能。雖然16MB的cache只能覆蓋整個(gè)磁盤(pán)容量的0.002%,可別看cache只有這么一點(diǎn)大,其效果十分明顯。它可以把一組零散的寫(xiě)入操作合成一個(gè),也就是使磁盤(pán)能夠控制寫(xiě)入操作的順序,從而減少尋道的次數(shù)。同樣的,為了提高效率,一系列讀取操作也可以被重組,而且操作系統(tǒng)和驅(qū)動(dòng)器固件(firmware)都會(huì)參與到這類優(yōu)化中來(lái)。 最后,圖中還列出了網(wǎng)絡(luò)和其他總線的實(shí)際數(shù)據(jù)吞吐量?;鹁€(fireware)僅供參考,Intel X48芯片組并不直接支持火線。我們可以把Internet看作是計(jì)算機(jī)之間的總線。去訪問(wèn)那些速度很快的網(wǎng)站(比如google.com),延遲大約45毫秒,與硬盤(pán)驅(qū)動(dòng)器帶來(lái)的延遲相當(dāng)。事實(shí)上,盡管硬盤(pán)比內(nèi)存慢了5個(gè)數(shù)量級(jí),它的速度與Internet是在同一數(shù)量級(jí)上的。目前,一般家用網(wǎng)絡(luò)的帶寬還是要落后于硬盤(pán)連續(xù)讀取速度的,但'網(wǎng)絡(luò)就是計(jì)算機(jī)'這句話可謂名符其實(shí)。如果將來(lái)Internet比硬盤(pán)還快了,那會(huì)是個(gè)什么景象呢? 我希望這些圖片能對(duì)您有所幫助。當(dāng)這些數(shù)字一起呈現(xiàn)在我面前時(shí),真的很迷人,也讓我看到了計(jì)算機(jī)技術(shù)發(fā)展到了哪一步。前文分開(kāi)的兩個(gè)圖片只是為了敘述方便,我把包含南北橋的整張圖片也貼出來(lái),供您參考。
參考: http://blog.csdn.net/drshenlei/article/details/4240703 轉(zhuǎn): CPU如何操作內(nèi)存 原文標(biāo)題:Getting Physical With Memory [注:本人水平有限,只好挑一些國(guó)外高手的精彩文章翻譯一下。一來(lái)自己復(fù)習(xí),二來(lái)與大家分享。] 在你試圖理解一個(gè)復(fù)雜的系統(tǒng)時(shí),如果能揭去表面的抽象并專注于最低級(jí)別的概念,往往會(huì)有不小的收獲。在這個(gè)精神的指導(dǎo)下,讓我們看看對(duì)于內(nèi)存和I/O端口操作來(lái)說(shuō)最簡(jiǎn)單、最基礎(chǔ)的概念,即CPU與總線之間的接口。其中的細(xì)節(jié)是很多上層概念的基礎(chǔ),比如線程同步。當(dāng)然了,既然我是個(gè)程序員,就暫且忽略那些只有電子工程師才會(huì)去關(guān)注的東西吧。下圖是我們的老朋友,Core 2:
Core 2 處理器有775個(gè)管腳,其中約半數(shù)僅僅用于供電而不參與數(shù)據(jù)傳輸。當(dāng)你把這些管腳按照功能分類后,就會(huì)發(fā)現(xiàn)這個(gè)處理器的物理接口驚人的簡(jiǎn)單。本圖展示了參與內(nèi)存和I/O端口操作的重要管腳:地址線,數(shù)據(jù)線,請(qǐng)求線。這些操作均發(fā)生在前端總線的事務(wù)上下文結(jié)構(gòu)(the context of a transaction)中。前端總線事務(wù)的執(zhí)行包含五個(gè)階段:仲裁,請(qǐng)求,偵聽(tīng),響應(yīng),數(shù)據(jù)操作。在執(zhí)行事務(wù)的過(guò)程中,前端總線上的各個(gè)部件扮演著不同的角色。這些部件稱之為agent。通常,agent就是全部的處理器外加北橋。 本文只分析請(qǐng)求階段。在此階段中,發(fā)出請(qǐng)求的agent往往是一個(gè)處理器,它輸出兩個(gè)數(shù)據(jù)包。下圖列出了第一個(gè)數(shù)據(jù)包中最為重要的位,這些數(shù)據(jù)位通過(guò)處理器的地址線和請(qǐng)求線輸出:
地址線輸出指定了事務(wù)發(fā)生的物理內(nèi)存起始地址。我們有33條地址線,他們指定了數(shù)據(jù)包的第35至第3位,第2至第0位為0。因此,實(shí)際上這33條地址線構(gòu)成了一個(gè)36位的、以8字節(jié)對(duì)齊的地址,正好覆蓋64GB的物理內(nèi)存。這種設(shè)定從奔騰Pro就開(kāi)始了。請(qǐng)求線指定了事務(wù)的類型。當(dāng)事務(wù)類型為I/O請(qǐng)求時(shí),地址線指出的是I/O端口地址而不是內(nèi)存地址。當(dāng)?shù)谝粋€(gè)數(shù)據(jù)包被發(fā)送以后,同樣由這組管腳,在下一個(gè)總線時(shí)鐘周期發(fā)送第二個(gè)數(shù)據(jù)包:
屬性信號(hào)(attribute signal A[31:24])很有趣,它反映了Intel處理器所支持的5種內(nèi)存緩沖功能。把這些信息發(fā)布到前端總線后,發(fā)出請(qǐng)求的agent就可以讓其他處理器知道如何根據(jù)當(dāng)前事務(wù)處理他們自己的cache,以及讓內(nèi)存控制器(也就是北橋)知道該如何應(yīng)對(duì)。一塊指定內(nèi)存區(qū)域的緩存類型由處理器通過(guò)查詢頁(yè)表(page table)來(lái)決定,頁(yè)表由OS內(nèi)核維護(hù)。 典型的情況是,內(nèi)核把全部?jī)?nèi)存都視為'回寫(xiě)'類型(write-back),從而獲得最好的性能。在回寫(xiě)模式下,內(nèi)存的最小訪問(wèn)單元為一個(gè)緩存線(cache line),在Core 2中是64字節(jié)。當(dāng)程序想讀取內(nèi)存中的一個(gè)字節(jié)時(shí),處理器會(huì)從L1/L2 cache讀取包含此字節(jié)的整條緩存線的內(nèi)容。當(dāng)程序做寫(xiě)入內(nèi)存操作時(shí),處理器只是修改cache中的對(duì)應(yīng)緩存線,而不會(huì)更新主存中的信息。之后,當(dāng)真的需要更新主存時(shí),處理器會(huì)把那個(gè)被修改了的緩存線整體放到總線上,一次性寫(xiě)入內(nèi)存。所以大部分的請(qǐng)求事務(wù),其數(shù)據(jù)長(zhǎng)度字段都是11(REQ[1:0]),對(duì)應(yīng)64 字節(jié)。下圖展示了當(dāng)cache中沒(méi)有對(duì)應(yīng)數(shù)據(jù)時(shí),內(nèi)存讀取訪問(wèn)的過(guò)程:
在Intel計(jì)算機(jī)上,有些物理內(nèi)存范圍被映射為設(shè)備地址而不是實(shí)際的RAM存儲(chǔ)器地址,比如硬盤(pán)和網(wǎng)卡。這使得驅(qū)動(dòng)程序可以像讀寫(xiě)內(nèi)存那樣,方便的與設(shè)備通信。內(nèi)核會(huì)在頁(yè)表中標(biāo)記出這類內(nèi)存映射區(qū)域?yàn)?strong>不可緩存的(uncacheable)。對(duì)不可緩存的內(nèi)存區(qū)域的訪問(wèn)操作會(huì)被總線原封不動(dòng)的按順序執(zhí)行,其操作與應(yīng)用程序或驅(qū)動(dòng)程序所發(fā)出的請(qǐng)求完全一致。因此,這時(shí)程序可以精確控制讀寫(xiě)單個(gè)字節(jié)、字、或其它長(zhǎng)度的信息。這都是通過(guò)設(shè)置第二個(gè)數(shù)據(jù)包中的字節(jié)使能掩碼(byte enable mask A[15:8])來(lái)完成的。 前面討論的這些基本知識(shí)還包含很多關(guān)聯(lián)的內(nèi)容。比如: 1、 如果應(yīng)用程序想要盡可能高的運(yùn)行速度,就應(yīng)該把會(huì)被一起訪問(wèn)的數(shù)據(jù)盡量組織在同一條緩存線中。一旦這條緩存線被載入,之后的讀取操作就會(huì)加快很多,不再需要額外的內(nèi)存訪問(wèn)了。 2、 對(duì)于回寫(xiě)式內(nèi)存訪問(wèn),作用于一條緩存線的任何內(nèi)存操作都一定是原子的(atomic)。這種能力是由處理器的L1 cache提供的,所有數(shù)據(jù)被同時(shí)讀寫(xiě),中途不會(huì)被其他處理器或線程打斷。特別的,32位和64位的內(nèi)存操作,只要不跨越緩存線的邊界,就都是原子操作。 3、 前端總線是被所有的agent所共享的。這些agent在開(kāi)啟一個(gè)事務(wù)之前,必須先進(jìn)行總線使用權(quán)的仲裁。而且,每一個(gè)agent都需要偵聽(tīng)總線上所有的事務(wù),以便維持cache的一致性。因此,隨著部署更多的、多核的處理器到Intel計(jì)算機(jī),總線競(jìng)爭(zhēng)問(wèn)題會(huì)變得越來(lái)越嚴(yán)重。為解決這個(gè)問(wèn)題,Core i7將處理器直接連接于內(nèi)存,并以點(diǎn)對(duì)點(diǎn)的方式通信,取代之前的廣播方式,從而減少總線競(jìng)爭(zhēng)。 本 文講述的都是有關(guān)物理內(nèi)存請(qǐng)求的重要內(nèi)容。當(dāng)涉及到內(nèi)存鎖定、多線程、緩存一致性的問(wèn)題時(shí),總線這個(gè)角色又將浮出水面。當(dāng)我第一次看到前端總線數(shù)據(jù)包的描 述時(shí),會(huì)有種恍然大悟的感覺(jué),所以我希望您也能從本文中獲益。下一篇文章,我們將從底層爬回到上層去,研究一個(gè)抽象概念:虛擬內(nèi)存。 參考: http://blog.csdn.net/drshenlei/article/details/4243733 [轉(zhuǎn)]: 主板芯片組與內(nèi)存映射 原文標(biāo)題:Motherboard Chipsets and the Memory Map [注:本人水平有限,只好挑一些國(guó)外高手的精彩文章翻譯一下。一來(lái)自己復(fù)習(xí),二來(lái)與大家分享。] 我打算寫(xiě)一組講述計(jì)算機(jī)內(nèi)幕的文章,旨在揭示現(xiàn)代操作系統(tǒng)內(nèi)核的工作原理。我希望這些文章能對(duì)電腦愛(ài)好者和程序員有所幫助,特別是對(duì)這類話題感興趣但沒(méi)有相關(guān)知識(shí)的人們。討論的焦點(diǎn)是Linux,Windows,和Intel處理器。鉆研系統(tǒng)內(nèi)幕是我的一個(gè)愛(ài)好。我曾經(jīng)編寫(xiě)過(guò)不少內(nèi)核模式的代碼,只是最近一段時(shí)間不再寫(xiě)了。這第一篇文章講述了現(xiàn)代Intel主板的布局,CPU如何訪問(wèn)內(nèi)存,以及系統(tǒng)的內(nèi)存映射。 作為開(kāi)始,讓我們看看當(dāng)今的Intel計(jì)算機(jī)是如何連接各個(gè)組件的吧。下圖展示了主板上的主要組件:
現(xiàn)代主板的示意圖,北橋和南橋構(gòu)成了芯片組。 當(dāng)你看圖時(shí),請(qǐng)牢記一個(gè)至關(guān)重要的事實(shí):CPU一點(diǎn)也不知道它連接了什么東西。CPU僅僅通過(guò)一組針腳與外界交互,它并不關(guān)心外界到底有什么??赡苁且粋€(gè)電腦主板,但也可能是烤面包機(jī),網(wǎng)絡(luò)路由器,植入腦內(nèi)的設(shè)備,或CPU測(cè)試工作臺(tái)。CPU主要通過(guò)3種方式與外界交互:內(nèi)存地址空間,I/O地址空間,還有中斷。 眼下,我們只關(guān)心主板和內(nèi)存。安裝在主板上的CPU與外界溝通的門(mén)戶是前端總線(front-side bus),前端總線把CPU與北橋連接起來(lái)。每當(dāng)CPU需要讀寫(xiě)內(nèi)存時(shí),都會(huì)使用這條總線。CPU通過(guò)一部分管腳來(lái)傳輸想要讀寫(xiě)的物理內(nèi)存地址,同時(shí)另一些管腳用于發(fā)送將被寫(xiě)入或接收被讀出的數(shù)據(jù)。一個(gè)Intel Core 2 QX6600有33個(gè)針腳用于傳輸物理內(nèi)存地址(可以表示233個(gè)地址位置),64個(gè)針腳用于接收/發(fā)送數(shù)據(jù)(所以數(shù)據(jù)在64位通道中傳輸,也就是8字節(jié)的數(shù)據(jù)塊)。這使得CPU可以控制64GB的物理內(nèi)存(233個(gè)地址乘以8字節(jié)),盡管大多數(shù)的芯片組只能支持8GB的RAM。 現(xiàn)在到了最難理解的部分。我們可能曾經(jīng)認(rèn)為內(nèi)存指的就是RAM,被各式各樣的程序讀寫(xiě)著。的確,大部分CPU發(fā)出的內(nèi)存請(qǐng)求都被北橋轉(zhuǎn)送給了RAM管理器,但并非全部如此。物理內(nèi)存地址還可能被用于主板上各種設(shè)備間的通信,這種通信方式叫做內(nèi)存映射I/O。這類設(shè)備包括顯卡,大多數(shù)的PCI卡(比如掃描儀或SCSI卡),以及BIOS中的flash存儲(chǔ)器等。 當(dāng)北橋接收到一個(gè)物理內(nèi)存訪問(wèn)請(qǐng)求時(shí),它需要決定把這個(gè)請(qǐng)求轉(zhuǎn)發(fā)到哪里:是發(fā)給RAM?抑或是顯卡?具體發(fā)給誰(shuí)是由內(nèi)存地址映射表來(lái)決定的。映射表知道每一個(gè)物理內(nèi)存地址區(qū)域所對(duì)應(yīng)的設(shè)備。絕大部分的地址被映射到了RAM,其余地址由映射表來(lái)通知芯片組該由哪個(gè)設(shè)備來(lái)響應(yīng)此地址的訪問(wèn)請(qǐng)求。這些被映射為設(shè)備的內(nèi)存地址形成了一個(gè)經(jīng)典的空洞,位于PC內(nèi)存的640KB到1MB之間。當(dāng)內(nèi)存地址被保留用于顯卡和PCI設(shè)備時(shí),就會(huì)形成更大的空洞。這就是為什么32位的操作系統(tǒng)無(wú)法使用全部的4GB RAM。Linux中,/proc/iomem這個(gè)文件簡(jiǎn)明的列舉了這些空洞的地址范圍。下圖展示了Intel PC低端4GB物理內(nèi)存地址形成的一個(gè)典型的內(nèi)存映射:
Intel系統(tǒng)中,低端4GB內(nèi)存地址空間的布局。 實(shí)際的地址和范圍依賴于特定的主板和電腦中接入的設(shè)備,但是對(duì)于大多數(shù)Core 2系統(tǒng),情形都跟上圖非常接近。所有棕色的區(qū)域都被設(shè)備地址映射走了。記住,這些在主板總線上使用的都是物理地址。在CPU內(nèi)部(比如我們正在編寫(xiě)和運(yùn)行的程序),使用的是邏輯地址,必須先由CPU翻譯成物理地址以后,才能發(fā)布到總線上去訪問(wèn)內(nèi)存。 這個(gè)把邏輯地址翻譯成物理地址的規(guī)則比較復(fù)雜,而且還依賴于當(dāng)時(shí)CPU的運(yùn)行模式(實(shí)模式,32位保護(hù)模式,64位保護(hù)模式)。不管采用哪種翻譯機(jī)制,CPU的運(yùn)行模式?jīng)Q定了有多少物理內(nèi)存可以被訪問(wèn)。比如,當(dāng)CPU工作于32位保護(hù)模式時(shí),它只可以尋址4GB物理地址空間(當(dāng)然,也有個(gè)例外叫做物理地址擴(kuò)展,但暫且忽略這個(gè)技術(shù)吧)。由于頂部的大約1GB物理地址被映射到了主板上的設(shè)備,CPU實(shí)際能夠使用的也就只有大約3GB的RAM(有時(shí)甚至更少,我曾用過(guò)一臺(tái)安裝了Vista的電腦,它只有2.4GB可用)。如果CPU工作于實(shí)模式,那么它將只能尋址1MB的物理地址空間(這是早期的Intel處理器所支持的唯一模式)。如果CPU工作于64位保護(hù)模式,則可以尋址64GB的地址空間(雖然很少有芯片組支持這么大的RAM)。處于64位保護(hù)模式時(shí),CPU就有可能訪問(wèn)到RAM空間中被主板上的設(shè)備映射走了的區(qū)域了(即訪問(wèn)空洞下的RAM)。要達(dá)到這種效果,就需要使用比系統(tǒng)中所裝載的RAM地址區(qū)域更高的地址。這種技術(shù)叫做回收(reclaiming),而且還需要芯片組的配合。 這些關(guān)于內(nèi)存的知識(shí)將為下一篇文章做好鋪墊。下次我們會(huì)探討機(jī)器的啟動(dòng)過(guò)程:從上電開(kāi)始,直到boot loader準(zhǔn)備跳轉(zhuǎn)執(zhí)行操作系統(tǒng)內(nèi)核為止。如果你想更深入的學(xué)習(xí)這些東西,我強(qiáng)烈推薦Intel手冊(cè)。雖然我列出的都是第一手資料,但Intel手冊(cè)寫(xiě)得很好很準(zhǔn)確。這是一些資料: ? 《Datasheet for Intel G35 Chipset》描述了一個(gè)支持Core 2處理器的有代表性的芯片組。這也是本文的主要信息來(lái)源。 ? 《Datasheet for Intel Core 2 Quad-Core Q6000 Sequence》是一個(gè)處理器數(shù)據(jù)手冊(cè)。它記載了處理器上每一個(gè)管腳的作用(當(dāng)你把管腳按功能分組后,其實(shí)并不算多)。很棒的資料,雖然對(duì)有些位的描述比較含糊。 ? 《Intel Software Developer's Manuals》是杰出的文檔。它優(yōu)美的解釋了體系結(jié)構(gòu)的各個(gè)部分,一點(diǎn)也不會(huì)讓人感到含糊不清。第一卷和第三卷A部很值得一讀(別被'卷'字嚇倒,每卷都不長(zhǎng),而且您可以選擇性的閱讀)。 ? Pádraig Brady建議我鏈接到Ulrich Drepper的一篇關(guān)于內(nèi)存的優(yōu)秀文章。確實(shí)是個(gè)好東西。我本打算把這個(gè)鏈接放到討論存儲(chǔ)器的文章中的,但此處列出的越多越好啦。 參考: http://blog.csdn.net/drshenlei/article/details/4246441 轉(zhuǎn): 計(jì)算機(jī)的引導(dǎo)過(guò)程 原文標(biāo)題:How Computers Boot Up [注:本人水平有限,只好挑一些國(guó)外高手的精彩文章翻譯一下。一來(lái)自己復(fù)習(xí),二來(lái)與大家分享。] 前一篇文章介紹了Intel計(jì)算機(jī)的主板與內(nèi)存映射,從而為本文設(shè)定了一個(gè)系統(tǒng)引導(dǎo)階段的場(chǎng)景。引導(dǎo)(Booting)是一個(gè)復(fù)雜的,充滿技巧的,涉及多個(gè)階段,又十分有趣的過(guò)程。下圖列出了此過(guò)程的概要:
引導(dǎo)過(guò)程概要 當(dāng) 你按下計(jì)算機(jī)的電源鍵后(現(xiàn)在別按?。?,機(jī)器就開(kāi)始運(yùn)轉(zhuǎn)了。一旦主板上電,它就會(huì)初始化自身的固件(firmware)——芯片組和其他零零碎碎的東西 ——并嘗試啟動(dòng)CPU。如果此時(shí)出了什么問(wèn)題(比如CPU壞了或根本沒(méi)裝),那么很可能出現(xiàn)的情況是電腦沒(méi)有任何動(dòng)靜,除了風(fēng)扇在轉(zhuǎn)。一些主板會(huì)在CPU 故障或缺失時(shí)發(fā)出鳴音提示,但以我的經(jīng)驗(yàn),此時(shí)大多數(shù)機(jī)器都會(huì)處于僵死狀態(tài)。一些USB或其他設(shè)備也可能導(dǎo)致機(jī)器啟動(dòng)時(shí)僵死。對(duì)于那些以前工作正常,突然 出現(xiàn)這種癥狀的電腦,一個(gè)可能的解決辦法是拔除所有不必要的設(shè)備。你也可以一次只斷開(kāi)一個(gè)設(shè)備,從而發(fā)現(xiàn)哪個(gè)是罪魁禍?zhǔn)住? 如果一切正常,CPU就開(kāi)始運(yùn)行了。在一個(gè)多處理器或多核處理器的系統(tǒng)中,會(huì)有一個(gè)CPU被動(dòng)態(tài)的指派為引導(dǎo)處理器(bootstrap processor簡(jiǎn)寫(xiě)BSP),用于執(zhí)行全部的BIOS和內(nèi)核初始化代碼。其余的處理器,此時(shí)被稱為應(yīng)用處理器(application processor簡(jiǎn)寫(xiě)AP),一直保持停機(jī)狀態(tài)直到內(nèi)核明確激活他們?yōu)橹?。雖然Intel CPU經(jīng)歷了很多年的發(fā)展,但他們一直保持著完全的向后兼容性,所以現(xiàn)代的CPU可以表現(xiàn)得跟原先1978年的Intel 8086完全一樣。其實(shí),當(dāng)CPU上電后,它就是這么做的。在這個(gè)基本的上電過(guò)程中,處理器工作于實(shí)模式,分頁(yè)功能是無(wú)效的。此時(shí)的系統(tǒng)環(huán)境,就像古老的MS-DOS一樣,只有1MB內(nèi)存可以尋址,任何代碼都可以讀寫(xiě)任何地址的內(nèi)存,這里沒(méi)有保護(hù)或特權(quán)級(jí)的概念。 CPU上電后,大部分寄存器的都具有定義良好的初始值,包括指令指針寄存器(EIP),它記錄了下一條即將被CPU執(zhí)行的指令所在的內(nèi)存地址。盡管此時(shí)的Intel CPU還只能尋址1MB的內(nèi)存,但憑借一個(gè)奇特的技巧,一個(gè)隱藏的基地址(其實(shí)就是個(gè)偏移量)會(huì)與EIP相加,其結(jié)果指向第一條將被執(zhí)行的指令所處的地址0xFFFFFFF0(長(zhǎng)16字節(jié),在4GB內(nèi)存空間的尾部,遠(yuǎn)高于1MB)。這個(gè)特殊的地址叫做復(fù)位向量(reset vector),而且是現(xiàn)代Intel CPU的標(biāo)準(zhǔn)。 主板保證在復(fù)位向量處的指令是一個(gè)跳轉(zhuǎn),而且是跳轉(zhuǎn)到BIOS執(zhí)行入口點(diǎn)所在的內(nèi)存映射地址。這個(gè)跳轉(zhuǎn)會(huì)順帶清除那個(gè)隱藏的、上電時(shí)的基地址。感謝芯片組提供的內(nèi)存映射功能,此時(shí)的內(nèi)存地址存放著CPU初始化所需的真正內(nèi)容。這些內(nèi)容全部是從包含有BIOS的閃存映射過(guò)來(lái)的,而此時(shí)的RAM模塊還只有隨機(jī)的垃圾數(shù)據(jù)。下面的圖例列出了相關(guān)的內(nèi)存區(qū)域:
引導(dǎo)時(shí)的重要內(nèi)存區(qū)域 隨后,CPU開(kāi)始執(zhí)行BIOS的代碼,初始化機(jī)器中的一些硬件。之后BIOS開(kāi)始執(zhí)行上電自檢過(guò)程(POST),檢測(cè)計(jì)算機(jī)中的各種組件。如果找不到一個(gè)可用的顯卡,POST就會(huì)失敗,導(dǎo)致BIOS進(jìn)入停機(jī)狀態(tài)并發(fā)出鳴音提示(因?yàn)榇藭r(shí)無(wú)法在屏幕上輸出提示信息)。如果顯卡正常,那么電腦看起來(lái)就真的運(yùn)轉(zhuǎn)起來(lái)了:顯示一個(gè)制造商定制的商標(biāo),開(kāi)始內(nèi)存自檢,天使們大聲的吹響號(hào)角。另有一些POST失敗的情況,比如缺少鍵盤(pán),會(huì)導(dǎo)致停機(jī),屏幕上顯示出錯(cuò)信息。其實(shí)POST即是檢測(cè)又是初始化,還要枚舉出所有PCI設(shè)備的資源——中斷,內(nèi)存范圍,I/O端口。現(xiàn)代的BIOS會(huì)遵循高級(jí)配置與電源接口(ACPI)協(xié)議,創(chuàng)建一些用于描述設(shè)備的數(shù)據(jù)表,這些表格將來(lái)會(huì)被操作系統(tǒng)內(nèi)核用到。 POST完畢后,BIOS就準(zhǔn)備引導(dǎo)操作系統(tǒng)了,它必須存在于某個(gè)地方:硬盤(pán),光驅(qū),軟盤(pán)等。BIOS搜索引導(dǎo)設(shè)備的實(shí)際順序是用戶可定制的。如果找不到合適的引導(dǎo)設(shè)備,BIOS會(huì)顯示出錯(cuò)信息并停機(jī),比如'Non-System Disk or Disk Error'沒(méi)有系統(tǒng)盤(pán)或驅(qū)動(dòng)器故障。一個(gè)壞了的硬盤(pán)可能導(dǎo)致此癥狀。幸運(yùn)的是,在這篇文章中,BIOS成功的找到了一個(gè)可以正常引導(dǎo)的驅(qū)動(dòng)器。 現(xiàn)在,BIOS會(huì)讀取硬盤(pán)的第一個(gè)扇區(qū)(0扇區(qū)),內(nèi)含512個(gè)字節(jié)。這些數(shù)據(jù)叫做主引導(dǎo)記錄(Master Boot Record簡(jiǎn)稱MBR)。一般說(shuō)來(lái),它包含兩個(gè)極其重要的部分:一個(gè)是位于MBR開(kāi)頭的操作系統(tǒng)相關(guān)的引導(dǎo)程序,另一個(gè)是緊跟其后的磁盤(pán)分區(qū)表。BIOS 絲毫不關(guān)心這些事情:它只是簡(jiǎn)單的加載MBR的內(nèi)容到內(nèi)存地址0x7C00處,并跳轉(zhuǎn)到此處開(kāi)始執(zhí)行,不管MBR里的代碼是什么。
主引導(dǎo)記錄 這段在MBR內(nèi)的特殊代碼可能是Windows 引導(dǎo)裝載程序,Linux 引導(dǎo)裝載程序(比如LILO或GRUB),甚至可能是病毒。與此不同,分區(qū)表則是標(biāo)準(zhǔn)化的:它是一個(gè)64字節(jié)的區(qū)塊,包含4個(gè)16字節(jié)的記錄項(xiàng),描述磁盤(pán)是如何被分割的(所以你可以在一個(gè)磁盤(pán)上安裝多個(gè)操作系統(tǒng)或擁有多個(gè)獨(dú)立的卷)。傳統(tǒng)上,Microsoft的MBR代碼會(huì)查看分區(qū)表,找到一個(gè)(唯一的)標(biāo)記為活動(dòng)(active)的分區(qū),加載那個(gè)分區(qū)的引導(dǎo)扇區(qū)(boot sector),并執(zhí)行其中的代碼。引導(dǎo)扇區(qū)是一個(gè)分區(qū)的第一個(gè)扇區(qū),而不是整個(gè)磁盤(pán)的第一個(gè)扇區(qū)。如果此時(shí)出了什么問(wèn)題,你可能會(huì)收到如下錯(cuò)誤信息:'Invalid Partition Table'無(wú)效分區(qū)表或'Missing Operating System'操作系統(tǒng)缺失。這條信息不是來(lái)自BIOS的,而是由從磁盤(pán)加載的MBR程序所給出的。因此這些信息依賴于MBR的內(nèi)容。 隨著時(shí)間的推移,引導(dǎo)裝載過(guò)程已經(jīng)發(fā)展得越來(lái)越復(fù)雜,越來(lái)越靈活。Linux的引導(dǎo)裝載程序Lilo和GRUB可以處理很多種類的操作系統(tǒng),文件系統(tǒng),以及引導(dǎo)配置信息。他們的MBR代碼不再需要效仿上述'從活動(dòng)分區(qū)來(lái)引導(dǎo)'的方法。但是從功能上講,這個(gè)過(guò)程大致如下: 1、 MBR本身包含有第一階段的引導(dǎo)裝載程序。GRUB稱之為階段一。 2、 由于MBR很小,其中的代碼僅僅用于從磁盤(pán)加載另一個(gè)含有額外的引導(dǎo)代碼的扇區(qū)。此扇區(qū)可能是某個(gè)分區(qū)的引導(dǎo)扇區(qū),但也可能是一個(gè)被硬編碼到MBR中的扇區(qū)位置。 3、 MBR配合第2步所加載的代碼去讀取一個(gè)文件,其中包含了下一階段所需的引導(dǎo)程序。這在GRUB中是'階段二'引導(dǎo)程序,在Windows Server中是C:/NTLDR。如果第2步失敗了,在Windows中你會(huì)收到錯(cuò)誤信息,比如'NTLDR is missing'NTLDR缺失。階段二的代碼進(jìn)一步讀取一個(gè)引導(dǎo)配置文件(比如在GRUB中是grub.conf,在Windows中是boot.ini)。之后要么給用戶顯示一些引導(dǎo)選項(xiàng),要么直接去引導(dǎo)系統(tǒng)。 4、 此時(shí),引導(dǎo)裝載程序需要啟動(dòng)操作系統(tǒng)核心。它必須擁有足夠的關(guān)于文件系統(tǒng)的信息,以便從引導(dǎo)分區(qū)中讀取內(nèi)核。在Linux中,這意味著讀取一個(gè)名字類似'vmlinuz-2.6.22-14-server'的含有內(nèi)核鏡像的文件,將之加載到內(nèi)存并跳轉(zhuǎn)去執(zhí)行內(nèi)核引導(dǎo)代碼。在Windows Server 2003中,一部份內(nèi)核啟動(dòng)代碼是與內(nèi)核鏡像本身分離的,事實(shí)上是嵌入到了NTLDR當(dāng)中。在完成一些初始化工作以后,NTDLR從'c:/Windows/System32/ntoskrnl.exe'文件加載內(nèi)核鏡像,就像GRUB所做的那樣,跳轉(zhuǎn)到內(nèi)核的入口點(diǎn)去執(zhí)行。 這里還有一個(gè)復(fù)雜的地方值得一提(這也是我說(shuō)引導(dǎo)富于技巧性的原因)。當(dāng)前Linux內(nèi)核的鏡像就算被壓縮了,在實(shí)模式下,也沒(méi)法塞進(jìn)640KB的可用RAM里。我的vanilla Ubuntu內(nèi)核壓縮后有1.7MB。然而,引導(dǎo)裝載程序必須運(yùn)行于實(shí)模式,以便調(diào)用BIOS代碼去讀取磁盤(pán),所以此時(shí)內(nèi)核肯定是沒(méi)法用的。解決之道是使用一種倍受推崇的'虛模式'。它并非一個(gè)真正的處理器運(yùn)行模式(希望Intel的工程師允許我以此作樂(lè)),而是一個(gè)特殊技巧。程序不斷的在實(shí)模式和保護(hù)模式之間切換,以便訪問(wèn)高于1MB的內(nèi)存同時(shí)還能使用BIOS。如果你閱讀了GRUB的源代碼,你就會(huì)發(fā)現(xiàn)這些切換到處都是(看看stage2/目錄下的程序,對(duì)real_to_prot 和 prot_to_real函數(shù)的調(diào)用)。在這個(gè)棘手的過(guò)程結(jié)束時(shí),裝載程序終于千方百計(jì)的把整個(gè)內(nèi)核都塞到內(nèi)存里了,但在這后,處理器仍保持在實(shí)模式運(yùn)行。 至此,我們來(lái)到了從'引導(dǎo)裝載'跳轉(zhuǎn)到'早期的內(nèi)核初始化'的時(shí)刻,就像第一張圖中所指示的那樣。在系統(tǒng)做完熱身運(yùn)動(dòng)后,內(nèi)核會(huì)展開(kāi)并讓系統(tǒng)開(kāi)始運(yùn)轉(zhuǎn)。下一篇文章將帶大家一步步深入Linux內(nèi)核的初始化過(guò)程,讀者還可以參考Linux Cross reference的資源。我沒(méi)辦法對(duì)Windows也這么做,但我會(huì)把要點(diǎn)指出來(lái)。 參考: http://blog.csdn.net/drshenlei/article/details/4250306轉(zhuǎn): 內(nèi)核引導(dǎo)過(guò)程 原文標(biāo)題:The Kernel Boot Process [注:本人水平有限,只好挑一些國(guó)外高手的精彩文章翻譯一下。一來(lái)自己復(fù)習(xí),二來(lái)與大家分享。] 上一篇文章解釋了計(jì)算機(jī)的引導(dǎo)過(guò)程,正好講到引導(dǎo)裝載程序把系統(tǒng)內(nèi)核鏡像塞進(jìn)內(nèi)存,準(zhǔn)備跳轉(zhuǎn)到內(nèi)核入口點(diǎn)去執(zhí)行的時(shí)刻。作為引導(dǎo)啟動(dòng)系列文章的最后一篇,就讓我們深入內(nèi)核,去看看操作系統(tǒng)是怎么啟動(dòng)的吧。由于我習(xí)慣以事實(shí)為依據(jù)討論問(wèn)題,所以文中會(huì)出現(xiàn)大量的鏈接引用Linux 內(nèi)核2.6.25.6版的源代碼(源自Linux Cross Reference)。如果你熟悉C的 語(yǔ)法,這些代碼就會(huì)非常容易讀懂;即使你忽略一些細(xì)節(jié),仍能大致明白程序都干了些什么。最主要的障礙在于對(duì)一些代碼的理解需要相關(guān)的背景知識(shí),比如機(jī)器的 底層特性或什么時(shí)候、為什么它會(huì)運(yùn)行。我希望能盡量給讀者提供一些背景知識(shí)。為了保持簡(jiǎn)潔,許多有趣的東西,比如中斷和內(nèi)存,文中只能點(diǎn)到為止了。在本文 的最后列出了Windows的引導(dǎo)過(guò)程的要點(diǎn)。 當(dāng)Intel x86的引導(dǎo)程序運(yùn)行到此刻時(shí),處理器處于實(shí)模式(可以尋址1MB的內(nèi)存),(針對(duì)現(xiàn)代的Linux系統(tǒng))RAM的內(nèi)容大致如下: 引導(dǎo)裝載完成后的RAM內(nèi)容 引導(dǎo)裝載程序通過(guò)BIOS的磁盤(pán)I/O服務(wù),已經(jīng)把內(nèi)核鏡像加載到內(nèi)存當(dāng)中。這個(gè)鏡像只是硬盤(pán)中內(nèi)核文件(比如/boot/vmlinuz-2.6.22-14-server)的一份完全相同的拷貝。鏡像分為兩個(gè)部分:一個(gè)較小的部分,包含實(shí)模式的內(nèi)核代碼,被加載到640KB內(nèi)存邊界以下;另一部分是一大塊內(nèi)核,運(yùn)行在保護(hù)模式,被加載到低端1MB內(nèi)存地址以上。 如上圖所示,之后的事情發(fā)生在實(shí)模式內(nèi)核的頭部(kernel header)。這段內(nèi)存區(qū)域用于實(shí)現(xiàn)引導(dǎo)裝載程序與內(nèi)核之間的Linux引導(dǎo)協(xié)議。 此處的一些數(shù)據(jù)會(huì)被引導(dǎo)裝載程序讀取。這些數(shù)據(jù)包括一些令人愉快的信息,比如包含內(nèi)核版本號(hào)的可讀字符串,也包括一些關(guān)鍵信息,比如實(shí)模式內(nèi)核代碼的大 小。引導(dǎo)裝載程序還會(huì)向這個(gè)區(qū)域?qū)懭霐?shù)據(jù),比如用戶選中的引導(dǎo)菜單項(xiàng)對(duì)應(yīng)的命令行參數(shù)所在的內(nèi)存地址。之后就到了跳轉(zhuǎn)到內(nèi)核入口點(diǎn)的時(shí)刻。下圖顯示了內(nèi)核 初始化代碼的執(zhí)行順序,包括源代碼的目錄、文件和行號(hào): 與體系結(jié)構(gòu)相關(guān)的Linux內(nèi)核初始化過(guò)程 對(duì)于Intel體系結(jié)構(gòu),內(nèi)核啟動(dòng)前期會(huì)執(zhí)行arch/x86/boot/header.S文件中的程序。它是用匯編語(yǔ)言書(shū)寫(xiě)的。一般說(shuō)來(lái)匯編代碼在內(nèi)核中很少出現(xiàn),但常見(jiàn)于引導(dǎo)代碼。這個(gè)文件的開(kāi)頭實(shí)際上包含了引導(dǎo)扇區(qū)代碼。早期的Linux不需要引導(dǎo)裝載程序就可以工作,這段代碼是從那個(gè)時(shí)候留傳下來(lái)的?,F(xiàn)今,如果這個(gè)引導(dǎo)扇區(qū)被執(zhí)行,它僅僅給用戶輸出一個(gè)'bugger_off_msg'之后就會(huì)重啟系統(tǒng)。現(xiàn)代的引導(dǎo)裝載程序會(huì)忽略這段遺留代碼。在引導(dǎo)扇區(qū)代碼之后,我們會(huì)看到實(shí)模式內(nèi)核頭部(kernel header)最開(kāi)始的15字節(jié);這兩部分合起來(lái)是512字節(jié),正好是Intel硬件平臺(tái)上一個(gè)典型的磁盤(pán)扇區(qū)的大小。 在這512字節(jié)之后,偏移量0x200處,我們會(huì)發(fā)現(xiàn)Linux內(nèi)核的第一條指令,也就是實(shí)模式內(nèi)核的入口點(diǎn)。具體的說(shuō),它在header.S:110,是一個(gè)2字節(jié)的跳轉(zhuǎn)指令,直接寫(xiě)成了機(jī)器碼的形式0x3AEB。你可以通過(guò)對(duì)內(nèi)核鏡像運(yùn)行hexdump,并查看偏移量0x200處的內(nèi)容來(lái)驗(yàn)證這一點(diǎn)——這僅僅是一個(gè)對(duì)神志清醒程度的檢查,以確保這一切并不是在做夢(mèng)。引導(dǎo)裝載程序運(yùn)行完畢時(shí)就會(huì)跳轉(zhuǎn)執(zhí)行這個(gè)位置的指令,進(jìn)而跳轉(zhuǎn)到header.S:229執(zhí)行一個(gè)普通的用匯編寫(xiě)成的子程序,叫做start_of_setup。這個(gè)短小的子程序初始化棧空間(stack),把實(shí)模式內(nèi)核的bss段清零(這個(gè)區(qū)域包含靜態(tài)變量,所以用0來(lái)初始化它們),之后跳轉(zhuǎn)執(zhí)行一段又老又好的C語(yǔ)言程序:arch/x86/boot/main.c:122。 main()會(huì)處理一些登記工作(比如檢測(cè)內(nèi)存布局),設(shè)置顯示模式等。然后它會(huì)調(diào)用go_to_protected_mode()。然而,在把CPU置于保護(hù)模式之前,還有一些工作必須完成。有兩個(gè)主要問(wèn)題:中斷和內(nèi)存。在實(shí)模式中,處理器的中斷向量表總是從內(nèi)存的0地址開(kāi)始的,然而在保護(hù)模式中,這個(gè)中斷向量表的位置是保存在一個(gè)叫IDTR的CPU寄存器當(dāng)中的。與此同時(shí),從邏輯內(nèi)存地址(在程序中使用)到線性內(nèi)存地址(一個(gè)從0連續(xù)編號(hào)到內(nèi)存頂端的數(shù)值)的翻譯方法在實(shí)模式和保護(hù)模式中是不同的。保護(hù)模式需要一個(gè)叫做GDTR的寄存器來(lái)存放內(nèi)存全局描述符表的地址。所以go_to_protected_mode()調(diào)用了setup_idt() 和 setup_gdt(),用于裝載臨時(shí)的中斷描述符表和全局描述符表。 現(xiàn)在我們可以轉(zhuǎn)入保護(hù)模式啦,這是由另一段匯編子程序protected_mode_jump來(lái)完成的。這個(gè)子程序通過(guò)設(shè)定CPU的CR0寄存器的PE位來(lái)使能保護(hù)模式。此時(shí),分頁(yè)功能還處于關(guān)閉狀態(tài);分頁(yè)是處理器的一個(gè)可選的功能,即使運(yùn)行于保護(hù)模式也并非必要。真正重要的是,我們不再受制于640K的內(nèi)存邊界,現(xiàn)在可以尋址高達(dá)4GB的RAM了。這個(gè)子程序進(jìn)而調(diào)用壓縮狀態(tài)內(nèi)核的32位內(nèi)核入口點(diǎn)startup_32。startup32會(huì)做一些簡(jiǎn)單的寄存器初始化工作,并調(diào)用一個(gè)C語(yǔ)言編寫(xiě)的函數(shù)decompress_kernel(),用于實(shí)際的解壓縮工作。 decompress_kernel()會(huì)打印一條大家熟悉的信息'Decompressing Linux…'(正在解壓縮Linux)。解壓縮過(guò)程是原地進(jìn)行的,一旦完成內(nèi)核鏡像的解壓縮,第一張圖中所示的壓縮內(nèi)核鏡像就會(huì)被覆蓋掉。因此解壓后的內(nèi)核也是從1MB位置開(kāi)始的。之后,decompress_kernel()會(huì)顯示'done'(完成)和令人振奮的'Booting the kernel'(正在引導(dǎo)內(nèi)核)。這里'Booting'的意思是跳轉(zhuǎn)到整個(gè)故事的最后一個(gè)入口點(diǎn),也是保護(hù)模式內(nèi)核的入口點(diǎn),位于RAM的第二個(gè)1MB開(kāi)始處(偏移量0x100000,此值是由芬蘭Halti山巔之上的神靈授意給Linus的)。在這個(gè)神圣的位置含有一個(gè)子程序調(diào)用,名叫…呃…startup_32。但你會(huì)發(fā)現(xiàn)這一位是在另一個(gè)目錄中的。 這位startup_32的第二個(gè)化身也是一個(gè)匯編子程序,但它包含了32位模式的初始化過(guò)程: 1、 它清理了保護(hù)模式內(nèi)核的bss段。(這回是真正的內(nèi)核了,它會(huì)一直運(yùn)行,直到機(jī)器重啟或關(guān)機(jī)。) 2、 為內(nèi)存建立最終的全局描述符表。 3、 建立頁(yè)表以便可以開(kāi)啟分頁(yè)功能。 4、 使能分頁(yè)功能。 5、 初始化??臻g。 6、 創(chuàng)建最終的中斷描述符表。 7、 最后,跳轉(zhuǎn)執(zhí)行一個(gè)體系結(jié)構(gòu)無(wú)關(guān)的內(nèi)核啟動(dòng)函數(shù):start_kernel()。 下圖顯示了引導(dǎo)最后一步的代碼執(zhí)行流程: 與體系結(jié)構(gòu)無(wú)關(guān)的Linux內(nèi)核初始化過(guò)程 start_kernel()看起來(lái)更像典型的內(nèi)核代碼,幾乎全用C語(yǔ)言編寫(xiě)而且與特定機(jī)器無(wú)關(guān)。這個(gè)函數(shù)調(diào)用了一長(zhǎng)串的函數(shù),用來(lái)初始化各個(gè)內(nèi)核子系統(tǒng)和數(shù)據(jù)結(jié)構(gòu),包括調(diào)度器(scheduler),內(nèi)存分區(qū)(memory zones),計(jì)時(shí)器(time keeping)等等。之后,start_kernel()調(diào)用rest_init(),此時(shí)幾乎所有的東西都可以工作了。rest_init()會(huì)創(chuàng)建一個(gè)內(nèi)核線程,并以另一個(gè)函數(shù)kernel_init()作為此線程的入口點(diǎn)。之后,rest_init()會(huì)調(diào)用schedule()來(lái)激活任務(wù)調(diào)度功能,然后調(diào)用cpu_idle()使自己進(jìn)入睡眠(sleep)狀態(tài),成為Linux內(nèi)核中的一個(gè)空閑線程(idle thread)。cpu_idle()會(huì)在0號(hào)進(jìn)程(process zero)中永遠(yuǎn)的運(yùn)行下去。一旦有什么事情可做,比如有了一個(gè)活動(dòng)就緒的進(jìn)程(runnable process),0號(hào)進(jìn)程就會(huì)激活CPU去執(zhí)行這個(gè)任務(wù),直到?jīng)]有活動(dòng)就緒的進(jìn)程后才返回。 但是,還有一個(gè)小麻煩需要處理。我們跟隨引導(dǎo)過(guò)程一路走下來(lái),這個(gè)漫長(zhǎng)的線程以一個(gè)空閑循環(huán)(idle loop)作為結(jié)尾。處理器上電執(zhí)行第一條跳轉(zhuǎn)指令以后,一路運(yùn)行,最終會(huì)到達(dá)此處。從復(fù)位向量(reset vector)->BIOS->MBR->引導(dǎo)裝載程序->實(shí)模式內(nèi)核->保護(hù)模式內(nèi)核,跳轉(zhuǎn)跳轉(zhuǎn)再跳轉(zhuǎn),經(jīng)過(guò)所有這些雜七雜八的步驟,最后來(lái)到引導(dǎo)處理器(boot processor)中的空閑循環(huán)cpu_idle()??雌饋?lái)真的很酷。然而,這并非故事的全部,否則計(jì)算機(jī)就不會(huì)工作。 在這個(gè)時(shí)候,前面啟動(dòng)的那個(gè)內(nèi)核線程已經(jīng)準(zhǔn)備就緒,可以取代0號(hào)進(jìn)程和它的空閑線程了。事實(shí)也是如此,就發(fā)生在kernel_init()開(kāi)始運(yùn)行的時(shí)刻(此函數(shù)之前被作為線程的入口點(diǎn))。kernel_init()的職責(zé)是初始化系統(tǒng)中其余的CPU,這些CPU從引導(dǎo)過(guò)程開(kāi)始到現(xiàn)在,還一直處于停機(jī)狀態(tài)。之前我們看過(guò)的所有代碼都是在一個(gè)單獨(dú)的CPU上運(yùn)行的,它叫做引導(dǎo)處理器(boot processor)。當(dāng)其他CPU——稱作應(yīng)用處理器(application processor)——啟動(dòng)以后,它們是處于實(shí)模式的,必須通過(guò)一些初始化步驟才能進(jìn)入保護(hù)模式。大部分的代碼過(guò)程都是相同的,你可以參考startup_32,但對(duì)于應(yīng)用處理器,還是有些細(xì)微的不同。最終,kernel_init()會(huì)調(diào)用init_post(),后者會(huì)嘗試啟動(dòng)一個(gè)用戶模式(user-mode)的進(jìn)程,嘗試的順序?yàn)椋?/span>/sbin/init,/etc/init,/bin/init,/bin/sh。如果都不行,內(nèi)核就會(huì)報(bào)錯(cuò)。幸運(yùn)的是init經(jīng)常就在這些地方的,于是1號(hào)進(jìn)程(PID 1)就開(kāi)始運(yùn)行了。它會(huì)根據(jù)對(duì)應(yīng)的配置文件來(lái)決定啟動(dòng)哪些進(jìn)程,這可能包括X11 Windows,控制臺(tái)登陸程序,網(wǎng)絡(luò)后臺(tái)程序等。從而結(jié)束了引導(dǎo)進(jìn)程,同時(shí)另一個(gè)Linux程序開(kāi)始在某處運(yùn)行。至此,讓我祝福您的電腦可以一直正常運(yùn)行下去,不出毛病。 在同樣的體系結(jié)構(gòu)下,Windows的啟動(dòng)過(guò)程與Linux有很多相似之處。它也面臨同樣的問(wèn)題,也必須完成類似的初始化過(guò)程。當(dāng)引導(dǎo)過(guò)程開(kāi)始后,一個(gè)最大的不同是,Windows把全部的實(shí)模式內(nèi)核代碼以及一部分初始的保護(hù)模式代碼都打包到了引導(dǎo)加載程序(C:/NTLDR)當(dāng)中。因此,Windows使用的二進(jìn)制鏡像文件就不一樣了,內(nèi)核鏡像中沒(méi)有包含兩個(gè)部分的代碼。另外,Linux把引導(dǎo)裝載程序與內(nèi)核完全分離,在某種程度上自動(dòng)的形成不同的開(kāi)源項(xiàng)目。下圖顯示了Windows內(nèi)核主要的啟動(dòng)過(guò)程: Windows內(nèi)核初始化過(guò)程 自然而然的,Windows用戶模式的啟動(dòng)就非常不同了。沒(méi)有/sbin/init程序,而是運(yùn)行Csrss.exe和Winlogon.exe。Winlogon會(huì)啟動(dòng)Services.exe(它會(huì)啟動(dòng)所有的Windows服務(wù)程序)、Lsass.exe和本地安全認(rèn)證子系統(tǒng)。經(jīng)典的Windows登陸對(duì)話框就是運(yùn)行在Winlogon的上下文中的。 本文是引導(dǎo)啟動(dòng)系列話題的最后一篇。感謝每一位讀者,感謝你們的反饋。我很抱歉,有些內(nèi)容只能點(diǎn)到為止;我打算把它們留在其他文章中深入討論,并盡量保持文章的長(zhǎng)度適合blog的風(fēng)格。下次我打算定期的撰寫(xiě)關(guān)于'Software Illustrated'的文章,就像本系列一樣。最后,給大家一些參考資料: ? 最好也最重要的資料是實(shí)際的內(nèi)核代碼,Linux或BSD的都成。 ? Intel出版的杰出的軟件開(kāi)發(fā)人員手冊(cè),你可以免費(fèi)下載到。 ? 《理解Linux內(nèi)核》是本好書(shū),其中討論了大量的Linux內(nèi)核代碼。這書(shū)也許有點(diǎn)過(guò)時(shí)有點(diǎn)枯燥,但我還是將它推薦給那些想要與內(nèi)核心意相通的人們?!?a >Linux設(shè)備驅(qū)動(dòng)程序》讀起來(lái)會(huì)有趣得多,講的也不錯(cuò),但是涉及的內(nèi)容有些局限性。最后,網(wǎng)友Patrick Moroney推薦Robert Love所寫(xiě)的《Linux內(nèi)核開(kāi)發(fā)》,我曾聽(tīng)過(guò)一些對(duì)此書(shū)的正面評(píng)價(jià),所以還是值得列出來(lái)的。 ? 對(duì)于Windows,目前最好的參考書(shū)是《Windows Internals》,作者是David Solomon和Mark Russinovich,后者是Sysinternals的知名專家。這是本特棒的書(shū),寫(xiě)的很好而且講解全面。主要的缺點(diǎn)是缺少源代碼的支持。 參考: http://blog.csdn.net/drshenlei/article/details/4253179轉(zhuǎn): 內(nèi)存地址轉(zhuǎn)換與分段 原文標(biāo)題:Memory Translation and Segmentation [注:本人水平有限,只好挑一些國(guó)外高手的精彩文章翻譯一下。一來(lái)自己復(fù)習(xí),二來(lái)與大家分享。] 本文是Intel兼容計(jì)算機(jī)(x86)的內(nèi)存與保護(hù)系列文章的第一篇,延續(xù)了啟動(dòng)引導(dǎo)系列文章的主題,進(jìn)一步分析操作系統(tǒng)內(nèi)核的工作流程。與以前一樣,我將引用Linux內(nèi)核的源代碼,但對(duì)Windows只給出示例(抱歉,我忽略了BSD,Mac等系統(tǒng),但大部分的討論對(duì)它們一樣適用)。文中如果有錯(cuò)誤,請(qǐng)不吝賜教。 在支持Intel的主板芯片組上,CPU對(duì)內(nèi)存的訪問(wèn)是通過(guò)連接著CPU和北橋芯片的前端總線來(lái)完成的。在前端總線上傳輸?shù)膬?nèi)存地址都是物理內(nèi)存地址,編號(hào)從0開(kāi)始一直到可用物理內(nèi)存的最高端。這些數(shù)字被北橋映射到實(shí)際的內(nèi)存條上。物理地址是明確的、最終用在總線上的編號(hào),不必轉(zhuǎn)換,不必分頁(yè),也沒(méi)有特權(quán)級(jí)檢查。然而,在CPU內(nèi)部,程序所使用的是邏輯內(nèi)存地址,它必須被轉(zhuǎn)換成物理地址后,才能用于實(shí)際內(nèi)存訪問(wèn)。從概念上講,地址轉(zhuǎn)換的過(guò)程如下圖所示: x86 CPU開(kāi)啟分頁(yè)功能后的內(nèi)存地址轉(zhuǎn)換過(guò)程 此圖并未指出詳實(shí)的轉(zhuǎn)換方式,它僅僅描述了在CPU的分頁(yè)功能開(kāi)啟的情況下內(nèi)存地址的轉(zhuǎn)換過(guò)程。如果CPU關(guān)閉了分頁(yè)功能,或運(yùn)行于16位實(shí)模式,那么從分段單元(segmentation unit)輸出的就是最終的物理地址了。當(dāng)CPU要執(zhí)行一條引用了內(nèi)存地址的指令時(shí),轉(zhuǎn)換過(guò)程就開(kāi)始了。第一步是把邏輯地址轉(zhuǎn)換成線性地址。但是,為什么不跳過(guò)這一步,而讓軟件直接使用線性地址(或物理地址呢?)其理由與:'人類為何要長(zhǎng)有闌尾?它的主要作用僅僅是被感染發(fā)炎而已'大致相同。這是進(jìn)化過(guò)程中產(chǎn)生的奇特構(gòu)造。要真正理解x86分段功能的設(shè)計(jì),我們就必須回溯到1978年。 最初的8086處理器的寄存器是16位的,其指令集大多使用8位或16位的操作數(shù)。這使得代碼可以控制216個(gè)字節(jié)(或64KB)的內(nèi)存。然而Intel的工程師們想要讓CPU可以使用更多的內(nèi)存,而又不用擴(kuò)展寄存器和指令的位寬。于是他們引入了段寄存器(segment register),用來(lái)告訴CPU一條程序指令將操作哪一個(gè)64K的內(nèi)存區(qū)塊。一個(gè)合理的解決方案是:你先加載段寄存器,相當(dāng)于說(shuō)'這兒!我打算操作開(kāi)始于X處的內(nèi)存區(qū)塊';之后,再用16位的內(nèi)存地址來(lái)表示相對(duì)于那個(gè)內(nèi)存區(qū)塊(或段)的偏移量??偣灿?/span>4個(gè)段寄存器:一個(gè)用于棧(ss),一個(gè)用于程序代碼(cs),兩個(gè)用于數(shù)據(jù)(ds,es)。在那個(gè)年代,大部分程序的棧、代碼、數(shù)據(jù)都可以塞進(jìn)對(duì)應(yīng)的段中,每段64KB長(zhǎng),所以分段功能經(jīng)常是透明的。 現(xiàn)今,分段功能依然存在,一直被x86處理器所使用著。每一條會(huì)訪問(wèn)內(nèi)存的指令都隱式的使用了段寄存器。比如,一條跳轉(zhuǎn)指令會(huì)用到代碼段寄存器(cs),一條壓棧指令(stack push instruction)會(huì)使用到堆棧段寄存器(ss)。在大部分情況下你可以使用指令明確的改寫(xiě)段寄存器的值。段寄存器存儲(chǔ)了一個(gè)16位的段選擇符(segment selector);它們可以經(jīng)由機(jī)器指令(比如MOV)被直接加載。唯一的例外是代碼段寄存器(cs),它只能被影響程序執(zhí)行順序的指令所改變,比如CALL或JMP指令。雖然分段功能一直是開(kāi)啟的,但其在實(shí)模式與保護(hù)模式下的運(yùn)作方式并不相同的。 在實(shí)模式下,比如在引導(dǎo)啟動(dòng)的初期,段選擇符是一個(gè)16位的數(shù)值,指示出一個(gè)段的開(kāi)始處的物理內(nèi)存地址。這個(gè)數(shù)值必須被以某種方式放大,否則它也會(huì)受限于64K當(dāng)中,分段就沒(méi)有意義了。比如,CPU可能會(huì)把這個(gè)段選擇符當(dāng)作物理內(nèi)存地址的高16位(只需將之左移16位,也就是乘以216)。這個(gè)簡(jiǎn)單的規(guī)則使得:可以按64K的段為單位,一塊塊的將4GB的內(nèi)存都尋址到。遺憾的是,Intel做了一個(gè)很詭異的設(shè)計(jì),讓段選擇符僅僅乘以24(或16),一舉將尋址范圍限制在了1MB,還引入了過(guò)度復(fù)雜的轉(zhuǎn)換過(guò)程。下述圖例顯示了一條跳轉(zhuǎn)指令,cs的值是0x1000: 實(shí)模式分段功能 實(shí)模式的段地址以16個(gè)字節(jié)為步長(zhǎng),從0開(kāi)始編號(hào)一直到0xFFFF0(即1MB)。你可以將一個(gè)從0到0xFFFF的16位偏移量(邏輯地址)加在段地址上。在這個(gè)規(guī)則下,對(duì)于同一個(gè)內(nèi)存地址,會(huì)有多個(gè)段地址/偏移量的組合與之對(duì)應(yīng),而且物理地址可以超過(guò)1MB的邊界,只要你的段地址足夠高(參見(jiàn)臭名昭著的A20線)。同樣的,在實(shí)模式的C語(yǔ)言代碼中,一個(gè)遠(yuǎn)指針(far pointer)既包含了段選擇符又包含了邏輯地址,用于尋址1MB的內(nèi)存范圍。真夠'遠(yuǎn)'的啊。隨著程序變得越來(lái)越大,超出了64K的段,分段功能以及它古怪的處理方式,使得x86平臺(tái)的軟件開(kāi)發(fā)變得非常復(fù)雜。這種設(shè)定可能聽(tīng)起來(lái)有些詭異,但它卻把當(dāng)時(shí)的程序員推進(jìn)了令人崩潰的深淵。 在32位保護(hù)模式下,段選擇符不再是一個(gè)單純的數(shù)值,取而代之的是一個(gè)索引編號(hào),用于引用段描述符表中的表項(xiàng)。這個(gè)表為一個(gè)簡(jiǎn)單的數(shù)組,元素長(zhǎng)度為8字節(jié),每個(gè)元素描述一個(gè)段??雌饋?lái)如下: 段描述符 有三種類型的段:代碼,數(shù)據(jù),系統(tǒng)。為了簡(jiǎn)潔明了,只有描述符的共有特征被繪制出來(lái)。基地址(base address)是一個(gè)32位的線性地址,指向段的開(kāi)始;段界限(limit)指出這個(gè)段有多大。將基地址加到邏輯地址上就形成了線性地址。DPL是描述符的特權(quán)級(jí)(privilege level),其值從0(最高特權(quán),內(nèi)核模式)到3(最低特權(quán),用戶模式),用于控制對(duì)段的訪問(wèn)。 這些段描述符被保存在兩個(gè)表中:全局描述符表(GDT)和局部描述符表(LDT)。電腦中的每一個(gè)CPU(或一個(gè)處理核心)都含有一個(gè)叫做gdtr的寄存器,用于保存GDT的首個(gè)字節(jié)所在的線性內(nèi)存地址。為了選出一個(gè)段,你必須向段寄存器加載符合以下格式的段選擇符: 段選擇符 對(duì)GDT,TI位為0;對(duì)LDT,TI位為1;index指出想要表中哪一個(gè)段描述符(譯注:原文是段選擇符,應(yīng)該是筆誤)。對(duì)于RPL,請(qǐng)求特權(quán)級(jí)(Requested Privilege Level),以后我們還會(huì)詳細(xì)討論?,F(xiàn)在,需要好好想想了。當(dāng)CPU運(yùn)行于32位模式時(shí),不管怎樣,寄存器和指令都可以尋址整個(gè)線性地址空間,所以根本就不需要再去使用基地址或其他什么鬼東西。那為什么不干脆將基地址設(shè)成0,好讓邏輯地址與線性地址一致呢?Intel的文檔將之稱為'扁平模型'(flat model),而且在現(xiàn)代的x86系統(tǒng)內(nèi)核中就是這么做的(特別指出,它們使用的是基本扁平模型)。基本扁平模型(basic flat model)等價(jià)于在轉(zhuǎn)換地址時(shí)關(guān)閉了分段功能。如此一來(lái)多么美好啊。就讓我們來(lái)看看32位保護(hù)模式下執(zhí)行一個(gè)跳轉(zhuǎn)指令的例子,其中的數(shù)值來(lái)自一個(gè)實(shí)際的Linux用戶模式應(yīng)用程序: 保護(hù)模式的分段 段描述符的內(nèi)容一旦被訪問(wèn),就會(huì)被cache(緩存),所以在隨后的訪問(wèn)中,就不再需要去實(shí)際讀取GDT了,否則會(huì)有損性能。每個(gè)段寄存器都有一個(gè)隱藏部分用于緩存段選擇符所對(duì)應(yīng)的那個(gè)段描述符。如果你想了解更多細(xì)節(jié),包括關(guān)于LDT的更多信息,請(qǐng)參閱《Intel System Programming Guide》3A卷的第三章。2A和2B卷講述了每一個(gè)x86指令,同時(shí)也指明了x86尋址時(shí)所使用的各種類型的操作數(shù):16位,16位加段描述符(可被用于實(shí)現(xiàn)遠(yuǎn)指針),32位,等等。 在Linux上,只有3個(gè)段描述符在引導(dǎo)啟動(dòng)過(guò)程被使用。他們使用GDT_ENTRY宏來(lái)定義并存儲(chǔ)在boot_gdt數(shù)組中。其中兩個(gè)段是扁平的,可對(duì)整個(gè)32位空間尋址:一個(gè)是代碼段,加載到cs中,一個(gè)是數(shù)據(jù)段,加載到其他段寄存器中。第三個(gè)段是系統(tǒng)段,稱為任務(wù)狀態(tài)段(Task State Segment)。在完成引導(dǎo)啟動(dòng)以后,每一個(gè)CPU都擁有一份屬于自己的GDT。其中大部分內(nèi)容是相同的,只有少數(shù)表項(xiàng)依賴于正在運(yùn)行的進(jìn)程。你可以從segment.h看到Linux GDT的布局以及其實(shí)際的樣子。這里有4個(gè)主要的GDT表項(xiàng):2個(gè)是扁平的,用于內(nèi)核模式的代碼和數(shù)據(jù),另兩個(gè)用于用戶模式。在看這個(gè)Linux GDT時(shí),請(qǐng)留意那些用于確保數(shù)據(jù)與CPU緩存線對(duì)齊的填充字節(jié)——目的是克服馮·諾依曼瓶頸。最后要說(shuō)說(shuō),那個(gè)經(jīng)典的Unix錯(cuò)誤信息'Segmentation fault'(分段錯(cuò)誤)并不是由x86風(fēng)格的段所引起的,而是由于分頁(yè)單元檢測(cè)到了非法的內(nèi)存地址。唉呀,下次再討論這個(gè)話題吧。 Intel巧妙的繞過(guò)了他們?cè)仍O(shè)計(jì)的那個(gè)拼拼湊湊的分段方法,而是提供了一種富于彈性的方式來(lái)讓我們選擇是使用段還是使用扁平模型。由于很容易將邏輯地址與線性地址合二為一,于是這成為了標(biāo)準(zhǔn),比如現(xiàn)在在64位模式中就強(qiáng)制使用扁平的線性地址空間了。但是即使是在扁平模型中,段對(duì)于x86的保護(hù)機(jī)制也十分重要。保護(hù)機(jī)制用于抵御用戶模式進(jìn)程對(duì)系統(tǒng)內(nèi)核的非法內(nèi)存訪問(wèn),或各個(gè)進(jìn)程之間的非法內(nèi)存訪問(wèn),否則系統(tǒng)將會(huì)進(jìn)入一個(gè)狗咬狗的世界!在下一篇文章中,我們將窺視保護(hù)級(jí)別以及如何用段來(lái)實(shí)現(xiàn)這些保護(hù)功能。 參考: http://blog.csdn.net/drshenlei/article/details/4261909 轉(zhuǎn): CPU的運(yùn)行環(huán), 特權(quán)級(jí)與保護(hù) 原文標(biāo)題:CPU Rings, Privilege, and Protection [注:本人水平有限,只好挑一些國(guó)外高手的精彩文章翻譯一下。一來(lái)自己復(fù)習(xí),二來(lái)與大家分享。] 可能你憑借直覺(jué)就知道應(yīng)用程序的功能受到了Intel x86計(jì)算機(jī)的某種限制,有些特定的任務(wù)只有操作系統(tǒng)的代碼才可以完成,但是你知道這到底是怎么一回事嗎?在這篇文章里,我們會(huì)接觸到x86的特權(quán)級(jí)(privilege level),看看操作系統(tǒng)和CPU是怎么一起合謀來(lái)限制用戶模式的應(yīng)用程序的。特權(quán)級(jí)總共有4個(gè),編號(hào)從0(最高特權(quán))到3(最低特權(quán))。有3種主要的資源受到保護(hù):內(nèi)存,I/O端口以及執(zhí)行特殊機(jī)器指令的能力。在任一時(shí)刻,x86 CPU都是在一個(gè)特定的特權(quán)級(jí)下運(yùn)行的,從而決定了代碼可以做什么,不可以做什么。這些特權(quán)級(jí)經(jīng)常被描述為保護(hù)環(huán)(protection ring),最內(nèi)的環(huán)對(duì)應(yīng)于最高特權(quán)。即使是最新的x86內(nèi)核也只用到其中的2個(gè)特權(quán)級(jí):0和3。 x86的保護(hù)環(huán) 在諸多機(jī)器指令中,只有大約15條指令被CPU限制只能在ring 0執(zhí)行(其余那么多指令的操作數(shù)都受到一定的限制)。這些指令如果被用戶模式的程序所使用,就會(huì)顛覆保護(hù)機(jī)制或引起混亂,所以它們被保留給內(nèi)核使用。如果企圖在ring 0以外運(yùn)行這些指令,就會(huì)導(dǎo)致一個(gè)一般保護(hù)錯(cuò)(general-protection exception),就像一個(gè)程序使用了非法的內(nèi)存地址一樣。類似的,對(duì)內(nèi)存和I/O端口的訪問(wèn)也受特權(quán)級(jí)的限制。但是,在我們分析保護(hù)機(jī)制之前,先讓我們看看CPU是怎么記錄當(dāng)前特權(quán)級(jí)的吧,這與前篇文章中提到的段選擇符(segment selector)有關(guān)。如下所示: 數(shù)據(jù)段和代碼段的段選擇符 數(shù)據(jù)段選擇符的整個(gè)內(nèi)容可由程序直接加載到各個(gè)段寄存器當(dāng)中,比如ss(堆棧段寄存器)和ds(數(shù)據(jù)段寄存器)。這些內(nèi)容里包含了請(qǐng)求特權(quán)級(jí)(Requested Privilege Level,簡(jiǎn)稱RPL)字段,其含義過(guò)會(huì)兒再說(shuō)。然而,代碼段寄存器(cs)就比較特別了。首先,它的內(nèi)容不能由裝載指令(如MOV)直接設(shè)置,而只能被那些會(huì)改變程序執(zhí)行順序的指令(如CALL)間接的設(shè)置。而且,不像那個(gè)可以被代碼設(shè)置的RPL字段,cs擁有一個(gè)由CPU自己維護(hù)的當(dāng)前特權(quán)級(jí)字段(Current Privilege Level,簡(jiǎn)稱CPL),這點(diǎn)對(duì)我們來(lái)說(shuō)非常重要。這個(gè)代碼段寄存器中的2位寬的CPL字段的值總是等于CPU的當(dāng)前特權(quán)級(jí)。Intel的文檔并未明確指出此事實(shí),而且有時(shí)在線文檔也對(duì)此含糊其辭,但這的確是個(gè)硬性規(guī)定。在任何時(shí)候,不管CPU內(nèi)部正在發(fā)生什么,只要看一眼cs中的CPL,你就可以知道此刻的特權(quán)級(jí)了。 記住,CPU特權(quán)級(jí)并不會(huì)對(duì)操作系統(tǒng)的用戶造成什么影響,不管你是根用戶,管理員,訪客還是一般用戶。所有的用戶代碼都在ring 3上執(zhí)行,所有的內(nèi)核代碼都在ring 0上執(zhí)行,跟是以哪個(gè)OS用戶的身份執(zhí)行無(wú)關(guān)。有時(shí)一些內(nèi)核任務(wù)可以被放到用戶模式中執(zhí)行,比如Windows Vista上的用戶模式驅(qū)動(dòng)程序,但是它們只是替內(nèi)核執(zhí)行任務(wù)的特殊進(jìn)程而已,而且往往可以被直接刪除而不會(huì)引起嚴(yán)重后果。 由于限制了對(duì)內(nèi)存和I/O端口的訪問(wèn),用戶模式代碼在不調(diào)用系統(tǒng)內(nèi)核的情況下,幾乎不能與外部世界交互。它不能打開(kāi)文件,發(fā)送網(wǎng)絡(luò)數(shù)據(jù)包,向屏幕打印信息或分配內(nèi)存。用戶模式進(jìn)程的執(zhí)行被嚴(yán)格限制在一個(gè)由ring 0之 神所設(shè)定的沙盤(pán)之中。這就是為什么從設(shè)計(jì)上就決定了:一個(gè)進(jìn)程所泄漏的內(nèi)存會(huì)在進(jìn)程結(jié)束后被統(tǒng)統(tǒng)回收,之前打開(kāi)的文件也會(huì)被自動(dòng)關(guān)閉。所有的控制著內(nèi)存或 打開(kāi)的文件等的數(shù)據(jù)結(jié)構(gòu)全都不能被用戶代碼直接使用;一旦進(jìn)程結(jié)束了,這個(gè)沙盤(pán)就會(huì)被內(nèi)核拆毀。這就是為什么我們的服務(wù)器只要硬件和內(nèi)核不出毛病,就可以 連續(xù)正常運(yùn)行600天,甚至一直運(yùn)行下去。這也解釋了為什么Windows 95/98那么容易死機(jī):這并非因?yàn)槲④洸顒?,而是因?yàn)橄到y(tǒng)中的一些重要數(shù)據(jù)結(jié)構(gòu),出于兼容的目的被設(shè)計(jì)成可以由用戶直接訪問(wèn)了。這在當(dāng)時(shí)可能是一個(gè)很好的折中,當(dāng)然代價(jià)也很大。 CPU會(huì)在兩個(gè)關(guān)鍵點(diǎn)上保護(hù)內(nèi)存:當(dāng)一個(gè)段選擇符被加載時(shí),以及,當(dāng)通過(guò)線形地址訪問(wèn)一個(gè)內(nèi)存頁(yè)時(shí)。因此,保護(hù)也反映在內(nèi)存地址轉(zhuǎn)換的過(guò)程之中,既包括分段又包括分頁(yè)。當(dāng)一個(gè)數(shù)據(jù)段選擇符被加載時(shí),就會(huì)發(fā)生下述的檢測(cè)過(guò)程: x86的分段保護(hù) 因?yàn)樵礁叩臄?shù)值代表越低的特權(quán),上圖中的MAX()用于挑出CPL和RPL中特權(quán)最低的一個(gè),并與描述符特權(quán)級(jí)(descriptor privilege level,簡(jiǎn)稱DPL)比較。如果DPL的值大于等于它,那么這個(gè)訪問(wèn)就獲得許可了。RPL背后的設(shè)計(jì)思想是:允許內(nèi)核代碼加載特權(quán)較低的段。比如,你可以使用RPL=3的段描述符來(lái)確保給定的操作所使用的段可以在用戶模式中訪問(wèn)。但堆棧段寄存器是個(gè)例外,它要求CPL,RPL和DPL這3個(gè)值必須完全一致,才可以被加載。 事實(shí)上,段保護(hù)功能幾乎沒(méi)什么用,因?yàn)楝F(xiàn)代的內(nèi)核使用扁平的地址空間。在那里,用戶模式的段可以訪問(wèn)整個(gè)線形地址空間。真正有用的內(nèi)存保護(hù)發(fā)生在分頁(yè)單元中,即從線形地址轉(zhuǎn)化為物理地址的時(shí)候。一個(gè)內(nèi)存頁(yè)就是由一個(gè)頁(yè)表項(xiàng)(page table entry)所描述的字節(jié)塊。頁(yè)表項(xiàng)包含兩個(gè)與保護(hù)有關(guān)的字段:一個(gè)超級(jí)用戶標(biāo)志(supervisor flag),一個(gè)讀寫(xiě)標(biāo)志(read/write flag)。超級(jí)用戶標(biāo)志是內(nèi)核所使用的重要的x86內(nèi)存保護(hù)機(jī)制。當(dāng)它開(kāi)啟時(shí),內(nèi)存頁(yè)就不能被ring 3訪問(wèn)了。盡管讀寫(xiě)標(biāo)志對(duì)于實(shí)施特權(quán)控制并不像前者那么重要,但它依然十分有用。當(dāng)一個(gè)進(jìn)程被加載后,那些存儲(chǔ)了二進(jìn)制鏡像(即代碼)的內(nèi)存頁(yè)就被標(biāo)記為只讀了,從而可以捕獲一些指針錯(cuò)誤,比如程序企圖通過(guò)此指針來(lái)寫(xiě)這些內(nèi)存頁(yè)。這個(gè)標(biāo)志還被用于在調(diào)用fork創(chuàng)建Unix子進(jìn)程時(shí),實(shí)現(xiàn)寫(xiě)時(shí)拷貝功能(copy on write)。 最后,我們需要一種方式來(lái)讓CPU切換它的特權(quán)級(jí)。如果ring 3的程序可以隨意的將控制轉(zhuǎn)移到(即跳轉(zhuǎn)到)內(nèi)核的任意位置,那么一個(gè)錯(cuò)誤的跳轉(zhuǎn)就會(huì)輕易的把操作系統(tǒng)毀掉了。但控制的轉(zhuǎn)移是必須的。這項(xiàng)工作是通過(guò)門(mén)描述符(gate descriptor)和sysenter指令來(lái)完成的。一個(gè)門(mén)描述符就是一個(gè)系統(tǒng)類型的段描述符,分為了4個(gè)子類型:調(diào)用門(mén)描述符(call-gate descriptor),中斷門(mén)描述符(interrupt-gate descriptor),陷阱門(mén)描述符(trap-gate descriptor)和任務(wù)門(mén)描述符(task-gate descriptor)。調(diào)用門(mén)提供了一個(gè)可以用于通常的CALL和JMP指令的內(nèi)核入口點(diǎn),但是由于調(diào)用門(mén)用得不多,我就忽略不提了。任務(wù)門(mén)也不怎么熱門(mén)(在Linux上,它們只在處理內(nèi)核或硬件問(wèn)題引起的雙重故障時(shí)才被用到)。 剩下兩個(gè)有趣的:中斷門(mén)和陷阱門(mén),它們用來(lái)處理硬件中斷(如鍵盤(pán),計(jì)時(shí)器,磁盤(pán))和異常(如缺頁(yè)異常,0除數(shù)異常)。我將不再區(qū)分中斷和異常,在文中統(tǒng)一用'中斷'一詞表示。這些門(mén)描述符被存儲(chǔ)在中斷描述符表(Interrupt Descriptor Table,簡(jiǎn)稱IDT)當(dāng)中。每一個(gè)中斷都被賦予一個(gè)從0到255的編號(hào),叫做中斷向量。處理器把中斷向量作為IDT表項(xiàng)的索引,用來(lái)指出當(dāng)中斷發(fā)生時(shí)使用哪一個(gè)門(mén)描述符來(lái)處理中斷。中斷門(mén)和陷阱門(mén)幾乎是一樣的。下圖給出了它們的格式。以及當(dāng)中斷發(fā)生時(shí)實(shí)施特權(quán)檢查的過(guò)程。我在其中填入了一些Linux內(nèi)核的典型數(shù)值,以便讓事情更加清晰具體。 伴隨特權(quán)檢查的中斷描述符 門(mén)中的DPL和段選擇符一起控制著訪問(wèn),同時(shí),段選擇符結(jié)合偏移量(Offset)指出了中斷處理代碼的入口點(diǎn)。內(nèi)核一般在門(mén)描述符中填入內(nèi)核代碼段的段選擇符。一個(gè)中斷永遠(yuǎn)不會(huì)將控制從高特權(quán)環(huán)轉(zhuǎn)向低特權(quán)環(huán)。特權(quán)級(jí)必須要么保持不變(當(dāng)內(nèi)核自己被中斷的時(shí)候),或被提升(當(dāng)用戶模式的代碼被中斷的時(shí)候)。無(wú)論哪一種情況,作為結(jié)果的CPL必須等于目的代碼段的DPL。如果CPL發(fā)生了改變,一個(gè)堆棧切換操作就會(huì)發(fā)生。如果中斷是被程序中的指令所觸發(fā)的(比如INT n),還會(huì)增加一個(gè)額外的檢查:門(mén)的DPL必須具有與CPL相同或更低的特權(quán)。這就防止了用戶代碼隨意觸發(fā)中斷。如果這些檢查失敗,正如你所猜測(cè)的,會(huì)產(chǎn)生一個(gè)一般保護(hù)錯(cuò)(general-protection exception)。所有的Linux中斷處理器都以ring 0特權(quán)退出。 在初始化階段,Linux內(nèi)核首先在setup_idt()中建立IDT,并忽略全部中斷。之后它使用include/asm-x86/desc.h的函數(shù)來(lái)填充普通的IDT表項(xiàng)(參見(jiàn)arch/x86/kernel/traps_32.c)。在Linux代碼中,名字中包含'system'字樣的門(mén)描述符是可以從用戶模式中訪問(wèn)的,而且其設(shè)置函數(shù)使用DPL 3。'system gate'是Intel的陷阱門(mén),也可以從用戶模式訪問(wèn)。除此之外,術(shù)語(yǔ)名詞都與本文對(duì)得上號(hào)。然而,硬件中斷門(mén)并不是在這里設(shè)置的,而是由適當(dāng)?shù)尿?qū)動(dòng)程序來(lái)完成。 有三個(gè)門(mén)可以被用戶模式訪問(wèn):中斷向量3和4分別用于調(diào)試和檢查數(shù)值運(yùn)算溢出。剩下的是一個(gè)系統(tǒng)門(mén),被設(shè)置為SYSCALL_VECTOR。對(duì)于x86體系結(jié)構(gòu),它等于0x80。它曾被作為一種機(jī)制,用于將進(jìn)程的控制轉(zhuǎn)移到內(nèi)核,進(jìn)行一個(gè)系統(tǒng)調(diào)用(system call),然后再跳轉(zhuǎn)回來(lái)。在那個(gè)時(shí)代,我需要去申請(qǐng)'INT 0x80'這個(gè)沒(méi)用的牌照 J。從奔騰Pro開(kāi)始,引入了sysenter指令,從此可以用這種更快捷的方式來(lái)啟動(dòng)系統(tǒng)調(diào)用了。它依賴于CPU上的特殊目的寄存器,這些寄存器存儲(chǔ)著代碼段、入口點(diǎn)及內(nèi)核系統(tǒng)調(diào)用處理器所需的其他零散信息。在sysenter執(zhí)行后,CPU不再進(jìn)行特權(quán)檢查,而是直接進(jìn)入CPL 0,并將新值加載到與代碼和堆棧有關(guān)的寄存器當(dāng)中(cs,eip,ss和esp)。只有ring 0的代碼enable_sep_cpu()可以加載sysenter 設(shè)置寄存器。 最后,當(dāng)需要跳轉(zhuǎn)回ring 3時(shí),內(nèi)核發(fā)出一個(gè)iret或sysexit指令,分別用于從中斷和系統(tǒng)調(diào)用中返回,從而離開(kāi)ring 0并恢復(fù)CPL=3的用戶代碼的執(zhí)行。噢!Vim提示我已經(jīng)接近1,900字了,所以I/O端口的保護(hù)只能下次再談了。這樣我們就結(jié)束了x86的運(yùn)行環(huán)與保護(hù)之旅。感謝您的耐心閱讀。 參考: http://blog.csdn.net/drshenlei/article/details/4265101轉(zhuǎn): Cache: 一個(gè)隱藏并保存數(shù)據(jù)的場(chǎng)所 原文標(biāo)題:Cache: a place for concealment and safekeeping [注:本人水平有限,只好挑一些國(guó)外高手的精彩文章翻譯一下。一來(lái)自己復(fù)習(xí),二來(lái)與大家分享。] 本文簡(jiǎn)要的展示了現(xiàn)代Intel處理器的CPU cache是如何組織的。有關(guān)cache的討論往往缺乏具體的實(shí)例,使得一些簡(jiǎn)單的概念變得撲朔迷離。也許是我可愛(ài)的小腦瓜有點(diǎn)遲鈍吧,但不管怎樣,至少下面講述了故事的前一半,即Core 2的 L1 cache是如何被訪問(wèn)的: L1 cache – 32KB,8路組相聯(lián),64字節(jié)緩存線 1. 由索引揀選緩存組(行) 在cache中的數(shù)據(jù)是以緩存線(line)為單位組織的,一條緩存線對(duì)應(yīng)于內(nèi)存中一個(gè)連續(xù)的字節(jié)塊。這個(gè)cache使用了64字節(jié)的緩存線。這些線被保存在cache bank中,也叫路(way)。每一路都有一個(gè)專門(mén)的目錄(directory)用來(lái)保存一些登記信息。你可以把每一路連同它的目錄想象成電子表格中的一列,而表的一行構(gòu)成了cache的一組(set)。列中的每一個(gè)單元(cell)都含有一條緩存線,由與之對(duì)應(yīng)的目錄單元跟蹤管理。圖中的cache有64 組、每組8路,因此有512個(gè)含有緩存線的單元,合計(jì)32KB的存儲(chǔ)空間。 在cache眼中,物理內(nèi)存被分割成了許多4KB大小的物理內(nèi)存頁(yè)(page)。每一頁(yè)都含有4KB / 64 bytes == 64條緩存線。在一個(gè)4KB的頁(yè)中,第0到63字節(jié)是第一條緩存線,第64到127字節(jié)是第二條緩存線,以此類推。每一頁(yè)都重復(fù)著這種劃分,所以第0頁(yè)第3條緩存線與第1頁(yè)第3條緩存線是不同的。 在全相聯(lián)緩存(fully associative cache)中,內(nèi)存中的任意一條緩存線都可以被存儲(chǔ)到任意的緩存單元中。這種存儲(chǔ)方式十分靈活,但也使得要訪問(wèn)它們時(shí),檢索緩存單元的工作變得復(fù)雜、昂貴。由于L1和L2 cache工作在很強(qiáng)的約束之下,包括功耗,芯片物理空間,存取速度等,所以在多數(shù)情況下,使用全相聯(lián)緩存并不是一個(gè)很好的折中。 取而代之的是圖中的組相聯(lián)緩存(set associative cache)。意思是,內(nèi)存中一條給定的緩存線只能被保存在一個(gè)特定的組(或行)中。所以,任意物理內(nèi)存頁(yè)的第0條緩存線(頁(yè)內(nèi)第0到63字節(jié))必須存儲(chǔ)到第0組,第1條緩存線存儲(chǔ)到第1組,以此類推。每一組有8個(gè)單元可用于存儲(chǔ)它所關(guān)聯(lián)的緩存線(譯注:就是那些需要存儲(chǔ)到這一組的緩存線),從而形成一個(gè)8路關(guān)聯(lián)的組(8-way associative set)。當(dāng)訪問(wèn)一個(gè)內(nèi)存地址時(shí),地址的第6到11位(譯注:組索引)指出了在4KB內(nèi)存頁(yè)中緩存線的編號(hào),從而決定了即將使用的緩存組。舉例來(lái)說(shuō),物理地址0x800010a0的組索引是000010,所以此地址的內(nèi)容一定是在第2組中緩存的。 但是還有一個(gè)問(wèn)題,就是要找出一組中哪個(gè)單元包含了想要的信息,如果有的話。這就到了緩存目錄登場(chǎng)的時(shí)刻。每一個(gè)緩存線都被其對(duì)應(yīng)的目錄單元做了標(biāo)記(tag);這個(gè)標(biāo)記就是一個(gè)簡(jiǎn)單的內(nèi)存頁(yè)編號(hào),指出緩存線來(lái)自于哪一頁(yè)。由于處理器可以尋址64GB的物理RAM,所以總共有64GB / 4KB == 224個(gè)內(nèi)存頁(yè),需要24位來(lái)保存標(biāo)記。前例中的物理地址0x800010a0對(duì)應(yīng)的頁(yè)號(hào)為524,289。下面是故事的后一半: 在組中搜索匹配標(biāo)記 由于我們只需要去查看某一組中的8路,所以查找匹配標(biāo)記是非常迅速的;事實(shí)上,從電學(xué)角度講,所有的標(biāo)記是同時(shí)進(jìn)行比對(duì)的,我用箭頭來(lái)表示這一點(diǎn)。如果此時(shí)正好有一條具有匹配標(biāo)簽的有效緩存線,我們就獲得一次緩存命中(cache hit)。否則,這個(gè)請(qǐng)求就會(huì)被轉(zhuǎn)發(fā)的L2 cache,如果還沒(méi)匹配上就再轉(zhuǎn)發(fā)給主系統(tǒng)內(nèi)存。通過(guò)應(yīng)用各種調(diào)節(jié)尺寸和容量的技術(shù),Intel給CPU配置了較大的L2 cache,但其基本的設(shè)計(jì)都是相同的。比如,你可以將原先的緩存增加8路而獲得一個(gè)64KB的緩存;再將組數(shù)增加到4096,每路可以存儲(chǔ)256KB。經(jīng)過(guò)這兩次修改,就得到了一個(gè)4MB的L2 cache。在此情況下,需要18位來(lái)保存標(biāo)記,12位保存組索引;緩存所使用的物理內(nèi)存頁(yè)的大小與其一路的大小相等。(譯注:有4096組,就需要lg(4096)==12位的組索引,緩存線依然是64字節(jié),所以一路有4096*64B==256KB字節(jié);在L2 cache眼中,內(nèi)存被分割為許多256KB的塊,所以需要lg(64GB/256KB)==18位來(lái)保存標(biāo)記。) 如果有一組已經(jīng)被放滿了,那么在另一條緩存線被存儲(chǔ)進(jìn)來(lái)之前,已有的某一條則必須被騰空(evict)。為了避免這種情況,對(duì)運(yùn)算速度要求較高的程序就要嘗試仔細(xì)組織它的數(shù)據(jù),使得內(nèi)存訪問(wèn)均勻的分布在已有的緩存線上。舉例來(lái)說(shuō),假設(shè)程序中有一個(gè)數(shù)組,元素的大小是512字節(jié),其中一些對(duì)象在內(nèi)存中相距4KB。這些對(duì)象的各個(gè)字段都落在同一緩存線上,并競(jìng)爭(zhēng)同一緩存組。如果程序頻繁的訪問(wèn)一個(gè)給定的字段(比如,通過(guò)虛函數(shù)表vtable調(diào)用虛函數(shù)),那么這個(gè)組看起來(lái)就好像一直是被填滿的,緩存開(kāi)始變得毫無(wú)意義,因?yàn)榫彺婢€一直在重復(fù)著騰空與重新載入的步驟。在我們的例子中,由于組數(shù)的限制,L1 cache僅能保存8個(gè)這類對(duì)象的虛函數(shù)表。這就是組相聯(lián)策略的折中所付出的代價(jià):即使在整體緩存的使用率并不高的情況下,由于組沖突,我們還是會(huì)遇到緩存缺失的情況。然而,鑒于計(jì)算機(jī)中各個(gè)存儲(chǔ)層次的相對(duì)速度,不管怎么說(shuō),大部分的應(yīng)用程序并不必為此而擔(dān)心。 一個(gè)內(nèi)存訪問(wèn)經(jīng)常由一個(gè)線性(或虛擬)地址發(fā)起,所以L1 cache需要依賴分頁(yè)單元(paging unit)來(lái)求出物理內(nèi)存頁(yè)的地址,以便用于緩存標(biāo)記。與此相反,組索引來(lái)自于線性地址的低位,所以不需要轉(zhuǎn)換就可以使用了(在我們的例子中為第6到11位)。因此L1 cache是物理標(biāo)記但虛擬索引的(physically tagged but virtually indexed),從而幫助CPU進(jìn)行并行的查找操作。因?yàn)?/span>L1 cache的一路絕不會(huì)比MMU的一頁(yè)還大,所以可以保證一個(gè)給定的物理地址位置總是關(guān)聯(lián)到同一組,即使組索引是虛擬的。在另一方面L2 cache必須是物理標(biāo)記和物理索引的,因?yàn)樗囊宦繁?/span>MMU的一頁(yè)要大。但是,當(dāng)一個(gè)請(qǐng)求到達(dá)L2 cache時(shí),物理地址已經(jīng)被L1 cache準(zhǔn)備(resolved)完畢了,所以L2 cache會(huì)工作得很好。 最后,目錄單元還存儲(chǔ)了對(duì)應(yīng)緩存線的狀態(tài)(state)。在L1代碼緩存中的一條緩存線要么是無(wú)效的(invalid)要么是共享的(shared,意思是有效的,真的J)。在L1數(shù)據(jù)緩存和L2緩存中,一條緩存線可以為4個(gè)MESI狀態(tài)之一:被修改的(modified),獨(dú)占的(exclusive),共享的(shared),無(wú)效的(invalid)。Intel緩存是包容式的(inclusive):L1緩存的內(nèi)容會(huì)被復(fù)制到L2緩存中。在下一篇討論線程(threading),鎖定(locking)等內(nèi)容的文章中,這些緩存線狀態(tài)將發(fā)揮作用。下一次,我們將看看前端總線以及內(nèi)存訪問(wèn)到底是怎么工作的。這將成為一個(gè)內(nèi)存研討周。 (在回復(fù)中Dave提到了直接映射緩存(direct-mapped cache)。它們基本上是一種特殊的組相聯(lián)緩存,只是只有一路而已。在各種折中方案中,它與全相聯(lián)緩存正好相反:訪問(wèn)非??旖荩蚪M沖突而導(dǎo)致的緩存缺失也非常多。) [譯者小結(jié): 1. 內(nèi)存層次結(jié)構(gòu)的意義在于利用引用的空間局部性和時(shí)間局部性原理,將經(jīng)常被訪問(wèn)的數(shù)據(jù)放到快速的存儲(chǔ)器中,而將不經(jīng)常訪問(wèn)的數(shù)據(jù)留在較慢的存儲(chǔ)器中。 2. 一般情況下,除了寄存器和L1緩存可以操作指定字長(zhǎng)的數(shù)據(jù),下層的內(nèi)存子系統(tǒng)就不會(huì)再使用這么小的單位了,而是直接移動(dòng)數(shù)據(jù)塊,比如以緩存線為單位訪問(wèn)數(shù)據(jù)。 3. 對(duì)于組沖突,可以這么理解:與上文相似,假設(shè)一個(gè)緩存,由512條緩存線組成,每條線64字節(jié),容量32KB。 a) 假如它是直接映射緩存,由于它往往使用地址的低位直接映射緩存線編號(hào),所以所有的32K倍數(shù)的地址(32K,64K,96K等)都會(huì)映射到同一條線上(即第0線)。假如程序的內(nèi)存組織不當(dāng),交替的去訪問(wèn)布置在這些地址的數(shù)據(jù),則會(huì)導(dǎo)致沖突。從外表看來(lái)就好像緩存只有1條線了,盡管其他緩存線一直是空閑著的。 b) 如果是全相聯(lián)緩存,那么每條緩存線都是獨(dú)立的,可以對(duì)應(yīng)于內(nèi)存中的任意緩存線。只有當(dāng)所有的512條緩存線都被占滿后才會(huì)出現(xiàn)沖突。 c) 組相聯(lián)是前兩者的折中,每一路中的緩存線采用直接映射方式,而在路與路之間,緩存控制器使用全相聯(lián)映射算法,決定選擇一組中的哪一條線。 d) 如果是2路組相聯(lián)緩存,那么這512條緩存線就被分為了2路,每路256條線,一路16KB。此時(shí)所有為16K整數(shù)倍的地址(16K,32K,48K等)都會(huì)映射到第0線,但由于2路是關(guān)聯(lián)的,所以可以同時(shí)有2個(gè)這種地址的內(nèi)容被緩存,不會(huì)發(fā)生沖突。當(dāng)然了,如果要訪問(wèn)第三個(gè)這種地址,還是要先騰空已有的一條才行。所以極端情況下,從外表看來(lái)就好像緩存只有2條線了,盡管其他緩存線一直是空閑著的。 e) 如果是8路組相聯(lián)緩存(與文中示例相同),那么這512條緩存線就被分為了8路,每路64條線,一路4KB。所以如果數(shù)組中元素地址是4K對(duì)齊的,并且程序交替的訪問(wèn)這些元素,就會(huì)出現(xiàn)組沖突。從外表看來(lái)就好像緩存只有8條線了,盡管其他緩存線一直是空閑著的。 ] 參考: http://blog.csdn.net/drshenlei/article/details/4277959 轉(zhuǎn): 剖析程序的內(nèi)存布局 原文標(biāo)題:Anatomy of a Program in Memory [注:本人水平有限,只好挑一些國(guó)外高手的精彩文章翻譯一下。一來(lái)自己復(fù)習(xí),二來(lái)與大家分享。] 內(nèi)存管理模塊是操作系統(tǒng)的心臟;它對(duì)應(yīng)用程序和系統(tǒng)管理非常重要。今后的幾篇文章中,我將著眼于實(shí)際的內(nèi)存問(wèn)題,但也不避諱其中的技術(shù)內(nèi)幕。由于不少概念是通用的,所以文中大部分例子取自32位x86平臺(tái)的Linux和Windows系統(tǒng)。本系列第一篇文章講述應(yīng)用程序的內(nèi)存布局。 在多任務(wù)操作系統(tǒng)中的每一個(gè)進(jìn)程都運(yùn)行在一個(gè)屬于它自己的內(nèi)存沙盤(pán)中。這個(gè)沙盤(pán)就是虛擬地址空間(virtual address space),在32位模式下它總是一個(gè)4GB的內(nèi)存地址塊。這些虛擬地址通過(guò)頁(yè)表(page table)映射到物理內(nèi)存,頁(yè)表由操作系統(tǒng)維護(hù)并被處理器引用。每一個(gè)進(jìn)程擁有一套屬于它自己的頁(yè)表,但是還有一個(gè)隱情。只要虛擬地址被使能,那么它就會(huì)作用于這臺(tái)機(jī)器上運(yùn)行的所有軟件,包括內(nèi)核本身。因此一部分虛擬地址必須保留給內(nèi)核使用: 這并不意味著內(nèi)核使用了那么多的物理內(nèi)存,僅表示它可支配這么大的地址空間,可根據(jù)內(nèi)核需要,將其映射到物理內(nèi)存。內(nèi)核空間在頁(yè)表中擁有較高的特權(quán)級(jí)(ring 2或以下),因此只要用戶態(tài)的程序試圖訪問(wèn)這些頁(yè),就會(huì)導(dǎo)致一個(gè)頁(yè)錯(cuò)誤(page fault)。在Linux中,內(nèi)核空間是持續(xù)存在的,并且在所有進(jìn)程中都映射到同樣的物理內(nèi)存。內(nèi)核代碼和數(shù)據(jù)總是可尋址的,隨時(shí)準(zhǔn)備處理中斷和系統(tǒng)調(diào)用。與此相反,用戶模式地址空間的映射隨進(jìn)程切換的發(fā)生而不斷變化:
藍(lán)色區(qū)域表示映射到物理內(nèi)存的虛擬地址,而白色區(qū)域表示未映射的部分。在上面的例子中,Firefox使用了相當(dāng)多的虛擬地址空間,因?yàn)樗莻髡f(shuō)中的吃內(nèi)存大戶。地址空間中的各個(gè)條帶對(duì)應(yīng)于不同的內(nèi)存段(memory segment),如:堆、棧之類的。記住,這些段只是簡(jiǎn)單的內(nèi)存地址范圍,與Intel處理器的段沒(méi)有關(guān)系。不管怎樣,下面是一個(gè)Linux進(jìn)程的標(biāo)準(zhǔn)的內(nèi)存段布局:
當(dāng)計(jì)算機(jī)開(kāi)心、安全、可愛(ài)、正常的運(yùn)轉(zhuǎn)時(shí),幾乎每一個(gè)進(jìn)程的各個(gè)段的起始虛擬地址都與上圖完全一致,這也給遠(yuǎn)程發(fā)掘程序安全漏洞打開(kāi)了方便之門(mén)。一個(gè)發(fā)掘過(guò)程往往需要引用絕對(duì)內(nèi)存地址:棧地址,庫(kù)函數(shù)地址等。遠(yuǎn)程攻擊者必須依賴地址空間布局的一致性,摸索著選擇這些地址。如果讓他們猜個(gè)正著,有人就會(huì)被整了。因此,地址空間的隨機(jī)排布方式逐漸流行起來(lái)。Linux通過(guò)對(duì)棧、內(nèi)存映射段、堆的起始地址加上隨機(jī)的偏移量來(lái)打亂布局。不幸的是,32位地址空間相當(dāng)緊湊,給隨機(jī)化所留下的空當(dāng)不大,削弱了這種技巧的效果。 進(jìn)程地址空間中最頂部的段是棧,大多數(shù)編程語(yǔ)言將之用于存儲(chǔ)局部變量和函數(shù)參數(shù)。調(diào)用一個(gè)方法或函數(shù)會(huì)將一個(gè)新的棧楨(stack frame)壓入棧中。棧楨在函數(shù)返回時(shí)被清理。也許是因?yàn)閿?shù)據(jù)嚴(yán)格的遵從LIFO的順序,這個(gè)簡(jiǎn)單的設(shè)計(jì)意味著不必使用復(fù)雜的數(shù)據(jù)結(jié)構(gòu)來(lái)追蹤棧的內(nèi)容,只需要一個(gè)簡(jiǎn)單的指針指向棧的頂端即可。因此壓棧(pushing)和退棧(popping)過(guò)程非常迅速、準(zhǔn)確。另外,持續(xù)的重用??臻g有助于使活躍的棧內(nèi)存保持在CPU緩存中,從而加速訪問(wèn)。進(jìn)程中的每一個(gè)線程都有屬于自己的棧。 通過(guò)不斷向棧中壓入的數(shù)據(jù),超出其容量就有會(huì)耗盡棧所對(duì)應(yīng)的內(nèi)存區(qū)域。這將觸發(fā)一個(gè)頁(yè)故障(page fault),并被Linux的expand_stack()處理,它會(huì)調(diào)用acct_stack_growth()來(lái)檢查是否還有合適的地方用于棧的增長(zhǎng)。如果棧的大小低于RLIMIT_STACK(通常是8MB),那么一般情況下棧會(huì)被加長(zhǎng),程序繼續(xù)愉快的運(yùn)行,感覺(jué)不到發(fā)生了什么事情。這是一種將棧擴(kuò)展至所需大小的常規(guī)機(jī)制。然而,如果達(dá)到了最大的??臻g大小,就會(huì)棧溢出(stack overflow),程序收到一個(gè)段錯(cuò)誤(Segmentation Fault)。當(dāng)映射了的棧區(qū)域擴(kuò)展到所需的大小后,它就不會(huì)再收縮回去,即使棧不那么滿了。這就好比聯(lián)邦預(yù)算,它總是在增長(zhǎng)的。 動(dòng)態(tài)棧增長(zhǎng)是唯一一種訪問(wèn)未映射內(nèi)存區(qū)域(圖中白色區(qū)域)而被允許的情形。其它任何對(duì)未映射內(nèi)存區(qū)域的訪問(wèn)都會(huì)觸發(fā)頁(yè)故障,從而導(dǎo)致段錯(cuò)誤。一些被映射的區(qū)域是只讀的,因此企圖寫(xiě)這些區(qū)域也會(huì)導(dǎo)致段錯(cuò)誤。 在棧的下方,是我們的內(nèi)存映射段。此處,內(nèi)核將文件的內(nèi)容直接映射到內(nèi)存。任何應(yīng)用程序都可以通過(guò)Linux的mmap()系統(tǒng)調(diào)用(實(shí)現(xiàn))或Windows的CreateFileMapping() / MapViewOfFile()請(qǐng)求這種映射。內(nèi)存映射是一種方便高效的文件I/O方式,所以它被用于加載動(dòng)態(tài)庫(kù)。創(chuàng)建一個(gè)不對(duì)應(yīng)于任何文件的匿名內(nèi)存映射也是可能的,此方法用于存放程序的數(shù)據(jù)。在Linux中,如果你通過(guò)malloc()請(qǐng)求一大塊內(nèi)存,C運(yùn)行庫(kù)將會(huì)創(chuàng)建這樣一個(gè)匿名映射而不是使用堆內(nèi)存。'大塊'意味著比MMAP_THRESHOLD還大,缺省是128KB,可以通過(guò)mallopt()調(diào)整。 說(shuō)到堆,它是接下來(lái)的一塊地址空間。與棧一樣,堆用于運(yùn)行時(shí)內(nèi)存分配;但不同點(diǎn)是,堆用于存儲(chǔ)那些生存期與函數(shù)調(diào)用無(wú)關(guān)的數(shù)據(jù)。大部分語(yǔ)言都提供了堆管理功能。因此,滿足內(nèi)存請(qǐng)求就成了語(yǔ)言運(yùn)行時(shí)庫(kù)及內(nèi)核共同的任務(wù)。在C語(yǔ)言中,堆分配的接口是malloc()系列函數(shù),而在具有垃圾收集功能的語(yǔ)言(如C#)中,此接口是new關(guān)鍵字。 如果堆中有足夠的空間來(lái)滿足內(nèi)存請(qǐng)求,它就可以被語(yǔ)言運(yùn)行時(shí)庫(kù)處理而不需要內(nèi)核參與。否則,堆會(huì)被擴(kuò)大,通過(guò)brk()系統(tǒng)調(diào)用(實(shí)現(xiàn))來(lái)分配請(qǐng)求所需的內(nèi)存塊。堆管理是很復(fù)雜的,需要精細(xì)的算法,應(yīng)付我們程序中雜亂的分配模式,優(yōu)化速度和內(nèi)存使用效率。處理一個(gè)堆請(qǐng)求所需的時(shí)間會(huì)大幅度的變動(dòng)。實(shí)時(shí)系統(tǒng)通過(guò)特殊目的分配器來(lái)解決這個(gè)問(wèn)題。堆也可能會(huì)變得零零碎碎,如下圖所示:
最后,我們來(lái)看看最底部的內(nèi)存段:BSS,數(shù)據(jù)段,代碼段。在C語(yǔ)言中,BSS和數(shù)據(jù)段保存的都是靜態(tài)(全局)變量的內(nèi)容。區(qū)別在于BSS保存的是未被初始化的靜態(tài)變量?jī)?nèi)容,它們的值不是直接在程序的源代碼中設(shè)定的。BSS內(nèi)存區(qū)域是匿名的:它不映射到任何文件。如果你寫(xiě)static int cntActiveUsers,則cntActiveUsers的內(nèi)容就會(huì)保存在BSS中。 另一方面,數(shù)據(jù)段保存在源代碼中已經(jīng)初始化了的靜態(tài)變量?jī)?nèi)容。這個(gè)內(nèi)存區(qū)域不是匿名的。它映射了一部分的程序二進(jìn)制鏡像,也就是源代碼中指定了初始值的靜態(tài)變量。所以,如果你寫(xiě)static int cntWorkerBees = 10,則cntWorkerBees的內(nèi)容就保存在數(shù)據(jù)段中了,而且初始值為10。盡管數(shù)據(jù)段映射了一個(gè)文件,但它是一個(gè)私有內(nèi)存映射,這意味著更改此處的內(nèi)存不會(huì)影響到被映射的文件。也必須如此,否則給全局變量賦值將會(huì)改動(dòng)你硬盤(pán)上的二進(jìn)制鏡像,這是不可想象的。 下圖中數(shù)據(jù)段的例子更加復(fù)雜,因?yàn)樗昧艘粋€(gè)指針。在此情況下,指針gonzo(4字節(jié)內(nèi)存地址)本身的值保 存在數(shù)據(jù)段中。而它所指向的實(shí)際字符串則不在這里。這個(gè)字符串保存在代碼段中,代碼段是只讀的,保存了你全部的代碼外加零零碎碎的東西,比如字符串字面 值。代碼段將你的二進(jìn)制文件也映射到了內(nèi)存中,但對(duì)此區(qū)域的寫(xiě)操作都會(huì)使你的程序收到段錯(cuò)誤。這有助于防范指針錯(cuò)誤,雖然不像在C語(yǔ)言編程時(shí)就注意防范來(lái)得那么有效。下圖展示了這些段以及我們例子中的變量:
你可以通過(guò)閱讀文件/proc/pid_of_process/maps來(lái)檢驗(yàn)一個(gè)Linux進(jìn)程中的內(nèi)存區(qū)域。記住一個(gè)段可能包含許多區(qū)域。比如,每個(gè)內(nèi)存映射文件在mmap段中都有屬于自己的區(qū)域,動(dòng)態(tài)庫(kù)擁有類似BSS和數(shù)據(jù)段的額外區(qū)域。下一篇文章講說(shuō)明這些'區(qū)域'(area)的真正含義。有時(shí)人們提到'數(shù)據(jù)段',指的就是全部的數(shù)據(jù)段 + BSS + 堆。 你可以通過(guò)nm和objdump命令來(lái)察看二進(jìn)制鏡像,打印其中的符號(hào),它們的地址,段等信息。最后需要指出的是,前文描述的虛擬地址布局在Linux中是一種'靈活布局'(flexible layout),而且以此作為默認(rèn)方式已經(jīng)有些年頭了。它假設(shè)我們有值RLIMIT_STACK。當(dāng)情況不是這樣時(shí),Linux退回使用'經(jīng)典布局'(classic layout),如下圖所示:
對(duì)虛擬地址空間的布局就講這些吧。下一篇文章將討論內(nèi)核是如何跟蹤這些內(nèi)存區(qū)域的。我們會(huì)分析內(nèi)存映射,看看文件的讀寫(xiě)操作是如何與之關(guān)聯(lián)的,以及內(nèi)存使用概況的含義。 參考: http://blog.csdn.net/drshenlei/article/details/4339110轉(zhuǎn): 內(nèi)核是如何管理內(nèi)存的 原文標(biāo)題:How The Kernel Manages Your Memory [注:本人水平有限,只好挑一些國(guó)外高手的精彩文章翻譯一下。一來(lái)自己復(fù)習(xí),二來(lái)與大家分享。] 在仔細(xì)審視了進(jìn)程的虛擬地址布局之后,讓我們把目光轉(zhuǎn)向內(nèi)核以及其管理用戶內(nèi)存的機(jī)制。再次從gonzo圖示開(kāi)始:
Linux進(jìn)程在內(nèi)核中是由task_struct的實(shí)例來(lái)表示的,即進(jìn)程描述符。task_struct的mm字段指向內(nèi)存描述符(memory descriptor),即mm_struct,一個(gè)程序的內(nèi)存的執(zhí)行期摘要。它存儲(chǔ)了上圖所示的內(nèi)存段的起止位置,進(jìn)程所使用的物理內(nèi)存頁(yè)的數(shù)量(rss表示Resident Set Size),虛擬內(nèi)存空間的使用量,以及其他信息。我們還可以在內(nèi)存描述符中找到用于管理程序內(nèi)存的兩個(gè)重要結(jié)構(gòu):虛擬內(nèi)存區(qū)域集合(the set of virtual memory areas)及頁(yè)表(page table)。Gonzo的內(nèi)存區(qū)域如下圖所示:
每一個(gè)虛擬內(nèi)存區(qū)域(簡(jiǎn)稱VMA)是一個(gè)連續(xù)的虛擬地址范圍;這些區(qū)域不會(huì)交疊。一個(gè)vm_area_struct的實(shí)例完備的描述了一個(gè)內(nèi)存區(qū)域,包括它的起止地址,決定訪問(wèn)權(quán)限和行為的標(biāo)志位,還有vm_file字段,用于指出被映射的文件(如果有的話)。一個(gè)VMA如果沒(méi)有映射到文件,則是匿名的(anonymous)。除memory mapping 段以外,上圖中的每一個(gè)內(nèi)存段(如:堆,棧)都對(duì)應(yīng)于一個(gè)單獨(dú)的VMA。這并不是強(qiáng)制要求,但在x86機(jī)器上經(jīng)常如此。VMA并不關(guān)心它在哪一個(gè)段。 一個(gè)程序的VMA同時(shí)以兩種形式存儲(chǔ)在它的內(nèi)存描述符中:一個(gè)是按起始虛擬地址排列的鏈表,保存在mmap字段;另一個(gè)是紅黑樹(shù),根節(jié)點(diǎn)保存在mm_rb字段。紅黑樹(shù)使得內(nèi)核可以快速的查找出給定虛擬地址所屬的內(nèi)存區(qū)域。當(dāng)你讀取文件/proc/pid_of_process/maps時(shí),內(nèi)核只須簡(jiǎn)單的遍歷指定進(jìn)程的VMA鏈表,并打印出每一項(xiàng)來(lái)即可。 在Windows中,EPROCESS塊可以粗略的看成是task_struct和mm_struct的組合。VMA在Windows中的對(duì)應(yīng)物時(shí)虛擬地址描述符(Virtual Address Descriptor),或簡(jiǎn)稱VAD;它們保存在平衡樹(shù)中(AVL tree)。你知道Windows和Linux最有趣的地方是什么嗎?就是這些細(xì)小的不同點(diǎn)。 4GB虛擬地址空間被分割為許多頁(yè)(page)。x86處理器在32位模式下所支持的頁(yè)面大小為4KB,2MB和4MB。Linux和Windows都使用4KB大小的頁(yè)面來(lái)映射用戶部分的虛擬地址空間。第0-4095字節(jié)在第0頁(yè),第4096-8191字節(jié)在第1頁(yè),以此類推。VMA的大小必須是頁(yè)面大小的整數(shù)倍。下圖是以4KB分頁(yè)的3GB用戶空間:
處理器會(huì)依照頁(yè)表(page table)來(lái)將虛擬地址轉(zhuǎn)換到物理內(nèi)存地址。每個(gè)進(jìn)程都有屬于自己的一套頁(yè)表;一旦進(jìn)程發(fā)生了切換,用戶空間的頁(yè)表也會(huì)隨之切換。Linux在內(nèi)存描述符的pgd字段保存了一個(gè)指向進(jìn)程頁(yè)表的指針。每一個(gè)虛擬內(nèi)存頁(yè)在頁(yè)表中都有一個(gè)與之對(duì)應(yīng)的頁(yè)表項(xiàng)(page table entry),簡(jiǎn)稱PTE。它在普通的x86分頁(yè)機(jī)制下,是一個(gè)簡(jiǎn)單的4字節(jié)記錄,如下圖所示:
Linux有一些函數(shù)可以用于讀取或設(shè)置PTE中的每一個(gè)標(biāo)志。P位告訴處理器虛擬頁(yè)面是否存在于(present)物理內(nèi)存中。如果是0,訪問(wèn)這個(gè)頁(yè)將觸發(fā)頁(yè)故障(page fault)。記住,當(dāng)這個(gè)位是0時(shí),內(nèi)核可以根據(jù)喜好,隨意的使用其余的字段。R/W標(biāo)志表示讀/寫(xiě);如果是0,頁(yè)面就是只讀的。U/S標(biāo)志表示用戶/管理員;如果是0,則這個(gè)頁(yè)面只能被內(nèi)核訪問(wèn)。這些標(biāo)志用于實(shí)現(xiàn)只讀內(nèi)存和保護(hù)內(nèi)核空間。 D位和A位表示數(shù)據(jù)臟(dirty)和訪問(wèn)過(guò)(accessed)。臟表示頁(yè)面被執(zhí)行過(guò)寫(xiě)操作,訪問(wèn)過(guò)表示頁(yè)面被讀或被寫(xiě)過(guò)。這兩個(gè)標(biāo)志都是粘滯的:處理器只會(huì)將它們置位,之后必須由內(nèi)核來(lái)清除。最后,PTE還保存了對(duì)應(yīng)該頁(yè)的起始物理內(nèi)存地址,對(duì)齊于4KB邊界。PTE中的其他字段我們改日再談,比如物理地址擴(kuò)展(Physical Address Extension)。 虛擬頁(yè)面是內(nèi)存保護(hù)的最小單元,因?yàn)轫?yè)內(nèi)的所有字節(jié)都共享U/S和R/W標(biāo)志。然而,同樣的物理內(nèi)存可以被映射到不同的頁(yè)面,甚至可以擁有不同的保護(hù)標(biāo)志。值得注意的是,在PTE中沒(méi)有對(duì)執(zhí)行許可(execute permission)的設(shè)定。這就是為什么經(jīng)典的x86分頁(yè)可以執(zhí)行位于stack上的代碼,從而為黑客利用堆棧溢出提供了便利(使用return-to-libc和其他技術(shù),甚至可以利用不可執(zhí)行的堆棧)。PTE缺少不可執(zhí)行(no-execute)標(biāo)志引出了一個(gè)影響更廣泛的事實(shí):VMA中的各種許可標(biāo)志可能會(huì)也可能不會(huì)被明確的轉(zhuǎn)換為硬件保護(hù)。對(duì)此,內(nèi)核可以盡力而為,但始終受到架構(gòu)的限制。 虛擬內(nèi)存并不存儲(chǔ)任何東西,它只是將程序地址空間映射到底層的物理內(nèi)存上,后者被處理器視為一整塊來(lái)訪問(wèn),稱作物理地址空間(physical address space)。對(duì)物理內(nèi)存的操作還與總線有點(diǎn)聯(lián)系,好在我們可以暫且忽略這些并假設(shè)物理地址范圍以字節(jié)為單位遞增,從0到最大可用內(nèi)存數(shù)。這個(gè)物理地址空間被內(nèi)核分割為一個(gè)個(gè)頁(yè)幀(page frame)。處理器并不知道也不關(guān)心這些幀,然而它們對(duì)內(nèi)核至關(guān)重要,因?yàn)?strong>頁(yè)幀是物理內(nèi)存管理的最小單元。Linux和Windows在32位模式下,都使用4KB大小的頁(yè)幀;以一個(gè)擁有2GB RAM的機(jī)器為例:
在Linux中,每一個(gè)頁(yè)幀都由一個(gè)描述符和一些標(biāo)志所跟蹤。這些描述符合在一起,記錄了計(jì)算機(jī)內(nèi)的全部物理內(nèi)存;可以隨時(shí)知道每一個(gè)頁(yè)幀的準(zhǔn)確狀態(tài)。物理內(nèi)存是用buddy memory allocation技術(shù)來(lái)管理的,因此如果一個(gè)頁(yè)幀可被buddy 系統(tǒng)分配,則它就是可用的(free)。一個(gè)被分配了的頁(yè)幀可能是匿名的(anonymous),保存著程序數(shù)據(jù);也可能是頁(yè)緩沖的(page cache),保存著一個(gè)文件或塊設(shè)備的數(shù)據(jù)。還有其他一些古怪的頁(yè)幀使用形式,但現(xiàn)在先不必考慮它們。Windows使用一個(gè)類似的頁(yè)幀編號(hào)(Page Frame Number簡(jiǎn)稱PFN)數(shù)據(jù)庫(kù)來(lái)跟蹤物理內(nèi)存。 讓我們把虛擬地址區(qū)域,頁(yè)表項(xiàng),頁(yè)幀放到一起,看看它們到底是怎么工作的。下圖是一個(gè)用戶堆的例子:
藍(lán)色矩形表示VMA范圍內(nèi)的頁(yè),箭頭表示頁(yè)表項(xiàng)將頁(yè)映射到頁(yè)幀上。一些虛擬頁(yè)并沒(méi)有箭頭;這意味著它們對(duì)應(yīng)的PTE的存在位(Present flag)為0。形成這種情況的原因可能是這些頁(yè)還沒(méi)有被訪問(wèn)過(guò),或者它們的內(nèi)容被系統(tǒng)換出了(swap out)。無(wú)論那種情況,對(duì)這些頁(yè)的訪問(wèn)都會(huì)導(dǎo)致頁(yè)故障(page fault),即使它們處在VMA之內(nèi)。VMA和頁(yè)表的不一致看起來(lái)令人奇怪,但實(shí)際經(jīng)常如此。 一個(gè)VMA就像是你的程序和內(nèi)核之間的契約。你請(qǐng)求去做一些事情(如:內(nèi)存分配,文件映射等),內(nèi)核說(shuō)'行',并創(chuàng)建或更新適當(dāng)?shù)?/span>VMA。但它并非立刻就去完成請(qǐng)求,而是一直等到出現(xiàn)了頁(yè)故障才會(huì)真正去做。內(nèi)核就是一個(gè)懶惰,騙人的敗類;這是虛擬內(nèi)存管理的基本原則。它對(duì)大多數(shù)情況都適用,有些比較熟悉,有些令人驚訝,但這個(gè)規(guī)則就是這樣:VMA記錄了雙方商定做什么,而PTE反映出懶惰的內(nèi)核實(shí)際做了什么。這兩個(gè)數(shù)據(jù)結(jié)構(gòu)共同管理程序的內(nèi)存;都扮演著解決頁(yè)故障,釋放內(nèi)存,換出內(nèi)存(swapping memory out)等等角色。讓我們看一個(gè)簡(jiǎn)單的內(nèi)存分配的例子:
當(dāng)程序通過(guò)brk()系統(tǒng)調(diào)用請(qǐng)求更多的內(nèi)存時(shí),內(nèi)核只是簡(jiǎn)單的更新堆的VMA,然后說(shuō)搞好啦。其實(shí)此時(shí)并沒(méi)有頁(yè)幀被分配,新的頁(yè)也并沒(méi)有出現(xiàn)于物理內(nèi)存中。一旦程序試圖訪問(wèn)這些頁(yè),處理器就會(huì)報(bào)告頁(yè)故障,并調(diào)用do_page_fault()。它會(huì)通過(guò)調(diào)用find_vma()去搜索哪一個(gè)VMA含蓋了產(chǎn)生故障的虛擬地址。如果找到了,還會(huì)根據(jù)VMA上的訪問(wèn)許可來(lái)比對(duì)檢查訪問(wèn)請(qǐng)求(讀或?qū)懀?。如果沒(méi)有合適的VMA,也就是說(shuō)內(nèi)存訪問(wèn)請(qǐng)求沒(méi)有與之對(duì)應(yīng)的合同,進(jìn)程就會(huì)被處以段錯(cuò)誤(Segmentation Fault)的罰單。 當(dāng)一個(gè)VMA被找到后,內(nèi)核必須處理這個(gè)故障,方式是察看PTE的內(nèi)容以及VMA的類型。在我們的例子中,PTE顯示了該頁(yè)并不存在。事實(shí)上,我們的PTE是完全空白的(全為0),在Linux中意味著虛擬頁(yè)還沒(méi)有被映射。既然這是一個(gè)匿名的VMA,我們面對(duì)的就是一個(gè)純粹的RAM事務(wù),必須由do_anonymous_page()處理,它會(huì)分配一個(gè)頁(yè)幀并生成一個(gè)PTE,將出故障的虛擬頁(yè)映射到那個(gè)剛剛分配的頁(yè)幀上。 事情還可能有些不同。被換出的頁(yè)所對(duì)應(yīng)的PTE,例如,它的Present標(biāo)志是0但并不是空白的。相反,它記錄了頁(yè)面內(nèi)容在交換系統(tǒng)中的位置,這些內(nèi)容必須從磁盤(pán)讀取出來(lái)并通過(guò)do_swap_page()加載到一個(gè)頁(yè)幀當(dāng)中,這就是所謂的major fault。 至此我們走完了'內(nèi)核的用戶內(nèi)存管理'之旅的前半程。在下一篇文章中,我們將把文件的概念也混進(jìn)來(lái),從而建立一個(gè)內(nèi)存基礎(chǔ)知識(shí)的完成畫(huà)面,并了解其對(duì)系統(tǒng)性能的影響。 參考: http://blog.csdn.net/drshenlei/article/details/4350928轉(zhuǎn): 頁(yè)面緩存-內(nèi)存與文件的那些事 原文標(biāo)題:Page Cache, the Affair Between Memory and Files [注:本人水平有限,只好挑一些國(guó)外高手的精彩文章翻譯一下。一來(lái)自己復(fù)習(xí),二來(lái)與大家分享。] 上次我們考察了內(nèi)核如何為一個(gè)用戶進(jìn)程管理虛擬內(nèi)存,但是沒(méi)有涉及文件及I/O。這次我們的討論將涵蓋非常重要且常被誤解的文件與內(nèi)存間關(guān)系的問(wèn)題,以及它對(duì)系統(tǒng)性能的影響。 提到文件,操作系統(tǒng)必須解決兩個(gè)重要的問(wèn)題。首先是硬盤(pán)驅(qū)動(dòng)器的存取速度緩慢得令人頭疼(相對(duì)于內(nèi)存而言),尤其是進(jìn)程瀏覽器翻看Windows進(jìn)程,就會(huì)發(fā)現(xiàn)大約15MB的共享DLL被加載進(jìn)了每一個(gè)進(jìn)程。我目前的Windows系統(tǒng)就運(yùn)行了100個(gè)進(jìn)程,如果沒(méi)有共享機(jī)制,那將消耗大約1.5GB的物理內(nèi)存僅僅用于存放公用DLL。這可不怎么好。同樣的,幾乎所有的Linux程序都需要ld.so和libc,以及其它的公用函數(shù)庫(kù)。 令人愉快的是,這兩個(gè)問(wèn)題可以被一石二鳥(niǎo)的解決:頁(yè)面緩存(page cache),內(nèi)核用它保存與頁(yè)面同等大小的文件數(shù)據(jù)塊。為了展示頁(yè)面緩存,我需要祭出一個(gè)名叫render的Linux程序,它會(huì)打開(kāi)一個(gè)scene.dat文件,每次讀取其中的512字節(jié),并將這些內(nèi)容保存到一個(gè)建立在堆上的內(nèi)存塊中。首次的讀取是這樣的:
在讀取了12KB以后,render的堆以及相關(guān)的頁(yè)幀情況如下:
這看起來(lái)很簡(jiǎn)單,但還有很多事情會(huì)發(fā)生。首先,即使這個(gè)程序只調(diào)用了常規(guī)的read函數(shù),此時(shí)也會(huì)有三個(gè) 4KB的頁(yè)幀存儲(chǔ)在頁(yè)面緩存當(dāng)中,它們持有scene.dat的一部分?jǐn)?shù)據(jù)。盡管有時(shí)這令人驚訝,但的確所有的常規(guī)文件I/O都是通過(guò)頁(yè)面緩存來(lái)進(jìn)行的。在x86 Linux里,內(nèi)核將文件看作是4KB大小的數(shù)據(jù)塊的序列。即使你只從文件讀取一個(gè)字節(jié),包含此字節(jié)的整個(gè)4KB數(shù)據(jù)塊都會(huì)被讀取,并放入到頁(yè)面緩存當(dāng)中。這樣做是有道理的,因?yàn)榇疟P(pán)的持續(xù)性數(shù)據(jù)吞吐量很不錯(cuò),而且一般說(shuō)來(lái),程序?qū)τ谖募心硡^(qū)域的讀取都不止幾個(gè)字節(jié)。頁(yè)面緩存知道每一個(gè)4KB數(shù)據(jù)塊在文件中的對(duì)應(yīng)位置,如上圖所示的#0, #1等等。與Linux的頁(yè)面緩存類似,Windows使用256KB的views。 不幸的是,在一個(gè)普通的文件讀取操作中,內(nèi)核必須復(fù)制頁(yè)面緩存的內(nèi)容到一個(gè)用戶緩沖區(qū)中,這不僅消耗CPU時(shí)間,傷害了CPU cache的性能,還因?yàn)榇鎯?chǔ)了重復(fù)信息而浪費(fèi)物理內(nèi)存。如上面每張圖所示,scene.dat的內(nèi)容被保存了兩遍,而且程序的每個(gè)實(shí)例都會(huì)保存一份。至此,我們緩和了磁盤(pán)延遲的問(wèn)題,但卻在其余的每個(gè)問(wèn)題上慘敗。內(nèi)存映射文件(memory-mapped files)將引領(lǐng)我們走出混亂:
當(dāng)你使用文件映射的時(shí)候,內(nèi)核將你的程序的虛擬內(nèi)存頁(yè)直接映射到頁(yè)面緩存上。這將導(dǎo)致一個(gè)顯著的性能提升:《Windows系統(tǒng)編程》指出常規(guī)的文件讀取操作運(yùn)行時(shí)性能改善30%以上;《Unix環(huán)境高級(jí)編程》指出類似的情況也發(fā)生在Linux和Solaris系統(tǒng)上。你還可能因此而節(jié)省下大量的物理內(nèi)存,這依賴于你的程序的具體情況。 和以前一樣,提到性能,實(shí)際測(cè)量才是王道,但是內(nèi)存映射的確值得被程序員們放入工具箱。相關(guān)的API也很漂亮,它提供了像訪問(wèn)內(nèi)存中的字節(jié)一樣的方式來(lái)訪問(wèn)一個(gè)文件,不需要你多操心,也不犧牲代碼的可讀性?;貞浺幌?a >地址空間、還有那個(gè)在Unix類系統(tǒng)上關(guān)于mmap的實(shí)驗(yàn),Windows下的CreateFileMapping及其在高級(jí)語(yǔ)言中的各種可用封裝。當(dāng)你映射一個(gè)文件時(shí),它的內(nèi)容并不是立刻就被全部放入內(nèi)存的,而是依賴頁(yè)故障(page fault)按需讀取。在獲取了一個(gè)包含所需的文件數(shù)據(jù)的頁(yè)幀后,對(duì)應(yīng)的故障處理函數(shù)會(huì)將你的虛擬內(nèi)存頁(yè)映射到頁(yè)面緩存上。如果所需內(nèi)容不在緩存當(dāng)中,此過(guò)程還將包含磁盤(pán)I/O操作。 現(xiàn)在給你出一個(gè)流行的測(cè)試題。想象一下,在最后一個(gè)render程序的實(shí)例退出之時(shí),那些保存了scene.dat的頁(yè)面緩存會(huì)被立刻清理嗎?人們通常會(huì)這樣認(rèn)為,但這是個(gè)壞主意。如果你仔細(xì)想想,我們經(jīng)常會(huì)在一個(gè)程序中創(chuàng)建一個(gè)文件,退出,緊接著在第二個(gè)程序中使用這個(gè)文件。頁(yè)面緩存必須能處理此類情況。如果你再多想想,內(nèi)核何必總是要舍棄頁(yè)面緩存中的內(nèi)容呢?記住,磁盤(pán)比RAM慢5個(gè)數(shù)量級(jí),因此一個(gè)頁(yè)面緩存的命中(hit)就意味著巨大的勝利。只要還有足夠的空閑物理內(nèi)存,緩存就應(yīng)該盡可能保持滿狀態(tài)。所以它與特定的進(jìn)程并不相關(guān),而是一個(gè)系統(tǒng)級(jí)的資源。如果你一周前運(yùn)行過(guò)render,而此時(shí)scene.dat還在緩存當(dāng)中,那真令人高興。這就是為什么內(nèi)核緩存的大小會(huì)穩(wěn)步增加,直到緩存上限。這并非因?yàn)椴僮飨到y(tǒng)是破爛貨,吞噬你的RAM,事實(shí)上這是種好的行為,反而釋放物理內(nèi)存才是一種浪費(fèi)。緩存要利用得越充分越好。 由于使用了頁(yè)面緩存體系結(jié)構(gòu),當(dāng)一個(gè)程序調(diào)用write()時(shí),相關(guān)的字節(jié)被簡(jiǎn)單的復(fù)制到頁(yè)面緩存中,并且將頁(yè)面標(biāo)記為臟的(dirty)。磁盤(pán)I/O一般不會(huì)立刻發(fā)生,因此你的程序的執(zhí)行不會(huì)被打斷去等待磁盤(pán)設(shè)備。這樣做的缺點(diǎn)是,如果此時(shí)計(jì)算機(jī)死機(jī),那么你寫(xiě)入的數(shù)據(jù)將不會(huì)被記錄下來(lái)。因此重要的文件,比如數(shù)據(jù)庫(kù)事務(wù)記錄必須被fsync() (但是還要小心磁盤(pán)控制器的緩存)。另一方面,讀取操作一般會(huì)打斷你的程序直到準(zhǔn)備好所需的數(shù)據(jù)。內(nèi)核通常采用積極加載(eager loading)的方式來(lái)緩解這個(gè)問(wèn)題。以提前讀取(read ahead)為例,內(nèi)核會(huì)預(yù)先加載一些頁(yè)到頁(yè)面緩存,并期待你的讀取操作。通過(guò)提示系統(tǒng)即將對(duì)文件進(jìn)行的是順序還是隨機(jī)讀取操作(參看madvise(), readahead(), Windows緩存提示),你可以幫助內(nèi)核調(diào)整它的積極加載行為。Linux的確會(huì)對(duì)內(nèi)存映射文件進(jìn)行預(yù)取,但我不太確定Windows是否也如此。最后需要一提的是,你還可以通過(guò)在Linux中使用O_DIRECT或在Windows中使用NO_BUFFERING來(lái)繞過(guò)頁(yè)面緩存,有些數(shù)據(jù)庫(kù)軟件就是這么做的。 一個(gè)文件映射可以是私有的(private)或共享的(shared)。這里的區(qū)別只有在更改(update)內(nèi)存中的內(nèi)容時(shí)才會(huì)顯現(xiàn)出來(lái):在私有映射中,更改并不會(huì)被提交到磁盤(pán)或?qū)ζ渌M(jìn)程可見(jiàn),而這在共享的映射中就會(huì)發(fā)生。內(nèi)核使用寫(xiě)時(shí)拷貝(copy on write)技術(shù),通過(guò)頁(yè)表項(xiàng)(page table entries),實(shí)現(xiàn)私有映射。在下面的例子中,render和另一個(gè)叫render3d的程序(我是不是很有創(chuàng)意?)同時(shí)私有映射了scene.dat。隨后render改寫(xiě)了映射到此文件的虛擬內(nèi)存區(qū)域:
上圖所示的只讀的頁(yè)表項(xiàng)并不意 味著映射是只讀的,它們只是內(nèi)核耍的小把戲,用于共享物理內(nèi)存直到可能的最后一刻。你會(huì)發(fā)現(xiàn)'私有'一詞是多么的不恰當(dāng),你只需記住它只在數(shù)據(jù)發(fā)生更改時(shí) 起作用。此設(shè)計(jì)所帶來(lái)的一個(gè)結(jié)果就是,一個(gè)以私有方式映射文件的虛擬內(nèi)存頁(yè)可以觀察到其他進(jìn)程對(duì)此文件的改動(dòng),只要之前對(duì)這個(gè)內(nèi)存頁(yè)進(jìn)行的都是讀取操作。 一旦發(fā)生過(guò)寫(xiě)時(shí)拷貝,就不會(huì)再觀察到其他進(jìn)程對(duì)此文件的改動(dòng)了。此行為不是內(nèi)核提供的,而是在x86系統(tǒng)上就會(huì)如此。另外,從API的角度來(lái)說(shuō),這也是合理的。與此相反,共享映射只是簡(jiǎn)單的映射到頁(yè)面緩存,僅此而已。對(duì)頁(yè)面的所有更改操作對(duì)其他進(jìn)程都可見(jiàn),而且最終會(huì)執(zhí)行磁盤(pán)操作。最后,如果此共享映射是只讀的,那么頁(yè)故障將觸發(fā)段錯(cuò)誤(segmentation fault)而不是寫(xiě)時(shí)拷貝。 被動(dòng)態(tài)加載的函數(shù)庫(kù)通過(guò)文件映射機(jī)制放入到你的程序的地址空間中。這里沒(méi)有任何特別之處,同樣是采用私有文件映射,跟提供給你調(diào)用的常規(guī)API別無(wú)二致。下面的例子展示了兩個(gè)運(yùn)行中的render程序的一部分地址空間,還有物理內(nèi)存。它將我們之前看到的概念都聯(lián)系在了一起。
至此我們完成了內(nèi)存基礎(chǔ)知識(shí)的三部曲系列。我希望這個(gè)系列對(duì)您有用,并在您頭腦中建立一個(gè)好的操作系統(tǒng)模型。 參考: http://blog.csdn.net/drshenlei/article/details/4582197 |
|
|