What Your Computer Does While You Wait

轉: CPU的等待有多久?html

原文標題:What Your Computer Does While You Wait前端

原文地址:http://duartes.org/gustavo/blog/linux

[注:本人水平有限,只好挑一些國外高手的精彩文章翻譯一下。一來本身複習,二來與你們分享。]程序員

   本文以一個現代的、實際的我的電腦爲對象,分析其中CPU(Intel Core 2 Duo 3.0GHz)以及各種子系統的運行速度——延遲和數據吞吐量。經過粗略的估算PC各個組件的相對運行速度,但願能給你們留下一個比較直觀的印象。本文中的數據來自實際應用,而非理論最大值。時間的單位是納秒(ns,十億分之一秒),毫秒(ms,千分之一秒),和秒(s)。吞吐量的單位是兆字節(MB)和千兆字節(GB)。讓咱們先從CPU和內存開始,下圖是北橋部分:   算法

   第一個使人驚歎的事實是:CPU快得離譜。在Core 2 3.0GHz上,大部分簡單指令的執行只須要一個時鐘週期,也就是1/3納秒。即便是真空中傳播的光,在這段時間內也只能走10釐米(約4英寸)。把上述事實記在心中是有好處的。當你要對程序作優化的時候就會想到,執行指令的開銷對於當今的CPU而言是多麼的微不足道。   數據庫

當CPU運轉起來之後,它便會經過L1 cache和L2 cache對系統中的主存進行讀寫訪問。cache使用的是靜態存儲器(SRAM)。相對於系統主存中使用的動態存儲器(DRAM),cache讀寫速度快得多、造價也高昂得多。cache通常被放置在CPU芯片的內部,加之使用昂貴高速的存儲器,使其給CPU帶來的延遲很是低。在指令層次上的優化(instruction-level optimization),其效果是與優化後代碼的大小息息相關。因爲使用了高速緩存技術(caching),那些可以總體放入L1/L2 cache中的代碼,和那些在運行時須要不斷調入/調出(marshall into/out of)cache的代碼,在性能上會產生很是明顯的差別。編程

   正常狀況下,當CPU操做一塊內存區域時,其中的信息要麼已經保存在L1/L2 cache,要麼就須要將之從系統主存中調入cache,而後再處理。若是是後一種狀況,咱們就碰到了第一個瓶頸,一個大約250個時鐘週期的延遲。在此期間若是CPU沒有其餘事情要作,則每每是處在停機狀態的(stall)。爲了給你們一個直觀的印象,咱們把CPU的一個時鐘週期看做一秒。那麼,從L1 cache讀取信息就好像是拿起桌上的一張草稿紙(3秒);從L2 cache讀取信息則是從身邊的書架上取出一本書(14秒);而從主存中讀取信息則至關於走到辦公樓下去買個零食(4分鐘)。bootstrap

   主存操做的準確延遲是不固定的,與具體的應用以及其餘許多因素有關。好比,它依賴於列選通延遲(CAS)以及內存條的型號,它還依賴於CPU指令預取的成功率。指令預取能夠根據當前執行的代碼來猜想主存中哪些部分即將被使用,從而提早將這些信息載入cache。數組

   看看L1/L2 cache的性能,再對比主存,就會發現:配置更大的cache或者編寫能更好的利用cache的應用程序,會使系統的性能獲得多麼顯著的提升。若是想進一步瞭解有關內存的諸多信息,讀者能夠參閱Ulrich Drepper所寫的一篇經典文章《What Every Programmer Should Know About Memory》。瀏覽器

   人們一般把CPU與內存之間的瓶頸叫作馮·諾依曼瓶頸(von Neumann bottleneck)。當今系統的前端總線帶寬約爲10GB/s,看起來很使人滿意。在這個速度下,你能夠在1秒內從內存中讀取8GB的信息,或者10納秒內讀取100字 節。遺憾的是,這個吞吐量只是理論最大值(圖中其餘數據爲實際值),並且是根本不可能達到的,由於主存控制電路會引入延遲。在作內存訪問時,會遇到不少零 散的等待週期。好比電平協議要求,在選通一行、選通一列、取到可靠的數據以前,須要有必定的信號穩定時間。因爲主存中使用電容來存儲信息,爲了防止因天然放電而致使的信息丟失,就須要週期性的刷新它所存儲的內容,這也帶來額外的等待時間。某些連續的內存訪問方式可能會比較高效,但仍然具備延時。而那些隨機 的內存訪問則消耗更多時間。因此延遲是不可避免的。

 

  

圖中下方的南橋鏈接了不少其餘總線(如:PCI-E, USB)和外圍設備:

  

  

使人沮喪的是,南橋管理了一些反應至關遲鈍的設備,好比硬盤。就算是緩慢的系統主存,和硬盤相比也可謂速度如飛了。繼續拿辦公室作比喻,等待硬盤尋道的時間至關於離開辦公大樓並開始長達一年零三個月的環球旅行。這就解釋了爲什麼電腦的大部分工做都受制於磁盤I/O,以及爲什麼數據庫的性能在內存緩衝區被耗盡後會陡然降低。同時也解釋了爲什麼充足的RAM(用於緩衝)和高速的磁盤驅動器對系統的總體性能如此重要。

  

雖然磁盤的"連續"存取速度確實能夠在實際使用中達到,但這並不是故事的所有。真正使人頭疼的瓶頸在於尋道操做,也就是在磁盤表面移動讀寫磁頭到正確的磁道上,而後再等待磁盤旋轉到正確的位置上,以便讀取指定扇區內的信息。RPM(每分鐘繞轉次數)用來指示磁盤的旋轉速度:RPM越大,耽誤在尋道上的時間就越少,因此越高的RPM意味着越快的磁盤。這裏有一篇由兩個Stanford的研究生寫的很酷的文章,其中講述了尋道時間對系統性能的影響:《Anatomy of a Large-Scale Hypertextual Web Search Engine》

   當 磁盤驅動器讀取一個大的、連續存儲的文件時會達到更高的持續讀取速度,由於省去了尋道的時間。文件系統的碎片整理器就是用來把文件信息重組在連續的數據塊 中,經過儘量減小尋道來提升數據吞吐量。然而,說到計算機實際使用時的感覺,磁盤的連續存取速度就不那麼重要了,反而應該關注驅動器在單位時間內能夠完 成的尋道和隨機I/O操做的次數。對此,固態硬盤能夠成爲一個很棒的選擇。

   硬盤的cache也有助於改進性能。雖然16MB的cache只能覆蓋整個磁盤容量的0.002%,可別看cache只有這麼一點大,其效果十分明顯。它能夠把一組零散的寫入操做合成一個,也就是使磁盤可以控制寫入操做的順序,從而減小尋道的次數。一樣的,爲了提升效率,一系列讀取操做也能夠被重組,並且操做系統和驅動器固件(firmware)都會參與到這類優化中來。

 

  

最後,圖中還列出了網絡和其餘總線的實際數據吞吐量。火線(fireware)僅供參考,Intel X48芯片組並不直接支持火線。咱們能夠把Internet看做是計算機之間的總線。去訪問那些速度很快的網站(好比google.com),延遲大約45毫秒,與硬盤驅動器帶來的延遲至關。事實上,儘管硬盤比內存慢了5個數量級,它的速度與Internet是在同一數量級上的。目前,通常家用網絡的帶寬仍是要落後於硬盤連續讀取速度的,但"網絡就是計算機"這句話可謂名符其實。若是未來Internet比硬盤還快了,那會是個什麼景象呢?

  

我但願這些圖片能對您有所幫助。當這些數字一塊兒呈如今我面前時,真的很迷人,也讓我看到了計算機技術發展到了哪一步。前文分開的兩個圖片只是爲了敘述方便,我把包含南北橋的整張圖片也貼出來,供您參考。

  

參考: http://blog.csdn.net/drshenlei/article/details/4240703

 

轉: CPU如何操做內存

原文標題:Getting Physical With Memory

原文地址:http://duartes.org/gustavo/blog/

   [注:本人水平有限,只好挑一些國外高手的精彩文章翻譯一下。一來本身複習,二來與你們分享。]

   在你試圖理解一個複雜的系統時,若是能揭去表面的抽象並專一於最低級別的概念,每每會有不小的收穫。在這個精神的指導下,讓咱們看看對於內存和I/O端口操做來講最簡單、最基礎的概念,即CPU與總線之間的接口。其中的細節是不少上層概念的基礎,好比線程同步。固然了,既然我是個程序員,就暫且忽略那些只有電子工程師纔會去關注的東西吧。下圖是咱們的老朋友,Core 2:

  

   Core 2 處理器有775個管腳,其中約半數僅僅用於供電而不參與數據傳輸。當你把這些管腳按照功能分類後,就會發現這個處理器的物理接口驚人的簡單。本圖展現了參與內存和I/O端口操做的重要管腳:地址線,數據線,請求線。這些操做均發生在前端總線的事務上下文結構(the context of a transaction)中。前端總線事務的執行包含五個階段:仲裁,請求,偵聽,響應,數據操做。在執行事務的過程當中,前端總線上的各個部件扮演着不一樣的角色。這些部件稱之爲agent。一般,agent就是所有的處理器外加北橋。

  

本文只分析請求階段。在此階段中,發出請求的agent每每是一個處理器,它輸出兩個數據包。下圖列出了第一個數據包中最爲重要的位,這些數據位經過處理器的地址線和請求線輸出:

     

   地址線輸出指定了事務發生的物理內存起始地址。咱們有33條地址線,他們指定了數據包的第35至第3位,第2至第0位爲0。所以,實際上這33條地址線構成了一個36位的、以8字節對齊的地址,正好覆蓋64GB的物理內存。這種設定從奔騰Pro就開始了。請求線指定了事務的類型。當事務類型爲I/O請求時,地址線指出的是I/O端口地址而不是內存地址。當第一個數據包被髮送之後,一樣由這組管腳,在下一個總線時鐘週期發送第二個數據包:

  

   屬性信號(attribute signal A[31:24])頗有趣,它反映了Intel處理器所支持的5種內存緩衝功能。把這些信息發佈到前端總線後,發出請求的agent就可讓其餘處理器知道如何根據當前事務處理他們本身的cache,以及讓內存控制器(也就是北橋)知道該如何應對。一塊指定內存區域的緩存類型由處理器經過查詢頁表(page table)來決定,頁表由OS內核維護。

   典型的狀況是,內核把所有內存都視爲"回寫"類型(write-back),從而得到最好的性能。在回寫模式下,內存的最小訪問單元爲一個緩存線(cache line),在Core 2中是64字節。當程序想讀取內存中的一個字節時,處理器會從L1/L2 cache讀取包含此字節的整條緩存線的內容。當程序作寫入內存操做時,處理器只是修改cache中的對應緩存線,而不會更新主存中的信息。以後,當真的須要更新主存時,處理器會把那個被修改了的緩存線總體放到總線上,一次性寫入內存。因此大部分的請求事務,其數據長度字段都是11(REQ[1:0]),對應64字節。下圖展現了當cache中沒有對應數據時,內存讀取訪問的過程:

 

  

  

在Intel計算機上,有些物理內存範圍被映射爲設備地址而不是實際的RAM存儲器地址,好比硬盤和網卡。這使得驅動程序能夠像讀寫內存那樣,方便的與設備通訊。內核會在頁表中標記出這類內存映射區域爲不可緩存的(uncacheable)。對不可緩存的內存區域的訪問操做會被總線原封不動的按順序執行,其操做與應用程序或驅動程序所發出的請求徹底一致。所以,這時程序能夠精確控制讀寫單個字節、字、或其它長度的信息。這都是經過設置第二個數據包中的字節使能掩碼(byte enable mask A[15:8])來完成的。

  

前面討論的這些基本知識還包含不少關聯的內容。好比:

一、  若是應用程序想要儘量高的運行速度,就應該把會被一塊兒訪問的數據儘可能組織在同一條緩存線中。一旦這條緩存線被載入,以後的讀取操做就會加快不少,再也不須要額外的內存訪問了。

二、  對於回寫式內存訪問,做用於一條緩存線的任何內存操做都必定是原子的(atomic)。這種能力是由處理器的L1 cache提供的,全部數據被同時讀寫,中途不會被其餘處理器或線程打斷。特別的,32位和64位的內存操做,只要不跨越緩存線的邊界,就都是原子操做。

三、  前端總線是被全部的agent所共享的。這些agent在開啓一個事務以前,必須先進行總線使用權的仲裁。並且,每個agent都須要偵聽總線上全部的事務,以便維持cache的一致性。所以,隨着部署更多的、多核的處理器到Intel計算機,總線競爭問題會變得愈來愈嚴重。爲解決這個問題,Core i7將處理器直接鏈接於內存,並以點對點的方式通訊,取代以前的廣播方式,從而減小總線競爭。

  

本 文講述的都是有關物理內存請求的重要內容。當涉及到內存鎖定、多線程、緩存一致性的問題時,總線這個角色又將浮出水面。當我第一次看到前端總線數據包的描 述時,會有種恍然大悟的感受,因此我但願您也能從本文中獲益。下一篇文章,咱們將從底層爬回到上層去,研究一個抽象概念:虛擬內存。

 參考: http://blog.csdn.net/drshenlei/article/details/4243733

 

[轉]: 主板芯片組與內存映射

原文標題:Motherboard Chipsets and the Memory Map

原文地址:http://duartes.org/gustavo/blog/

   [注:本人水平有限,只好挑一些國外高手的精彩文章翻譯一下。一來本身複習,二來與你們分享。]

   我打算寫一組講述計算機內幕的文章,旨在揭示現代操做系統內核的工做原理。我但願這些文章能對電腦愛好者和程序員有所幫助,特別是對這類話題感興趣但沒有相關知識的人們。討論的焦點是Linux,Windows,和Intel處理器。鑽研系統內幕是個人一個愛好。我曾經編寫過很多內核模式的代碼,只是最近一段時間再也不寫了。這第一篇文章講述了現代Intel主板的佈局,CPU如何訪問內存,以及系統的內存映射。

   做爲開始,讓咱們看看當今的Intel計算機是如何鏈接各個組件的吧。下圖展現了主板上的主要組件:

  

現代主板的示意圖,北橋和南橋構成了芯片組。

   當你看圖時,請牢記一個相當重要的事實:CPU一點也不知道它鏈接了什麼東西。CPU僅僅經過一組針腳與外界交互,它並不關心外界到底有什麼。多是一個電腦主板,但也多是烤麪包機,網絡路由器,植入腦內的設備,或CPU測試工做臺。CPU主要經過3種方式與外界交互:內存地址空間,I/O地址空間,還有中斷。

 

  

眼下,咱們只關心主板和內存。安裝在主板上的CPU與外界溝通的門戶是前端總線(front-side bus),前端總線把CPU與北橋鏈接起來。每當CPU須要讀寫內存時,都會使用這條總線。CPU經過一部分管腳來傳輸想要讀寫的物理內存地址,同時另外一些管腳用於發送將被寫入或接收被讀出的數據。一個Intel Core 2 QX6600有33個針腳用於傳輸物理內存地址(能夠表示233個地址位置),64個針腳用於接收/發送數據(因此數據在64位通道中傳輸,也就是8字節的數據塊)。這使得CPU能夠控制64GB的物理內存(233個地址乘以8字節),儘管大多數的芯片組只能支持8GB的RAM。

  

如今到了最難理解的部分。咱們可能曾經認爲內存指的就是RAM,被各式各樣的程序讀寫着。的確,大部分CPU發出的內存請求都被北橋轉送給了RAM管理器,但並不是所有如此。物理內存地址還可能被用於主板上各類設備間的通訊,這種通訊方式叫作內存映射I/O。這類設備包括顯卡,大多數的PCI卡(好比掃描儀或SCSI卡),以及BIOS中的flash存儲器等。

   當北橋接收到一個物理內存訪問請求時,它須要決定把這個請求轉發到哪裏:是發給RAM?抑或是顯卡?具體發給誰是由內存地址映射表來決定的。映射表知道每個物理內存地址區域所對應的設備。絕大部分的地址被映射到了RAM,其他地址由映射表來通知芯片組該由哪一個設備來響應此地址的訪問請求。這些被映射爲設備的內存地址造成了一個經典的空洞,位於PC內存的640KB到1MB之間。當內存地址被保留用於顯卡和PCI設備時,就會造成更大的空洞。這就是爲何32位的操做系統沒法使用所有的4GB RAM。Linux中,/proc/iomem這個文件簡明的列舉了這些空洞的地址範圍。下圖展現了Intel PC低端4GB物理內存地址造成的一個典型的內存映射:

 

  

Intel系統中,低端4GB內存地址空間的佈局。

  

實際的地址和範圍依賴於特定的主板和電腦中接入的設備,可是對於大多數Core 2系統,情形都跟上圖很是接近。全部棕色的區域都被設備地址映射走了。記住,這些在主板總線上使用的都是物理地址。在CPU內部(好比咱們正在編寫和運行的程序),使用的是邏輯地址,必須先由CPU翻譯成物理地址之後,才能發佈到總線上去訪問內存。

  

這個把邏輯地址翻譯成物理地址的規則比較複雜,並且還依賴於當時CPU的運行模式(實模式,32位保護模式,64位保護模式)。無論採用哪一種翻譯機制,CPU的運行模式決定了有多少物理內存能夠被訪問。好比,當CPU工做於32位保護模式時,它只能夠尋址4GB物理地址空間(固然,也有個例外叫作物理地址擴展,但暫且忽略這個技術吧)。因爲頂部的大約1GB物理地址被映射到了主板上的設備,CPU實際可以使用的也就只有大約3GB的RAM(有時甚至更少,我曾用過一臺安裝了Vista的電腦,它只有2.4GB可用)。若是CPU工做於實模式,那麼它將只能尋址1MB的物理地址空間(這是早期的Intel處理器所支持的惟一模式)。若是CPU工做於64位保護模式,則能夠尋址64GB的地址空間(雖然不多有芯片組支持這麼大的RAM)。處於64位保護模式時,CPU就有可能訪問到RAM空間中被主板上的設備映射走了的區域了(即訪問空洞下的RAM)。要達到這種效果,就須要使用比系統中所裝載的RAM地址區域更高的地址。這種技術叫作回收(reclaiming),並且還須要芯片組的配合。

  

這些關於內存的知識將爲下一篇文章作好鋪墊。下次咱們會探討機器的啓動過程:從上電開始,直到boot loader準備跳轉執行操做系統內核爲止。若是你想更深刻的學習這些東西,我強烈推薦Intel手冊。雖然我列出的都是第一手資料,但Intel手冊寫得很好很準確。這是一些資料:

         《Datasheet for Intel G35 Chipset》描述了一個支持Core 2處理器的有表明性的芯片組。這也是本文的主要信息來源。

         《Datasheet for Intel Core 2 Quad-Core Q6000 Sequence》是一個處理器數據手冊。它記載了處理器上每個管腳的做用(當你把管腳按功能分組後,其實並不算多)。很棒的資料,雖然對有些位的描述比較含糊。

         《Intel Software Developer's Manuals》是傑出的文檔。它優美的解釋了體系結構的各個部分,一點也不會讓人感到含糊不清。第一卷和第三卷A部很值得一讀(別被"卷"字嚇倒,每卷都不長,並且您能夠選擇性的閱讀)。

         Pádraig Brady建議我連接到Ulrich Drepper的一篇關於內存的優秀文章。確實是個好東西。我本打算把這個連接放到討論存儲器的文章中的,但此處列出的越多越好啦。

 

參考: http://blog.csdn.net/drshenlei/article/details/4246441

 

轉: 計算機的引導過程

原文標題:How Computers Boot Up

原文地址:http://duartes.org/gustavo/blog/

    [注:本人水平有限,只好挑一些國外高手的精彩文章翻譯一下。一來本身複習,二來與你們分享。] 

   前一篇文章介紹了Intel計算機的主板與內存映射,從而爲本文設定了一個系統引導階段的場景。引導(Booting)是一個複雜的,充滿技巧的,涉及多個階段,又十分有趣的過程。下圖列出了此過程的概要:

 

  

引導過程概要

  

當 你按下計算機的電源鍵後(如今別按!),機器就開始運轉了。一旦主板上電,它就會初始化自身的固件(firmware)——芯片組和其餘零零碎碎的東西 ——並嘗試啓動CPU。若是此時出了什麼問題(好比CPU壞了或根本沒裝),那麼極可能出現的狀況是電腦沒有任何動靜,除了風扇在轉。一些主板會在CPU 故障或缺失時發出鳴音提示,但以個人經驗,此時大多數機器都會處於僵死狀態。一些USB或其餘設備也可能致使機器啓動時僵死。對於那些之前工做正常,忽然 出現這種症狀的電腦,一個可能的解決辦法是拔除全部沒必要要的設備。你也能夠一次只斷開一個設備,從而發現哪一個是罪魁禍首。

  

若是一切正常,CPU就開始運行了。在一個多處理器或多核處理器的系統中,會有一個CPU被動態的指派爲引導處理器(bootstrap processor簡寫BSP),用於執行所有的BIOS和內核初始化代碼。其他的處理器,此時被稱爲應用處理器(application processor簡寫AP),一直保持停機狀態直到內核明確激活他們爲止。雖然Intel CPU經歷了不少年的發展,但他們一直保持着徹底的向後兼容性,因此現代的CPU能夠表現得跟原先1978年的Intel 8086徹底同樣。其實,當CPU上電後,它就是這麼作的。在這個基本的上電過程當中,處理器工做於實模式分頁功能是無效的。此時的系統環境,就像古老的MS-DOS同樣,只有1MB內存能夠尋址,任何代碼均可以讀寫任何地址的內存,這裏沒有保護或特權級的概念。

  

CPU上電後,大部分寄存器的都具備定義良好的初始值,包括指令指針寄存器(EIP),它記錄了下一條即將被CPU執行的指令所在的內存地址。儘管此時的Intel CPU還只能尋址1MB的內存,但憑藉一個奇特的技巧,一個隱藏的基地址(其實就是個偏移量)會與EIP相加,其結果指向第一條將被執行的指令所處的地址0xFFFFFFF0(長16字節,在4GB內存空間的尾部,遠高於1MB)。這個特殊的地址叫作復位向量(reset vector),並且是現代Intel CPU的標準。

  

主板保證在復位向量處的指令是一個跳轉,並且是跳轉到BIOS執行入口點所在的內存映射地址。這個跳轉會順帶清除那個隱藏的、上電時的基地址。感謝芯片組提供的內存映射功能,此時的內存地址存放着CPU初始化所需的真正內容。這些內容所有是從包含有BIOS的閃存映射過來的,而此時的RAM模塊還只有隨機的垃圾數據。下面的圖例列出了相關的內存區域:

  

引導時的重要內存區域

  

隨後,CPU開始執行BIOS的代碼,初始化機器中的一些硬件。以後BIOS開始執行上電自檢過程(POST),檢測計算機中的各類組件。若是找不到一個可用的顯卡,POST就會失敗,致使BIOS進入停機狀態併發出鳴音提示(由於此時沒法在屏幕上輸出提示信息)。若是顯卡正常,那麼電腦看起來就真的運轉起來了:顯示一個製造商定製的商標,開始內存自檢,天使們大聲的吹響號角。另有一些POST失敗的狀況,好比缺乏鍵盤,會致使停機,屏幕上顯示出錯信息。其實POST便是檢測又是初始化,還要枚舉出全部PCI設備的資源——中斷,內存範圍,I/O端口。現代的BIOS會遵循高級配置與電源接口(ACPI)協議,建立一些用於描述設備的數據表,這些表格未來會被操做系統內核用到。

  

POST完畢後,BIOS就準備引導操做系統了,它必須存在於某個地方:硬盤,光驅,軟盤等。BIOS搜索引導設備的實際順序是用戶可定製的。若是找不到合適的引導設備,BIOS會顯示出錯信息並停機,好比"Non-System Disk or Disk Error"沒有系統盤或驅動器故障。一個壞了的硬盤可能致使此症狀。幸運的是,在這篇文章中,BIOS成功的找到了一個能夠正常引導的驅動器。

  

如今,BIOS會讀取硬盤的第一個扇區(0扇區),內含512個字節。這些數據叫作主引導記錄(Master Boot Record簡稱MBR)。通常說來,它包含兩個極其重要的部分:一個是位於MBR開頭的操做系統相關的引導程序,另外一個是緊跟其後的磁盤分區表。BIOS 絲絕不關心這些事情:它只是簡單的加載MBR的內容到內存地址0x7C00處,並跳轉到此處開始執行,無論MBR裏的代碼是什麼。

  

主引導記錄

  

這段在MBR內的特殊代碼多是Windows 引導裝載程序,Linux 引導裝載程序(好比LILO或GRUB),甚至多是病毒。與此不一樣,分區表則是標準化的:它是一個64字節的區塊,包含4個16字節的記錄項,描述磁盤是如何被分割的(因此你能夠在一個磁盤上安裝多個操做系統或擁有多個獨立的卷)。傳統上,Microsoft的MBR代碼會查看分區表,找到一個(惟一的)標記爲活動(active)的分區,加載那個分區的引導扇區(boot sector),並執行其中的代碼。引導扇區是一個分區的第一個扇區,而不是整個磁盤的第一個扇區。若是此時出了什麼問題,你可能會收到以下錯誤信息:"Invalid Partition Table"無效分區表或"Missing Operating System"操做系統缺失。這條信息不是來自BIOS的,而是由從磁盤加載的MBR程序所給出的。所以這些信息依賴於MBR的內容。

  

隨着時間的推移,引導裝載過程已經發展得愈來愈複雜,愈來愈靈活。Linux的引導裝載程序Lilo和GRUB能夠處理不少種類的操做系統,文件系統,以及引導配置信息。他們的MBR代碼再也不須要效仿上述"從活動分區來引導"的方法。可是從功能上講,這個過程大體以下:

  

一、  MBR自己包含有第一階段的引導裝載程序。GRUB稱之爲階段一。

二、  因爲MBR很小,其中的代碼僅僅用於從磁盤加載另外一個含有額外的引導代碼的扇區。此扇區多是某個分區的引導扇區,但也多是一個被硬編碼到MBR中的扇區位置。

三、  MBR配合第2步所加載的代碼去讀取一個文件,其中包含了下一階段所需的引導程序。這在GRUB中是"階段二"引導程序,在Windows Server中是C:/NTLDR。若是第2步失敗了,在Windows中你會收到錯誤信息,好比"NTLDR is missing"NTLDR缺失。階段二的代碼進一步讀取一個引導配置文件(好比在GRUB中是grub.conf,在Windows中是boot.ini)。以後要麼給用戶顯示一些引導選項,要麼直接去引導系統。

四、  此時,引導裝載程序須要啓動操做系統核心。它必須擁有足夠的關於文件系統的信息,以便從引導分區中讀取內核。在Linux中,這意味着讀取一個名字相似"vmlinuz-2.6.22-14-server"的含有內核鏡像的文件,將之加載到內存並跳轉去執行內核引導代碼。在Windows Server 2003中,一部分內核啓動代碼是與內核鏡像自己分離的,事實上是嵌入到了NTLDR當中。在完成一些初始化工做之後,NTDLR從"c:/Windows/System32/ntoskrnl.exe"文件加載內核鏡像,就像GRUB所作的那樣,跳轉到內核的入口點去執行。

  

這裏還有一個複雜的地方值得一提(這也是我說引導富於技巧性的緣由)。當前Linux內核的鏡像就算被壓縮了,在實模式下,也無法塞進640KB的可用RAM裏。個人vanilla Ubuntu內核壓縮後有1.7MB。然而,引導裝載程序必須運行於實模式,以便調用BIOS代碼去讀取磁盤,因此此時內核確定是無法用的。解決之道是使用一種倍受推崇的"虛模式"。它並不是一個真正的處理器運行模式(但願Intel的工程師容許我以此做樂),而是一個特殊技巧。程序不斷的在實模式和保護模式之間切換,以便訪問高於1MB的內存同時還能使用BIOS。若是你閱讀了GRUB的源代碼,你就會發現這些切換處處都是(看看stage2/目錄下的程序,對real_to_prot 和 prot_to_real函數的調用)。在這個棘手的過程結束時,裝載程序終於想方設法的把整個內核都塞到內存裏了,但在這後,處理器仍保持在實模式運行。

  

  

至此,咱們來到了從"引導裝載"跳轉到"早期的內核初始化"的時刻,就像第一張圖中所指示的那樣。在系統作完熱身運動後,內核會展開並讓系統開始運轉。下一篇文章將帶你們一步步深刻Linux內核的初始化過程,讀者還能夠參考Linux Cross reference的資源。我沒辦法對Windows也這麼作,但我會把要點指出來。

參考:

http://blog.csdn.net/drshenlei/article/details/4250306

 

 

 

轉: 內核引導過程

原文標題:The Kernel Boot Process

原文地址:http://duartes.org/gustavo/blog/

   [注:本人水平有限,只好挑一些國外高手的精彩文章翻譯一下。一來本身複習,二來與你們分享。]

   上一篇文章解釋了計算機的引導過程,正好講到引導裝載程序把系統內核鏡像塞進內存,準備跳轉到內核入口點去執行的時刻。做爲引導啓動系列文章的最後一篇,就讓咱們深刻內核,去看看操做系統是怎麼啓動的吧。因爲我習慣以事實爲依據討論問題,因此文中會出現大量的連接引用Linux 內核2.6.25.6版的源代碼(源自Linux Cross Reference)。若是你熟悉C的 語法,這些代碼就會很是容易讀懂;即便你忽略一些細節,仍能大體明白程序都幹了些什麼。最主要的障礙在於對一些代碼的理解須要相關的背景知識,好比機器的 底層特性或何時、爲何它會運行。我但願能儘可能給讀者提供一些背景知識。爲了保持簡潔,許多有趣的東西,好比中斷和內存,文中只能點到爲止了。在本文 的最後列出了Windows的引導過程的要點。

   當Intel x86的引導程序運行到此刻時,處理器處於實模式(能夠尋址1MB的內存),(針對現代的Linux系統)RAM的內容大體以下:

 

引導裝載完成後的RAM內容

   引導裝載程序經過BIOS的磁盤I/O服務,已經把內核鏡像加載到內存當中。這個鏡像只是硬盤中內核文件(好比/boot/vmlinuz-2.6.22-14-server)的一份徹底相同的拷貝。鏡像分爲兩個部分:一個較小的部分,包含實模式的內核代碼,被加載到640KB內存邊界如下;另外一部分是一大塊內核,運行在保護模式,被加載到低端1MB內存地址以上。

   如上圖所示,以後的事情發生在實模式內核的頭部(kernel header)。這段內存區域用於實現引導裝載程序與內核之間的Linux引導協議。 此處的一些數據會被引導裝載程序讀取。這些數據包括一些使人愉快的信息,好比包含內核版本號的可讀字符串,也包括一些關鍵信息,好比實模式內核代碼的大 小。引導裝載程序還會向這個區域寫入數據,好比用戶選中的引導菜單項對應的命令行參數所在的內存地址。以後就到了跳轉到內核入口點的時刻。下圖顯示了內核 初始化代碼的執行順序,包括源代碼的目錄、文件和行號:

 

與體系結構相關的Linux內核初始化過程

  

對於Intel體系結構,內核啓動前期會執行arch/x86/boot/header.S文件中的程序。它是用匯編語言書寫的。通常說來彙編代碼在內核中不多出現,但常見於引導代碼。這個文件的開頭實際上包含了引導扇區代碼。早期的Linux不須要引導裝載程序就能夠工做,這段代碼是從那個時候留傳下來的。現今,若是這個引導扇區被執行,它僅僅給用戶輸出一個"bugger_off_msg"以後就會重啓系統。現代的引導裝載程序會忽略這段遺留代碼。在引導扇區代碼以後,咱們會看到實模式內核頭部(kernel header)最開始的15字節;這兩部分合起來是512字節,正好是Intel硬件平臺上一個典型的磁盤扇區的大小。

   在這512字節以後,偏移量0x200處,咱們會發現Linux內核的第一條指令,也就是實模式內核的入口點。具體的說,它在header.S:110,是一個2字節的跳轉指令,直接寫成了機器碼的形式0x3AEB。你能夠經過對內核鏡像運行hexdump,並查看偏移量0x200處的內容來驗證這一點——這僅僅是一個對神志清醒程度的檢查,以確保這一切並非在作夢。引導裝載程序運行完畢時就會跳轉執行這個位置的指令,進而跳轉到header.S:229執行一個普通的用匯編寫成的子程序,叫作start_of_setup。這個短小的子程序初始化棧空間(stack),把實模式內核的bss段清零(這個區域包含靜態變量,因此用0來初始化它們),以後跳轉執行一段又老又好的C語言程序:arch/x86/boot/main.c:122。

   main()會處理一些登記工做(好比檢測內存佈局),設置顯示模式等。而後它會調用go_to_protected_mode()。然而,在把CPU置於保護模式以前,還有一些工做必須完成。有兩個主要問題:中斷和內存。在實模式中,處理器的中斷向量表老是從內存的0地址開始的,然而在保護模式中,這個中斷向量表的位置是保存在一個叫IDTR的CPU寄存器當中的。與此同時,從邏輯內存地址(在程序中使用)到線性內存地址(一個從0連續編號到內存頂端的數值)的翻譯方法在實模式和保護模式中是不一樣的。保護模式須要一個叫作GDTR的寄存器來存放內存全局描述符表的地址。因此go_to_protected_mode()調用了setup_idt() 和 setup_gdt(),用於裝載臨時的中斷描述符表和全局描述符表。

 

  

如今咱們能夠轉入保護模式啦,這是由另外一段彙編子程序protected_mode_jump來完成的。這個子程序經過設定CPU的CR0寄存器的PE位來使能保護模式。此時,分頁功能還處於關閉狀態;分頁是處理器的一個可選的功能,即便運行於保護模式也並不是必要。真正重要的是,咱們再也不受制於640K的內存邊界,如今能夠尋址高達4GB的RAM了。這個子程序進而調用壓縮狀態內核的32位內核入口點startup_32。startup32會作一些簡單的寄存器初始化工做,並調用一個C語言編寫的函數decompress_kernel(),用於實際的解壓縮工做。

   decompress_kernel()會打印一條你們熟悉的信息"Decompressing Linux…"(正在解壓縮Linux)。解壓縮過程是原地進行的,一旦完成內核鏡像的解壓縮,第一張圖中所示的壓縮內核鏡像就會被覆蓋掉。所以解壓後的內核也是從1MB位置開始的。以後,decompress_kernel()會顯示"done"(完成)和使人振奮的"Booting the kernel"(正在引導內核)。這裏"Booting"的意思是跳轉到整個故事的最後一個入口點,也是保護模式內核的入口點,位於RAM的第二個1MB開始處(偏移量0x100000,此值是由芬蘭Halti山巔之上的神靈授意給Linus的)。在這個神聖的位置含有一個子程序調用,名叫…呃…startup_32。但你會發現這一位是在另外一個目錄中的。

 

  

這位startup_32的第二個化身也是一個彙編子程序,但它包含了32位模式的初始化過程:

一、  它清理了保護模式內核的bss段。(這回是真正的內核了,它會一直運行,直到機器重啓或關機。)

二、  爲內存創建最終的全局描述符表。

三、  創建頁表以即可以開啓分頁功能。

四、  使能分頁功能。

五、  初始化棧空間。

六、  建立最終的中斷描述符表。

七、  最後,跳轉執行一個體繫結構無關的內核啓動函數:start_kernel()。

下圖顯示了引導最後一步的代碼執行流程:

 

與體系結構無關的Linux內核初始化過程

   start_kernel()看起來更像典型的內核代碼,幾乎全用C語言編寫並且與特定機器無關。這個函數調用了一長串的函數,用來初始化各個內核子系統和數據結構,包括調度器(scheduler),內存分區(memory zones),計時器(time keeping)等等。以後,start_kernel()調用rest_init(),此時幾乎全部的東西均可以工做了。rest_init()會建立一個內核線程,並以另外一個函數kernel_init()做爲此線程的入口點。以後,rest_init()會調用schedule()來激活任務調度功能,而後調用cpu_idle()使本身進入睡眠(sleep)狀態,成爲Linux內核中的一個空閒線程(idle thread)。cpu_idle()會在0號進程(process zero)中永遠的運行下去。一旦有什麼事情可作,好比有了一個活動就緒的進程(runnable process),0號進程就會激活CPU去執行這個任務,直到沒有活動就緒的進程後才返回。

   可是,還有一個小麻煩須要處理。咱們跟隨引導過程一路走下來,這個漫長的線程以一個空閒循環(idle loop)做爲結尾。處理器上電執行第一條跳轉指令之後,一路運行,最終會到達此處。從復位向量(reset vector)->BIOS->MBR->引導裝載程序->實模式內核->保護模式內核,跳轉跳轉再跳轉,通過全部這些雜七雜八的步驟,最後來到引導處理器(boot processor)中的空閒循環cpu_idle()。看起來真的很酷。然而,這並不是故事的所有,不然計算機就不會工做。

   在這個時候,前面啓動的那個內核線程已經準備就緒,能夠取代0號進程和它的空閒線程了。事實也是如此,就發生在kernel_init()開始運行的時刻(此函數以前被做爲線程的入口點)。kernel_init()的職責是初始化系統中其他的CPU,這些CPU從引導過程開始到如今,還一直處於停機狀態。以前咱們看過的全部代碼都是在一個單獨的CPU上運行的,它叫作引導處理器(boot processor)。當其餘CPU——稱做應用處理器(application processor)——啓動之後,它們是處於實模式的,必須經過一些初始化步驟才能進入保護模式。大部分的代碼過程都是相同的,你能夠參考startup_32,但對於應用處理器,仍是有些細微的不一樣。最終,kernel_init()會調用init_post(),後者會嘗試啓動一個用戶模式(user-mode)的進程,嘗試的順序爲:/sbin/init,/etc/init,/bin/init,/bin/sh。若是都不行,內核就會報錯。幸運的是init常常就在這些地方的,因而1號進程(PID 1)就開始運行了。它會根據對應的配置文件來決定啓動哪些進程,這可能包括X11 Windows,控制檯登錄程序,網絡後臺程序等。從而結束了引導進程,同時另外一個Linux程序開始在某處運行。至此,讓我祝福您的電腦能夠一直正常運行下去,不出毛病。

   在一樣的體系結構下,Windows的啓動過程與Linux有不少類似之處。它也面臨一樣的問題,也必須完成相似的初始化過程。當引導過程開始後,一個最大的不一樣是,Windows把所有的實模式內核代碼以及一部分初始的保護模式代碼都打包到了引導加載程序(C:/NTLDR)當中。所以,Windows使用的二進制鏡像文件就不同了,內核鏡像中沒有包含兩個部分的代碼。另外,Linux把引導裝載程序與內核徹底分離,在某種程度上自動的造成不一樣的開源項目。下圖顯示了Windows內核主要的啓動過程:

 

Windows內核初始化過程

   天然而然的,Windows用戶模式的啓動就很是不一樣了。沒有/sbin/init程序,而是運行Csrss.exe和Winlogon.exe。Winlogon會啓動Services.exe(它會啓動全部的Windows服務程序)、Lsass.exe和本地安全認證子系統。經典的Windows登錄對話框就是運行在Winlogon的上下文中的。

   本文是引導啓動系列話題的最後一篇。感謝每一位讀者,感謝大家的反饋。我很抱歉,有些內容只能點到爲止;我打算把它們留在其餘文章中深刻討論,並儘可能保持文章的長度適合blog的風格。下次我打算按期的撰寫關於"Software Illustrated"的文章,就像本系列同樣。最後,給你們一些參考資料:

         最好也最重要的資料是實際的內核代碼,Linux或BSD的都成。

         Intel出版的傑出的軟件開發人員手冊,你能夠免費下載到。

 

         《理解Linux內核》是本好書,其中討論了大量的Linux內核代碼。這書也許有點過期有點枯燥,但我仍是將它推薦給那些想要與內核心意相通的人們。《Linux設備驅動程序》讀起來會有趣得多,講的也不錯,可是涉及的內容有些侷限性。最後,網友Patrick Moroney推薦Robert Love所寫的《Linux內核開發》,我曾聽過一些對此書的正面評價,因此仍是值得列出來的。

         對於Windows,目前最好的參考書是《Windows Internals》,做者是David Solomon和Mark Russinovich,後者是Sysinternals的知名專家。這是本特棒的書,寫的很好並且講解全面。主要的缺點是缺乏源代碼的支持。

參考:

http://blog.csdn.net/drshenlei/article/details/4253179

 

 

轉: 內存地址轉換與分段

原文標題:Memory Translation and Segmentation

原文地址:http://duartes.org/gustavo/blog/

   [注:本人水平有限,只好挑一些國外高手的精彩文章翻譯一下。一來本身複習,二來與你們分享。]

   本文是Intel兼容計算機(x86)的內存與保護系列文章的第一篇,延續了啓動引導系列文章的主題,進一步分析操做系統內核的工做流程。與之前同樣,我將引用Linux內核的源代碼,但對Windows只給出示例(抱歉,我忽略了BSD,Mac等系統,但大部分的討論對它們同樣適用)。文中若是有錯誤,請不吝賜教。

 

  

在支持Intel的主板芯片組上,CPU對內存的訪問是經過鏈接着CPU和北橋芯片的前端總線來完成的。在前端總線上傳輸的內存地址都是物理內存地址,編號從0開始一直到可用物理內存的最高端。這些數字被北橋映射到實際的內存條上。物理地址是明確的、最終用在總線上的編號,沒必要轉換,沒必要分頁,也沒有特權級檢查。然而,在CPU內部,程序所使用的是邏輯內存地址,它必須被轉換成物理地址後,才能用於實際內存訪問。從概念上講,地址轉換的過程以下圖所示:

 

x86 CPU開啓分頁功能後的內存地址轉換過程

  

此圖並未指出詳實的轉換方式,它僅僅描述了在CPU的分頁功能開啓的狀況下內存地址的轉換過程。若是CPU關閉了分頁功能,或運行於16位實模式,那麼從分段單元(segmentation unit)輸出的就是最終的物理地址了。當CPU要執行一條引用了內存地址的指令時,轉換過程就開始了。第一步是把邏輯地址轉換成線性地址。可是,爲何不跳過這一步,而讓軟件直接使用線性地址(或物理地址呢?)其理由與:"人類爲什麼要長有闌尾?它的主要做用僅僅是被感染髮炎而已"大體相同。這是進化過程當中產生的奇特構造。要真正理解x86分段功能的設計,咱們就必須回溯到1978年。

  

最初的8086處理器的寄存器是16位的,其指令集大多使用8位或16位的操做數。這使得代碼能夠控制216個字節(或64KB)的內存。然而Intel的工程師們想要讓CPU能夠使用更多的內存,而又不用擴展寄存器和指令的位寬。因而他們引入了段寄存器(segment register),用來告訴CPU一條程序指令將操做哪個64K的內存區塊。一個合理的解決方案是:你先加載段寄存器,至關於說"這兒!我打算操做開始於X處的內存區塊";以後,再用16位的內存地址來表示相對於那個內存區塊(或段)的偏移量。總共有4個段寄存器:一個用於棧(ss),一個用於程序代碼(cs),兩個用於數據(ds,es)。在那個年代,大部分程序的棧、代碼、數據均可以塞進對應的段中,每段64KB長,因此分段功能常常是透明的。

   現今,分段功能依然存在,一直被x86處理器所使用着。每一條會訪問內存的指令都隱式的使用了段寄存器。好比,一條跳轉指令會用到代碼段寄存器(cs),一條壓棧指令(stack push instruction)會使用到堆棧段寄存器(ss)。在大部分狀況下你能夠使用指令明確的改寫段寄存器的值。段寄存器存儲了一個16位的段選擇符(segment selector);它們能夠經由機器指令(好比MOV)被直接加載。惟一的例外是代碼段寄存器(cs),它只能被影響程序執行順序的指令所改變,好比CALL或JMP指令。雖然分段功能一直是開啓的,但其在實模式與保護模式下的運做方式並不相同的。

   在實模式下,好比在引導啓動的初期,段選擇符是一個16位的數值,指示出一個段的開始處的物理內存地址。這個數值必須被以某種方式放大,不然它也會受限於64K當中,分段就沒有意義了。好比,CPU可能會把這個段選擇符看成物理內存地址的高16位(只需將之左移16位,也就是乘以216)。這個簡單的規則使得:能夠按64K的段爲單位,一塊塊的將4GB的內存都尋址到。遺憾的是,Intel作了一個很詭異的設計,讓段選擇符僅僅乘以24(或16),一舉將尋址範圍限制在了1MB,還引入了過分複雜的轉換過程。下述圖例顯示了一條跳轉指令,cs的值是0x1000:

 

 

實模式分段功能

  

實模式的段地址以16個字節爲步長,從0開始編號一直到0xFFFF0(即1MB)。你能夠將一個從0到0xFFFF的16位偏移量(邏輯地址)加在段地址上。在這個規則下,對於同一個內存地址,會有多個段地址/偏移量的組合與之對應,並且物理地址能夠超過1MB的邊界,只要你的段地址足夠高(參見臭名昭著的A20線)。一樣的,在實模式的C語言代碼中,一個遠指針(far pointer)既包含了段選擇符又包含了邏輯地址,用於尋址1MB的內存範圍。真夠"遠"的啊。隨着程序變得愈來愈大,超出了64K的段,分段功能以及它古怪的處理方式,使得x86平臺的軟件開發變得很是複雜。這種設定可能聽起來有些詭異,但它卻把當時的程序員推動了使人崩潰的深淵。

  

在32位保護模式下,段選擇符再也不是一個單純的數值,取而代之的是一個索引編號,用於引用段描述符表中的表項。這個表爲一個簡單的數組,元素長度爲8字節,每一個元素描述一個段。看起來以下:

 

段描述符

  

有三種類型的段:代碼,數據,系統。爲了簡潔明瞭,只有描述符的共有特徵被繪製出來。基地址(base address)是一個32位的線性地址,指向段的開始;段界限(limit)指出這個段有多大。將基地址加到邏輯地址上就造成了線性地址。DPL是描述符的特權級(privilege level),其值從0(最高特權,內核模式)到3(最低特權,用戶模式),用於控制對段的訪問。

  

這些段描述符被保存在兩個表中:全局描述符表(GDT)和局部描述符表(LDT)。電腦中的每個CPU(或一個處理核心)都含有一個叫作gdtr的寄存器,用於保存GDT的首個字節所在的線性內存地址。爲了選出一個段,你必須向段寄存器加載符合如下格式的段選擇符:

 

段選擇符

  

對GDT,TI位爲0;對LDT,TI位爲1;index指出想要表中哪個段描述符(譯註:原文是段選擇符,應該是筆誤)。對於RPL,請求特權級(Requested Privilege Level),之後咱們還會詳細討論。如今,須要好好想一想了。當CPU運行於32位模式時,無論怎樣,寄存器和指令均可以尋址整個線性地址空間,因此根本就不須要再去使用基地址或其餘什麼鬼東西。那爲何不乾脆將基地址設成0,好讓邏輯地址與線性地址一致呢?Intel的文檔將之稱爲"扁平模型"(flat model),並且在現代的x86系統內核中就是這麼作的(特別指出,它們使用的是基本扁平模型)。基本扁平模型(basic flat model)等價於在轉換地址時關閉了分段功能。如此一來多麼美好啊。就讓咱們來看看32位保護模式下執行一個跳轉指令的例子,其中的數值來自一個實際的Linux用戶模式應用程序:

 

保護模式的分段

  

段描述符的內容一旦被訪問,就會被cache(緩存),因此在隨後的訪問中,就再也不須要去實際讀取GDT了,不然會有損性能。每一個段寄存器都有一個隱藏部分用於緩存段選擇符所對應的那個段描述符。若是你想了解更多細節,包括關於LDT的更多信息,請參閱《Intel System Programming Guide》3A卷的第三章。2A和2B卷講述了每個x86指令,同時也指明瞭x86尋址時所使用的各類類型的操做數:16位,16位加段描述符(可被用於實現遠指針),32位,等等。

  

在Linux上,只有3個段描述符在引導啓動過程被使用。他們使用GDT_ENTRY宏來定義並存儲在boot_gdt數組中。其中兩個段是扁平的,可對整個32位空間尋址:一個是代碼段,加載到cs中,一個是數據段,加載到其餘段寄存器中。第三個段是系統段,稱爲任務狀態段(Task State Segment)。在完成引導啓動之後,每個CPU都擁有一份屬於本身的GDT。其中大部份內容是相同的,只有少數表項依賴於正在運行的進程。你能夠從segment.h看到Linux GDT的佈局以及其實際的樣子。這裏有4個主要的GDT表項:2個是扁平的,用於內核模式的代碼和數據,另兩個用於用戶模式。在看這個Linux GDT時,請留意那些用於確保數據與CPU緩存線對齊的填充字節——目的是克服馮·諾依曼瓶頸。最後要說說,那個經典的Unix錯誤信息"Segmentation fault"(分段錯誤)並非由x86風格的段所引發的,而是因爲分頁單元檢測到了非法的內存地址。唉呀,下次再討論這個話題吧。

  

Intel巧妙的繞過了他們原先設計的那個拼拼湊湊的分段方法,而是提供了一種富於彈性的方式來讓咱們選擇是使用段仍是使用扁平模型。因爲很容易將邏輯地址與線性地址合二爲一,因而這成爲了標準,好比如今在64位模式中就強制使用扁平的線性地址空間了。可是即便是在扁平模型中,段對於x86的保護機制也十分重要。保護機制用於抵禦用戶模式進程對系統內核的非法內存訪問,或各個進程之間的非法內存訪問,不然系統將會進入一個狗咬狗的世界!在下一篇文章中,咱們將窺視保護級別以及如何用段來實現這些保護功能。

參考: http://blog.csdn.net/drshenlei/article/details/4261909

 

 

轉: CPU的運行環, 特權級與保護

原文標題:CPU Rings, Privilege, and Protection

原文地址:http://duartes.org/gustavo/blog/

   [注:本人水平有限,只好挑一些國外高手的精彩文章翻譯一下。一來本身複習,二來與你們分享。]

   可能你憑藉直覺就知道應用程序的功能受到了Intel x86計算機的某種限制,有些特定的任務只有操做系統的代碼才能夠完成,可是你知道這究竟是怎麼一回事嗎?在這篇文章裏,咱們會接觸到x86的特權級(privilege level),看看操做系統和CPU是怎麼一塊兒合謀來限制用戶模式的應用程序的。特權級總共有4個,編號從0(最高特權)到3(最低特權)。有3種主要的資源受到保護:內存,I/O端口以及執行特殊機器指令的能力。在任一時刻,x86 CPU都是在一個特定的特權級下運行的,從而決定了代碼能夠作什麼,不能夠作什麼。這些特權級常常被描述爲保護環(protection ring),最內的環對應於最高特權。即便是最新的x86內核也只用到其中的2個特權級:0和3。

 

x86的保護環

   在諸多機器指令中,只有大約15條指令被CPU限制只能在ring 0執行(其他那麼多指令的操做數都受到必定的限制)。這些指令若是被用戶模式的程序所使用,就會顛覆保護機制或引發混亂,因此它們被保留給內核使用。若是企圖在ring 0之外運行這些指令,就會致使一個通常保護錯(general-protection exception),就像一個程序使用了非法的內存地址同樣。相似的,對內存和I/O端口的訪問也受特權級的限制。可是,在咱們分析保護機制以前,先讓咱們看看CPU是怎麼記錄當前特權級的吧,這與前篇文章中提到的段選擇符(segment selector)有關。以下所示:

 

 

數據段和代碼段的段選擇符

  

數據段選擇符的整個內容可由程序直接加載到各個段寄存器當中,好比ss(堆棧段寄存器)和ds(數據段寄存器)。這些內容裏包含了請求特權級(Requested Privilege Level,簡稱RPL)字段,其含義過會兒再說。然而,代碼段寄存器(cs)就比較特別了。首先,它的內容不能由裝載指令(如MOV)直接設置,而只能被那些會改變程序執行順序的指令(如CALL)間接的設置。並且,不像那個能夠被代碼設置的RPL字段,cs擁有一個由CPU本身維護的當前特權級字段(Current Privilege Level,簡稱CPL),這點對咱們來講很是重要。這個代碼段寄存器中的2位寬的CPL字段的值老是等於CPU的當前特權級。Intel的文檔並未明確指出此事實,並且有時在線文檔也對此含糊其辭,但這的確是個硬性規定。在任什麼時候候,無論CPU內部正在發生什麼,只要看一眼cs中的CPL,你就能夠知道此刻的特權級了。

  

記住,CPU特權級並不會對操做系統的用戶形成什麼影響,無論你是根用戶,管理員,訪客仍是通常用戶。全部的用戶代碼都在ring 3上執行,全部的內核代碼都在ring 0上執行,跟是以哪一個OS用戶的身份執行無關。有時一些內核任務能夠被放到用戶模式中執行,好比Windows Vista上的用戶模式驅動程序,可是它們只是替內核執行任務的特殊進程而已,並且每每能夠被直接刪除而不會引發嚴重後果。

  

因爲限制了對內存和I/O端口的訪問,用戶模式代碼在不調用系統內核的狀況下,幾乎不能與外部世界交互。它不能打開文件,發送網絡數據包,向屏幕打印信息或分配內存。用戶模式進程的執行被嚴格限制在一個由ring 0之 神所設定的沙盤之中。這就是爲何從設計上就決定了:一個進程所泄漏的內存會在進程結束後被通通回收,以前打開的文件也會被自動關閉。全部的控制着內存或 打開的文件等的數據結構全都不能被用戶代碼直接使用;一旦進程結束了,這個沙盤就會被內核拆毀。這就是爲何咱們的服務器只要硬件和內核不出毛病,就能夠 連續正常運行600天,甚至一直運行下去。這也解釋了爲何Windows 95/98那麼容易死機:這並不是由於微軟差勁,而是由於系統中的一些重要數據結構,出於兼容的目的被設計成能夠由用戶直接訪問了。這在當時多是一個很好的折中,固然代價也很大。

  

CPU會在兩個關鍵點上保護內存:當一個段選擇符被加載時,以及,當經過線形地址訪問一個內存頁時。所以,保護也反映在內存地址轉換的過程之中,既包括分段又包括分頁。當一個數據段選擇符被加載時,就會發生下述的檢測過程:

 

x86的分段保護

  

由於越高的數值表明越低的特權,上圖中的MAX()用於挑出CPL和RPL中特權最低的一個,並與描述符特權級(descriptor privilege level,簡稱DPL)比較。若是DPL的值大於等於它,那麼這個訪問就得到許可了。RPL背後的設計思想是:容許內核代碼加載特權較低的段。好比,你能夠使用RPL=3的段描述符來確保給定的操做所使用的段能夠在用戶模式中訪問。但堆棧段寄存器是個例外,它要求CPL,RPL和DPL這3個值必須徹底一致,才能夠被加載。

  

事實上,段保護功能幾乎沒什麼用,由於現代的內核使用扁平的地址空間。在那裏,用戶模式的段能夠訪問整個線形地址空間。真正有用的內存保護髮生在分頁單元中,即從線形地址轉化爲物理地址的時候。一個內存頁就是由一個頁表項(page table entry)所描述的字節塊。頁表項包含兩個與保護有關的字段:一個超級用戶標誌(supervisor flag),一個讀寫標誌(read/write flag)。超級用戶標誌是內核所使用的重要的x86內存保護機制。當它開啓時,內存頁就不能被ring 3訪問了。儘管讀寫標誌對於實施特權控制並不像前者那麼重要,但它依然十分有用。當一個進程被加載後,那些存儲了二進制鏡像(即代碼)的內存頁就被標記爲只讀了,從而能夠捕獲一些指針錯誤,好比程序企圖經過此指針來寫這些內存頁。這個標誌還被用於在調用fork建立Unix子進程時,實現寫時拷貝功能(copy on write)。

   最後,咱們須要一種方式來讓CPU切換它的特權級。若是ring 3的程序能夠隨意的將控制轉移到(即跳轉到)內核的任意位置,那麼一個錯誤的跳轉就會輕易的把操做系統毀掉了。但控制的轉移是必須的。這項工做是經過門描述符(gate descriptor)和sysenter指令來完成的。一個門描述符就是一個系統類型的段描述符,分爲了4個子類型:調用門描述符(call-gate descriptor),中斷門描述符(interrupt-gate descriptor),陷阱門描述符(trap-gate descriptor)和任務門描述符(task-gate descriptor)。調用門提供了一個能夠用於一般的CALL和JMP指令的內核入口點,可是因爲調用門用得很少,我就忽略不提了。任務門也不怎麼熱門(在Linux上,它們只在處理內核或硬件問題引發的雙重故障時才被用到)。

   剩下兩個有趣的:中斷門和陷阱門,它們用來處理硬件中斷(如鍵盤,計時器,磁盤)和異常(如缺頁異常,0除數異常)。我將再也不區分中斷和異常,在文中統一用"中斷"一詞表示。這些門描述符被存儲在中斷描述符表(Interrupt Descriptor Table,簡稱IDT)當中。每個中斷都被賦予一個從0到255的編號,叫作中斷向量。處理器把中斷向量做爲IDT表項的索引,用來指出當中斷髮生時使用哪個門描述符來處理中斷。中斷門和陷阱門幾乎是同樣的。下圖給出了它們的格式。以及當中斷髮生時實施特權檢查的過程。我在其中填入了一些Linux內核的典型數值,以便讓事情更加清晰具體。

 

伴隨特權檢查的中斷描述符

   門中的DPL和段選擇符一塊兒控制着訪問,同時,段選擇符結合偏移量(Offset)指出了中斷處理代碼的入口點。內核通常在門描述符中填入內核代碼段的段選擇符。一箇中斷永遠不會將控制從高特權環轉向低特權環。特權級必需要麼保持不變(當內核本身被中斷的時候),或被提高(當用戶模式的代碼被中斷的時候)。不管哪種狀況,做爲結果的CPL必須等於目的代碼段的DPL。若是CPL發生了改變,一個堆棧切換操做就會發生。若是中斷是被程序中的指令所觸發的(好比INT n),還會增長一個額外的檢查:門的DPL必須具備與CPL相同或更低的特權。這就防止了用戶代碼隨意觸發中斷。若是這些檢查失敗,正如你所猜想的,會產生一個通常保護錯(general-protection exception)。全部的Linux中斷處理器都以ring 0特權退出。

   在初始化階段,Linux內核首先在setup_idt()中創建IDT,並忽略所有中斷。以後它使用include/asm-x86/desc.h的函數來填充普通的IDT表項(參見arch/x86/kernel/traps_32.c)。在Linux代碼中,名字中包含"system"字樣的門描述符是能夠從用戶模式中訪問的,並且其設置函數使用DPL 3。"system gate"是Intel的陷阱門,也能夠從用戶模式訪問。除此以外,術語名詞都與本文對得上號。然而,硬件中斷門並非在這裏設置的,而是由適當的驅動程序來完成。

   有三個門能夠被用戶模式訪問:中斷向量3和4分別用於調試和檢查數值運算溢出。剩下的是一個系統門,被設置爲SYSCALL_VECTOR。對於x86體系結構,它等於0x80。它曾被做爲一種機制,用於將進程的控制轉移到內核,進行一個系統調用(system call),而後再跳轉回來。在那個時代,我須要去申請"INT 0x80"這個沒用的牌照 J。從奔騰Pro開始,引入了sysenter指令,今後能夠用這種更快捷的方式來啓動系統調用了。它依賴於CPU上的特殊目的寄存器,這些寄存器存儲着代碼段、入口點及內核系統調用處理器所需的其餘零散信息。在sysenter執行後,CPU再也不進行特權檢查,而是直接進入CPL 0,並將新值加載到與代碼和堆棧有關的寄存器當中(cs,eip,ss和esp)。只有ring 0的代碼enable_sep_cpu()能夠加載sysenter 設置寄存器。

   最後,當須要跳轉回ring 3時,內核發出一個iretsysexit指令,分別用於從中斷和系統調用中返回,從而離開ring 0並恢復CPL=3的用戶代碼的執行。噢!Vim提示我已經接近1,900字了,因此I/O端口的保護只能下次再談了。這樣咱們就結束了x86的運行環與保護之旅。感謝您的耐心閱讀。

參考:

http://blog.csdn.net/drshenlei/article/details/4265101

 

 

 

 

轉: Cache: 一個隱藏並保存數據的場所

原文標題:Cache: a place for concealment and safekeeping

原文地址:http://duartes.org/gustavo/blog/

   [注:本人水平有限,只好挑一些國外高手的精彩文章翻譯一下。一來本身複習,二來與你們分享。]

   本文簡要的展現了現代Intel處理器的CPU cache是如何組織的。有關cache的討論每每缺少具體的實例,使得一些簡單的概念變得撲朔迷離。也許是我可愛的小腦瓜有點遲鈍吧,但無論怎樣,至少下面講述了故事的前一半,即Core 2的 L1 cache是如何被訪問的:

 

L1 cache – 32KB,8路組相聯,64字節緩存線

1.       由索引揀選緩存組(行)

   在cache中的數據是以緩存線(line)爲單位組織的,一條緩存線對應於內存中一個連續的字節塊。這個cache使用了64字節的緩存線。這些線被保存在cache bank中,也叫(way)。每一路都有一個專門的目錄(directory)用來保存一些登記信息。你能夠把每一路連同它的目錄想象成電子表格中的一列,而表的一行構成了cache的一(set)。列中的每個單元(cell)都含有一條緩存線,由與之對應的目錄單元跟蹤管理。圖中的cache有64 組、每組8路,所以有512個含有緩存線的單元,合計32KB的存儲空間。

   在cache眼中,物理內存被分割成了許多4KB大小的物理內存頁(page)。每一頁都含有4KB / 64 bytes == 64條緩存線。在一個4KB的頁中,第0到63字節是第一條緩存線,第64到127字節是第二條緩存線,以此類推。每一頁都重複着這種劃分,因此第0頁第3條緩存線與第1頁第3條緩存線是不一樣的。

   在全相聯緩存(fully associative cache)中,內存中的任意一條緩存線均可以被存儲到任意的緩存單元中。這種存儲方式十分靈活,但也使得要訪問它們時,檢索緩存單元的工做變得複雜、昂貴。因爲L1和L2 cache工做在很強的約束之下,包括功耗,芯片物理空間,存取速度等,因此在多數狀況下,使用全相聯緩存並非一個很好的折中。

   取而代之的是圖中的組相聯緩存(set associative cache)。意思是,內存中一條給定的緩存線只能被保存在一個特定的組(或行)中。因此,任意物理內存頁的第0條緩存線(頁內第0到63字節)必須存儲到第0組,第1條緩存線存儲到第1組,以此類推。每一組有8個單元可用於存儲它所關聯的緩存線(譯註:就是那些須要存儲到這一組的緩存線),從而造成一個8路關聯的組(8-way associative set)。當訪問一個內存地址時,地址的第6到11位(譯註:組索引)指出了在4KB內存頁中緩存線的編號,從而決定了即將使用的緩存組。舉例來講,物理地址0x800010a0的組索引是000010,因此此地址的內容必定是在第2組中緩存的。

   可是還有一個問題,就是要找出一組中哪一個單元包含了想要的信息,若是有的話。這就到了緩存目錄登場的時刻。每個緩存線都被其對應的目錄單元作了標記(tag);這個標記就是一個簡單的內存頁編號,指出緩存線來自於哪一頁。因爲處理器能夠尋址64GB的物理RAM,因此總共有64GB / 4KB == 224個內存頁,須要24位來保存標記。前例中的物理地址0x800010a0對應的頁號爲524,289。下面是故事的後一半:

 

在組中搜索匹配標記

   因爲咱們只須要去查看某一組中的8路,因此查找匹配標記是很是迅速的;事實上,從電學角度講,全部的標記是同時進行比對的,我用箭頭來表示這一點。若是此時正好有一條具備匹配標籤的有效緩存線,咱們就得到一次緩存命中(cache hit)。不然,這個請求就會被轉發的L2 cache,若是還沒匹配上就再轉發給主系統內存。經過應用各類調節尺寸和容量的技術,Intel給CPU配置了較大的L2 cache,但其基本的設計都是相同的。好比,你能夠將原先的緩存增長8路而得到一個64KB的緩存;再將組數增長到4096,每路能夠存儲256KB。通過這兩次修改,就獲得了一個4MB的L2 cache。在此狀況下,須要18位來保存標記,12位保存組索引;緩存所使用的物理內存頁的大小與其一路的大小相等。(譯註:有4096組,就須要lg(4096)==12位的組索引,緩存線依然是64字節,因此一路有4096*64B==256KB字節;在L2 cache眼中,內存被分割爲許多256KB的塊,因此須要lg(64GB/256KB)==18位來保存標記。)

   若是有一組已經被放滿了,那麼在另外一條緩存線被存儲進來以前,已有的某一條則必須被騰空(evict)。爲了不這種狀況,對運算速度要求較高的程序就要嘗試仔細組織它的數據,使得內存訪問均勻的分佈在已有的緩存線上。舉例來講,假設程序中有一個數組,元素的大小是512字節,其中一些對象在內存中相距4KB。這些對象的各個字段都落在同一緩存線上,並競爭同一緩存組。若是程序頻繁的訪問一個給定的字段(好比,經過虛函數表vtable調用虛函數),那麼這個組看起來就好像一直是被填滿的,緩存開始變得毫無心義,由於緩存線一直在重複着騰空與從新載入的步驟。在咱們的例子中,因爲組數的限制,L1 cache僅能保存8個這類對象的虛函數表。這就是組相聯策略的折中所付出的代價:即便在總體緩存的使用率並不高的狀況下,因爲組衝突,咱們仍是會遇到緩存缺失的狀況。然而,鑑於計算機中各個存儲層次的相對速度,無論怎麼說,大部分的應用程序並沒必要爲此而擔憂。

 

  

一個內存訪問常常由一個線性(或虛擬)地址發起,因此L1 cache須要依賴分頁單元(paging unit)來求出物理內存頁的地址,以便用於緩存標記。與此相反,組索引來自於線性地址的低位,因此不須要轉換就能夠使用了(在咱們的例子中爲第6到11位)。所以L1 cache是物理標記但虛擬索引的(physically tagged but virtually indexed),從而幫助CPU進行並行的查找操做。由於L1 cache的一路毫不會比MMU的一頁還大,因此能夠保證一個給定的物理地址位置老是關聯到同一組,即便組索引是虛擬的。在另外一方面L2 cache必須是物理標記和物理索引的,由於它的一路比MMU的一頁要大。可是,當一個請求到達L2 cache時,物理地址已經被L1 cache準備(resolved)完畢了,因此L2 cache會工做得很好。

  

最後,目錄單元還存儲了對應緩存線的狀態(state)。在L1代碼緩存中的一條緩存線要麼是無效的(invalid)要麼是共享的(shared,意思是有效的,真的J)。在L1數據緩存和L2緩存中,一條緩存線能夠爲4個MESI狀態之一:被修改的(modified),獨佔的(exclusive),共享的(shared),無效的(invalid)。Intel緩存是包容式的(inclusive):L1緩存的內容會被複制到L2緩存中。在下一篇討論線程(threading),鎖定(locking)等內容的文章中,這些緩存線狀態將發揮做用。下一次,咱們將看看前端總線以及內存訪問到底是怎麼工做的。這將成爲一個內存研討周。

  

(在回覆中Dave提到了直接映射緩存(direct-mapped cache)。它們基本上是一種特殊的組相聯緩存,只是只有一路而已。在各類折中方案中,它與全相聯緩存正好相反:訪問很是快捷,但因組衝突而致使的緩存缺失也很是多。)

  

[譯者小結:

  

1.         內存層次結構的意義在於利用引用的空間局部性和時間局部性原理,將常常被訪問的數據放到快速的存儲器中,而將不常常訪問的數據留在較慢的存儲器中。

2.         通常狀況下,除了寄存器和L1緩存能夠操做指定字長的數據,下層的內存子系統就不會再使用這麼小的單位了,而是直接移動數據塊,好比以緩存線爲單位訪問數據。

3.         對於組衝突,能夠這麼理解:與上文類似,假設一個緩存,由512條緩存線組成,每條線64字節,容量32KB。

a)         假如它是直接映射緩存,因爲它每每使用地址的低位直接映射緩存線編號,因此全部的32K倍數的地址(32K,64K,96K等)都會映射到同一條線上(即第0線)。假如程序的內存組織不當,交替的去訪問佈置在這些地址的數據,則會致使衝突。從外表看來就好像緩存只有1條線了,儘管其餘緩存線一直是空閒着的。

b)        若是是全相聯緩存,那麼每條緩存線都是獨立的,能夠對應於內存中的任意緩存線。只有當全部的512條緩存線都被佔滿後纔會出現衝突。

c)        組相聯是前二者的折中,每一路中的緩存線採用直接映射方式,而在路與路之間,緩存控制器使用全相聯映射算法,決定選擇一組中的哪一條線。

d)        若是是2路組相聯緩存,那麼這512條緩存線就被分爲了2路,每路256條線,一路16KB。此時全部爲16K整數倍的地址(16K,32K,48K等)都會映射到第0線,但因爲2路是關聯的,因此能夠同時有2個這種地址的內容被緩存,不會發生衝突。固然了,若是要訪問第三個這種地址,仍是要先騰空已有的一條才行。因此極端狀況下,從外表看來就好像緩存只有2條線了,儘管其餘緩存線一直是空閒着的。

e)         若是是8路組相聯緩存(與文中示例相同),那麼這512條緩存線就被分爲了8路,每路64條線,一路4KB。因此若是數組中元素地址是4K對齊的,而且程序交替的訪問這些元素,就會出現組衝突。從外表看來就好像緩存只有8條線了,儘管其餘緩存線一直是空閒着的。

]

參考: http://blog.csdn.net/drshenlei/article/details/4277959

 

轉: 剖析程序的內存佈局

原文標題:Anatomy of a Program in Memory

原文地址:http://duartes.org/gustavo/blog/

   [注:本人水平有限,只好挑一些國外高手的精彩文章翻譯一下。一來本身複習,二來與你們分享。]

       內存管理模塊是操做系統的心臟;它對應用程序和系統管理很是重要。從此的幾篇文章中,我將着眼於實際的內存問題,但也不避諱其中的技術內幕。因爲很多概念是通用的,因此文中大部分例子取自32位x86平臺的Linux和Windows系統。本系列第一篇文章講述應用程序的內存佈局。

       在多任務操做系統中的每個進程都運行在一個屬於它本身的內存沙盤中。這個沙盤就是虛擬地址空間(virtual address space),在32位模式下它老是一個4GB的內存地址塊。這些虛擬地址經過頁表(page table)映射到物理內存,頁表由操做系統維護並被處理器引用。每個進程擁有一套屬於它本身的頁表,可是還有一個隱情。只要虛擬地址被使能,那麼它就會做用於這臺機器上運行的全部軟件,包括內核自己。所以一部分虛擬地址必須保留給內核使用:

  

 

  

    這並不意味着內核使用了那麼多的物理內存,僅表示它可支配這麼大的地址空間,可根據內核須要,將其映射到物理內存。內核空間在頁表中擁有較高的特權級(ring 2或如下),所以只要用戶態的程序試圖訪問這些頁,就會致使一個頁錯誤(page fault)。在Linux中,內核空間是持續存在的,而且在全部進程中都映射到一樣的物理內存。內核代碼和數據老是可尋址的,隨時準備處理中斷和系統調用。與此相反,用戶模式地址空間的映射隨進程切換的發生而不斷變化:

 

 

  

    藍色區域表示映射到物理內存的虛擬地址,而白色區域表示未映射的部分。在上面的例子中,Firefox使用了至關多的虛擬地址空間,由於它是傳說中的吃內存大戶。地址空間中的各個條帶對應於不一樣的內存段(memory segment),如:堆、棧之類的。記住,這些段只是簡單的內存地址範圍,與Intel處理器的段沒有關係。無論怎樣,下面是一個Linux進程的標準的內存段佈局:

  

  

    當計算機開心、安全、可愛、正常的運轉時,幾乎每個進程的各個段的起始虛擬地址都與上圖徹底一致,這也給遠程發掘程序安全漏洞打開了方便之門。一個發掘過程每每須要引用絕對內存地址:棧地址,庫函數地址等。遠程攻擊者必須依賴地址空間佈局的一致性,摸索着選擇這些地址。若是讓他們猜個正着,有人就會被整了。所以,地址空間的隨機排布方式逐漸流行起來。Linux經過對內存映射段的起始地址加上隨機的偏移量來打亂佈局。不幸的是,32位地址空間至關緊湊,給隨機化所留下的空當不大,削弱了這種技巧的效果

  

    進程地址空間中最頂部的段是棧,大多數編程語言將之用於存儲局部變量和函數參數。調用一個方法或函數會將一個新的棧楨(stack frame)壓入棧中。棧楨在函數返回時被清理。也許是由於數據嚴格的聽從LIFO的順序,這個簡單的設計意味着沒必要使用複雜的數據結構來追蹤棧的內容,只須要一個簡單的指針指向棧的頂端便可。所以壓棧(pushing)和退棧(popping)過程很是迅速、準確。另外,持續的重用棧空間有助於使活躍的棧內存保持在CPU緩存中,從而加速訪問。進程中的每個線程都有屬於本身的棧。

       經過不斷向棧中壓入的數據,超出其容量就有會耗盡棧所對應的內存區域。這將觸發一個頁故障(page fault),並被Linux的expand_stack()處理,它會調用acct_stack_growth()來檢查是否還有合適的地方用於棧的增加。若是棧的大小低於RLIMIT_STACK(一般是8MB),那麼通常狀況下棧會被加長,程序繼續愉快的運行,感受不到發生了什麼事情。這是一種將棧擴展至所需大小的常規機制。然而,若是達到了最大的棧空間大小,就會棧溢出(stack overflow),程序收到一個段錯誤(Segmentation Fault)。當映射了的棧區域擴展到所需的大小後,它就不會再收縮回去,即便棧不那麼滿了。這就比如聯邦預算,它老是在增加的。

       動態棧增加是惟一一種訪問未映射內存區域(圖中白色區域)而被容許的情形。其它任何對未映射內存區域的訪問都會觸發頁故障,從而致使段錯誤。一些被映射的區域是隻讀的,所以企圖寫這些區域也會致使段錯誤。

       在棧的下方,是咱們的內存映射段。此處,內核將文件的內容直接映射到內存。任何應用程序均可以經過Linux的mmap()系統調用(實現)或Windows的CreateFileMapping() / MapViewOfFile()請求這種映射。內存映射是一種方便高效的文件I/O方式,因此它被用於加載動態庫。建立一個不對應於任何文件的匿名內存映射也是可能的,此方法用於存放程序的數據。在Linux中,若是你經過malloc()請求一大塊內存,C運行庫將會建立這樣一個匿名映射而不是使用堆內存。'大塊'意味着比MMAP_THRESHOLD還大,缺省是128KB,能夠經過mallopt()調整。

       說到堆,它是接下來的一塊地址空間。與棧同樣,堆用於運行時內存分配;但不一樣點是,堆用於存儲那些生存期與函數調用無關的數據。大部分語言都提供了堆管理功能。所以,知足內存請求就成了語言運行時庫及內核共同的任務。在C語言中,堆分配的接口是malloc()系列函數,而在具備垃圾收集功能的語言(如C#)中,此接口是new關鍵字。

       若是堆中有足夠的空間來知足內存請求,它就能夠被語言運行時庫處理而不須要內核參與。不然,堆會被擴大,經過brk()系統調用(實現)來分配請求所需的內存塊。堆管理是很複雜的,須要精細的算法,應付咱們程序中雜亂的分配模式,優化速度和內存使用效率。處理一個堆請求所需的時間會大幅度的變更。實時系統經過特殊目的分配器來解決這個問題。堆也可能會變得零零碎碎,以下圖所示:

 

  

  

    最後,咱們來看看最底部的內存段:BSS,數據段,代碼段。在C語言中,BSS和數據段保存的都是靜態(全局)變量的內容。區別在於BSS保存的是未被初始化的靜態變量內容,它們的值不是直接在程序的源代碼中設定的。BSS內存區域是匿名的:它不映射到任何文件。若是你寫static int cntActiveUsers,則cntActiveUsers的內容就會保存在BSS中。

  

    另外一方面,數據段保存在源代碼中已經初始化了的靜態變量內容。這個內存區域不是匿名的。它映射了一部分的程序二進制鏡像,也就是源代碼中指定了初始值的靜態變量。因此,若是你寫static int cntWorkerBees = 10,則cntWorkerBees的內容就保存在數據段中了,並且初始值爲10。儘管數據段映射了一個文件,但它是一個私有內存映射,這意味着更改此處的內存不會影響到被映射的文件。也必須如此,不然給全局變量賦值將會改動你硬盤上的二進制鏡像,這是不可想象的。

  

    下圖中數據段的例子更加複雜,由於它用了一個指針。在此狀況下,指針gonzo(4字節內存地址)自己的值保 存在數據段中。而它所指向的實際字符串則不在這裏。這個字符串保存在代碼段中,代碼段是隻讀的,保存了你所有的代碼外加零零碎碎的東西,好比字符串字面 值。代碼段將你的二進制文件也映射到了內存中,但對此區域的寫操做都會使你的程序收到段錯誤。這有助於防範指針錯誤,雖然不像在C語言編程時就注意防範來得那麼有效。下圖展現了這些段以及咱們例子中的變量:

  

  

    你能夠經過閱讀文件/proc/pid_of_process/maps來檢驗一個Linux進程中的內存區域。記住一個段可能包含許多區域。好比,每一個內存映射文件在mmap段中都有屬於本身的區域,動態庫擁有相似BSS和數據段的額外區域。下一篇文章講說明這些"區域"(area)的真正含義。有時人們提到"數據段",指的就是所有的數據段 + BSS +堆。

  

    你能夠經過nm和objdump命令來察看二進制鏡像,打印其中的符號,它們的地址,段等信息。最後須要指出的是,前文描述的虛擬地址佈局在Linux中是一種"靈活佈局"(flexible layout),並且以此做爲默認方式已經有些年頭了。它假設咱們有值RLIMIT_STACK。當狀況不是這樣時,Linux退回使用"經典佈局"(classic layout),以下圖所示:

  

       對虛擬地址空間的佈局就講這些吧。下一篇文章將討論內核是如何跟蹤這些內存區域的。咱們會分析內存映射,看看文件的讀寫操做是如何與之關聯的,以及內存使用概況的含義。

參考:

http://blog.csdn.net/drshenlei/article/details/4339110

 

 

 

 

轉: 內核是如何管理內存的

原文標題:How The Kernel Manages Your Memory

原文地址:http://duartes.org/gustavo/blog/

   [注:本人水平有限,只好挑一些國外高手的精彩文章翻譯一下。一來本身複習,二來與你們分享。]

       在仔細審視了進程的虛擬地址佈局以後,讓咱們把目光轉向內核以及其管理用戶內存的機制。再次從gonzo圖示開始:

 

  

  

  

    Linux進程在內核中是由task_struct的實例來表示的,即進程描述符。task_struct的mm字段指向內存描述符(memory descriptor),即mm_struct,一個程序的內存的執行期摘要。它存儲了上圖所示的內存段的起止位置,進程所使用的物理內存頁的數量rss表示Resident Set Size),虛擬內存空間的使用量,以及其餘信息。咱們還能夠在內存描述符中找到用於管理程序內存的兩個重要結構:虛擬內存區域集合(the set of virtual memory areas)及頁表(page table)。Gonzo的內存區域以下圖所示:

  

  

    每個虛擬內存區域(簡稱VMA)是一個連續的虛擬地址範圍;這些區域不會交疊。一個vm_area_struct的實例完備的描述了一個內存區域,包括它的起止地址,決定訪問權限和行爲的標誌位,還有vm_file字段,用於指出被映射的文件(若是有的話)。一個VMA若是沒有映射到文件,則是匿名的(anonymous)。除memory mapping 段之外,上圖中的每個內存段(如:堆,棧)都對應於一個單獨的VMA。這並非強制要求,但在x86機器上常常如此。VMA並不關心它在哪個段。

       一個程序的VMA同時以兩種形式存儲在它的內存描述符中:一個是按起始虛擬地址排列的鏈表,保存在mmap字段;另外一個是紅黑樹,根節點保存在mm_rb字段。紅黑樹使得內核能夠快速的查找出給定虛擬地址所屬的內存區域。當你讀取文件/proc/pid_of_process/maps時,內核只須簡單的遍歷指定進程的VMA鏈表,並打印出每一項來便可。

 

  

    在Windows中,EPROCESS塊能夠粗略的當作是task_struct和mm_struct的組合。VMA在Windows中的對應物時虛擬地址描述符(Virtual Address Descriptor),或簡稱VAD;它們保存在平衡樹中(AVL tree)。你知道Windows和Linux最有趣的地方是什麼嗎?就是這些細小的不一樣點。

           4GB虛擬地址空間被分割爲許多(page)。x86處理器在32位模式下所支持的頁面大小爲4KB,2MB和4MB。Linux和Windows都使用4KB大小的頁面來映射用戶部分的虛擬地址空間。第0-4095字節在第0頁,第4096-8191字節在第1頁,以此類推。VMA的大小必須是頁面大小的整數倍。下圖是以4KB分頁的3GB用戶空間:

  

       處理器會依照頁表(page table)來將虛擬地址轉換到物理內存地址。每一個進程都有屬於本身的一套頁表;一旦進程發生了切換,用戶空間的頁表也會隨之切換。Linux在內存描述符的pgd字段保存了一個指向進程頁表的指針。每個虛擬內存頁在頁表中都有一個與之對應的頁表項(page table entry),簡稱PTE。它在普通的x86分頁機制下,是一個簡單的4字節記錄,以下圖所示:

  

        Linux有一些函數能夠用於讀取設置PTE中的每個標誌。P位告訴處理器虛擬頁面是否存在於(present)物理內存中。若是是0,訪問這個頁將觸發頁故障(page fault)。記住,當這個位是0時,內核能夠根據喜愛,隨意的使用其他的字段。R/W標誌表示讀/寫;若是是0,頁面就是隻讀的。U/S標誌表示用戶/管理員;若是是0,則這個頁面只能被內核訪問。這些標誌用於實現只讀內存和保護內核空間。

 

  

        D位和A位表示數據髒(dirty)和訪問過(accessed)。髒表示頁面被執行過寫操做,訪問過表示頁面被讀或被寫過。這兩個標誌都是粘滯的:處理器只會將它們置位,以後必須由內核來清除。最後,PTE還保存了對應該頁的起始物理內存地址,對齊於4KB邊界。PTE中的其餘字段咱們改日再談,好比物理地址擴展(Physical Address Extension)。

  

    虛擬頁面是內存保護的最小單元,由於頁內的全部字節都共享U/S和R/W標誌。然而,一樣的物理內存能夠被映射到不一樣的頁面,甚至能夠擁有不一樣的保護標誌。值得注意的是,在PTE中沒有對執行許可(execute permission)的設定。這就是爲何經典的x86分頁能夠執行位於stack上的代碼,從而爲黑客利用堆棧溢出提供了便利(使用return-to-libc和其餘技術,甚至能夠利用不可執行的堆棧)。PTE缺乏不可執行(no-execute)標誌引出了一個影響更普遍的事實:VMA中的各類許可標誌可能會也可能不會被明確的轉換爲硬件保護。對此,內核能夠盡力而爲,但始終受到架構的限制。

       虛擬內存並不存儲任何東西,它只是將程序地址空間映射到底層的物理內存上,後者被處理器視爲一整塊來訪問,稱做物理地址空間(physical address space)。對物理內存的操做還與總線有點聯繫,好在咱們能夠暫且忽略這些並假設物理地址範圍以字節爲單位遞增,從0到最大可用內存數。這個物理地址空間被內核分割爲一個個頁幀(page frame)。處理器並不知道也不關心這些幀,然而它們對內核相當重要,由於頁幀是物理內存管理的最小單元。Linux和Windows在32位模式下,都使用4KB大小的頁幀;以一個擁有2GB RAM的機器爲例:

 

  

  

  

    在Linux中,每個頁幀都由一個描述符一些標誌所跟蹤。這些描述符合在一塊兒,記錄了計算機內的所有物理內存;能夠隨時知道每個頁幀的準確狀態。物理內存是用buddy memory allocation技術來管理的,所以若是一個頁幀可被buddy 系統分配,則它就是可用的(free)。一個被分配了的頁幀多是匿名的(anonymous),保存着程序數據;也多是頁緩衝的(page cache),保存着一個文件或塊設備的數據。還有其餘一些古怪的頁幀使用形式,但如今先沒必要考慮它們。Windows使用一個相似的頁幀編號(Page Frame Number簡稱PFN)數據庫來跟蹤物理內存。

       讓咱們把虛擬地址區域,頁表項,頁幀放到一塊兒,看看它們究竟是怎麼工做的。下圖是一個用戶堆的例子:

  

  

       藍色矩形表示VMA範圍內的頁,箭頭表示頁表項將頁映射到頁幀上。一些虛擬頁並無箭頭;這意味着它們對應的PTE的存在位(Present flag)爲0。造成這種狀況的緣由多是這些頁尚未被訪問過,或者它們的內容被系統換出了(swap out)。不管那種狀況,對這些頁的訪問都會致使頁故障(page fault),即便它們處在VMA以內。VMA和頁表的不一致看起來使人奇怪,但實際常常如此。

       一個VMA就像是你的程序和內核之間的契約。你請求去作一些事情(如:內存分配,文件映射等),內核說"行",並建立或更新適當的VMA。但它並不是馬上就去完成請求,而是一直等到出現了頁故障纔會真正去作。內核就是一個懶惰,騙人的敗類;這是虛擬內存管理的基本原則。它對大多數狀況都適用,有些比較熟悉,有些使人驚訝,但這個規則就是這樣:VMA記錄了雙方商定作什麼,而PTE反映出懶惰的內核實際作了什麼。這兩個數據結構共同管理程序的內存;都扮演着解決頁故障,釋放內存,換出內存(swapping memory out)等等角色。讓咱們看一個簡單的內存分配的例子:

  

  

       當程序經過brk()系統調用請求更多的內存時,內核只是簡單的更新堆的VMA,而後說搞好啦。其實此時並無頁幀被分配,新的頁也並無出現於物理內存中。一旦程序試圖訪問這些頁,處理器就會報告頁故障,並調用do_page_fault()。它會經過調用find_vma()去搜索哪個VMA含蓋了產生故障的虛擬地址。若是找到了,還會根據VMA上的訪問許可來比對檢查訪問請求(讀或寫)。若是沒有合適的VMA,也就是說內存訪問請求沒有與之對應的合同,進程就會被處以段錯誤(Segmentation Fault)的罰單。

 

  

    當一個VMA被找到後,內核必須處理這個故障,方式是察看PTE的內容以及VMA的類型。在咱們的例子中,PTE顯示了該頁並不存在。事實上,咱們的PTE是徹底空白的(全爲0),在Linux中意味着虛擬頁尚未被映射。既然這是一個匿名的VMA,咱們面對的就是一個純粹的RAM事務,必須由do_anonymous_page()處理,它會分配一個頁幀並生成一個PTE,將出故障的虛擬頁映射到那個剛剛分配的頁幀上。

       事情還可能有些不一樣。被換出的頁所對應的PTE,例如,它的Present標誌是0但並非空白的。相反,它記錄了頁面內容在交換系統中的位置,這些內容必須從磁盤讀取出來並經過do_swap_page()加載到一個頁幀當中,這就是所謂的major fault。

       至此咱們走完了"內核的用戶內存管理"之旅的前半程。在下一篇文章中,咱們將把文件的概念也混進來,從而創建一個內存基礎知識的完成畫面,並瞭解其對系統性能的影響。

參考:

http://blog.csdn.net/drshenlei/article/details/4350928

 

 

 

轉: 頁面緩存-內存與文件的那些事

原文標題:Page Cache, the Affair Between Memory and Files

原文地址:http://duartes.org/gustavo/blog/

   [注:本人水平有限,只好挑一些國外高手的精彩文章翻譯一下。一來本身複習,二來與你們分享。]

   上次咱們考察了內核如何爲一個用戶進程管理虛擬內存,可是沒有涉及文件及I/O。此次咱們的討論將涵蓋很是重要且常被誤解的文件與內存間關係的問題,以及它對系統性能的影響。

 

  

提到文件,操做系統必須解決兩個重要的問題。首先是硬盤驅動器的存取速度緩慢得使人頭疼(相對於內存而言),尤爲是磁盤的尋道性能。第二個是要知足'一次性加載文件內容到物理內存並在程序間共享'的需求。若是你使用進程瀏覽器翻看Windows進程,就會發現大約15MB的共享DLL被加載進了每個進程。我目前的Windows系統就運行了100個進程,若是沒有共享機制,那將消耗大約1.5GB的物理內存僅僅用於存放公用DLL。這可不怎麼好。一樣的,幾乎全部的Linux程序都須要ld.so和libc,以及其它的公用函數庫。

  

使人愉快的是,這兩個問題能夠被一石二鳥的解決:頁面緩存(page cache),內核用它保存與頁面同等大小的文件數據塊。爲了展現頁面緩存,我須要祭出一個名叫render的Linux程序,它會打開一個scene.dat文件,每次讀取其中的512字節,並將這些內容保存到一個創建在堆上的內存塊中。首次的讀取是這樣的:

  

在讀取了12KB之後,render的堆以及相關的頁幀狀況以下:

  

這看起來很簡單,但還有不少事情會發生。首先,即便這個程序只調用了常規的read函數,此時也會有三個 4KB的頁幀存儲在頁面緩存當中,它們持有scene.dat的一部分數據。儘管有時這使人驚訝,但的確全部的常規文件I/O都是經過頁面緩存來進行的。在x86 Linux裏,內核將文件看做是4KB大小的數據塊的序列。即便你只從文件讀取一個字節,包含此字節的整個4KB數據塊都會被讀取,並放入到頁面緩存當中。這樣作是有道理的,由於磁盤的持續性數據吞吐量很不錯,並且通常說來,程序對於文件中某區域的讀取都不止幾個字節。頁面緩存知道每個4KB數據塊在文件中的對應位置,如上圖所示的#0, #1等等。與Linux的頁面緩存相似,Windows使用256KB的views。

  

不幸的是,在一個普通的文件讀取操做中,內核必須複製頁面緩存的內容到一個用戶緩衝區中,這不只消耗CPU時間,傷害了CPU cache的性能,還由於存儲了重複信息而浪費物理內存。如上面每張圖所示,scene.dat的內容被保存了兩遍,並且程序的每一個實例都會保存一份。至此,咱們緩和了磁盤延遲的問題,但卻在其他的每一個問題上慘敗。內存映射文件(memory-mapped files)將引領咱們走出混亂:

  

當你使用文件映射的時候,內核將你的程序的虛擬內存頁直接映射到頁面緩存上。這將致使一個顯著的性能提高:Windows系統編程》指出常規的文件讀取操做運行時性能改善30%以上;Unix環境高級編程》指出相似的狀況也發生在Linux和Solaris系統上。你還可能所以而節省下大量的物理內存,這依賴於你的程序的具體狀況。

 

  

和之前同樣,提到性能,實際測量纔是王道,可是內存映射的確值得被程序員們放入工具箱。相關的API也很漂亮,它提供了像訪問內存中的字節同樣的方式來訪問一個文件,不須要你多操心,也不犧牲代碼的可讀性。回憶一下地址空間、還有那個在Unix類系統上關於mmap的實驗,Windows下的CreateFileMapping及其在高級語言中的各類可用封裝。當你映射一個文件時,它的內容並非馬上就被所有放入內存的,而是依賴頁故障(page fault)按需讀取。在獲取了一個包含所需的文件數據的頁幀後,對應的故障處理函數會將你的虛擬內存頁映射到頁面緩存上。若是所需內容不在緩存當中,此過程還將包含磁盤I/O操做。

  

如今給你出一個流行的測試題。想象一下,在最後一個render程序的實例退出之時,那些保存了scene.dat的頁面緩存會被馬上清理嗎?人們一般會這樣認爲,但這是個壞主意。若是你仔細想一想,咱們常常會在一個程序中建立一個文件,退出,緊接着在第二個程序中使用這個文件。頁面緩存必須能處理此類狀況。若是你再多想一想,內核何須老是要捨棄頁面緩存中的內容呢?記住,磁盤比RAM慢5個數量級,所以一個頁面緩存的命中(hit)就意味着巨大的勝利。只要還有足夠的空閒物理內存,緩存就應該儘量保持滿狀態。因此它與特定的進程並不相關,而是一個系統級的資源。若是你一週前運行過render,而此時scene.dat還在緩存當中,那真使人高興。這就是爲何內核緩存的大小會穩步增長,直到緩存上限。這並不是由於操做系統是破爛貨,吞噬你的RAM,事實上這是種好的行爲,反而釋放物理內存纔是一種浪費。緩存要利用得越充分越好。

  

因爲使用了頁面緩存體系結構,當一個程序調用write()時,相關的字節被簡單的複製到頁面緩存中,而且將頁面標記爲髒的(dirty)。磁盤I/O通常不會馬上發生,所以你的程序的執行不會被打斷去等待磁盤設備。這樣作的缺點是,若是此時計算機死機,那麼你寫入的數據將不會被記錄下來。所以重要的文件,好比數據庫事務記錄必須被fsync() (可是還要當心磁盤控制器的緩存)。另外一方面,讀取操做通常會打斷你的程序直到準備好所需的數據。內核一般採用積極加載(eager loading)的方式來緩解這個問題。以提早讀取(read ahead)爲例,內核會預先加載一些頁到頁面緩存,並期待你的讀取操做。經過提示系統即將對文件進行的是順序仍是隨機讀取操做(參看madvise(), readahead()Windows緩存提示),你能夠幫助內核調整它的積極加載行爲。Linux的確會對內存映射文件進行預取,但我不太肯定Windows是否也如此。最後須要一提的是,你還能夠經過在Linux中使用O_DIRECT或在Windows中使用NO_BUFFERING來繞過頁面緩存,有些數據庫軟件就是這麼作的。

   一個文件映射能夠是私有的(private)或共享的(shared)。這裏的區別只有在更改(update)內存中的內容時纔會顯現出來:在私有映射中,更改並不會被提交到磁盤或對其餘進程可見,而這在共享的映射中就會發生。內核使用寫時拷貝(copy on write)技術,經過頁表項(page table entries),實現私有映射。在下面的例子中,render和另外一個叫render3d的程序(我是否是頗有創意?)同時私有映射了scene.dat。隨後render改寫了映射到此文件的虛擬內存區域:

  

上圖所示的只讀的頁表項並不意 味着映射是隻讀的,它們只是內核耍的小把戲,用於共享物理內存直到可能的最後一刻。你會發現'私有'一詞是多麼的不恰當,你只需記住它只在數據發生更改時 起做用。此設計所帶來的一個結果就是,一個以私有方式映射文件的虛擬內存頁能夠觀察到其餘進程對此文件的改動,只要以前對這個內存頁進行的都是讀取操做。 一旦發生過寫時拷貝,就不會再觀察到其餘進程對此文件的改動了。此行爲不是內核提供的,而是在x86系統上就會如此。另外,從API的角度來講,這也是合理的。與此相反,共享映射只是簡單的映射到頁面緩存,僅此而已。對頁面的全部更改操做對其餘進程均可見,並且最終會執行磁盤操做。最後,若是此共享映射是隻讀的,那麼頁故障將觸發段錯誤(segmentation fault)而不是寫時拷貝。

   被動態加載的函數庫經過文件映射機制放入到你的程序的地址空間中。這裏沒有任何特別之處,一樣是採用私有文件映射,跟提供給你調用的常規API別無二致。下面的例子展現了兩個運行中的render程序的一部分地址空間,還有物理內存。它將咱們以前看到的概念都聯繫在了一塊兒。

  

至此咱們完成了內存基礎知識的三部曲系列。我但願這個系列對您有用,並在您頭腦中創建一個好的操做系統模型。

相關文章
相關標籤/搜索