久久精品精选,精品九九视频,www久久只有这里有精品,亚洲熟女乱色综合一区
    分享

    CPU與內存的那些事

     guitarhua 2019-10-23

    下面是網上看到的一些關于內存和CPU方面的一些很不錯的文章. 整理如下:

    轉: CPU的等待有多久?

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

    原文地址:http:///gustavo/blog/

    [注:本人水平有限,只好挑一些國外高手的精彩文章翻譯一下。一來自己復習,二來與大家分享。]

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

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

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

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

       主存操作的準確延遲是不固定的,與具體的應用以及其他許多因素有關。比如,它依賴于列選通延遲(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也有助于改進性能。雖然16MBcache只能覆蓋整個磁盤容量的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:///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中的對應緩存線,而不會更新主存中的信息。之后,當真的需要更新主存時,處理器會把那個被修改了的緩存線整體放到總線上,一次性寫入內存。所以大部分的請求事務,其數據長度字段都是11REQ[1:0]),對應64 字節。下圖展示了當cache中沒有對應數據時,內存讀取訪問的過程:

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

    前面討論的這些基本知識還包含很多關聯的內容。比如:

    1、  如果應用程序想要盡可能高的運行速度,就應該把會被一起訪問的數據盡量組織在同一條緩存線中。一旦這條緩存線被載入,之后的讀取操作就會加快很多,不再需要額外的內存訪問了。

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

    3、  前端總線是被所有的agent所共享的。這些agent在開啟一個事務之前,必須先進行總線使用權的仲裁。而且,每一個agent都需要偵聽總線上所有的事務,以便維持cache的一致性。因此,隨著部署更多的、多核的處理器到Intel計算機,總線競爭問題會變得越來越嚴重。為解決這個問題,Core i7將處理器直接連接于內存,并以點對點的方式通信,取代之前的廣播方式,從而減少總線競爭。

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

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

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

    原文標題:Motherboard Chipsets and the Memory Map

    原文地址:http:///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 QX660033個針腳用于傳輸物理內存地址(可以表示233個地址位置),64個針腳用于接收/發送數據(所以數據在64位通道中傳輸,也就是8字節的數據塊)。這使得CPU可以控制64GB的物理內存(233個地址乘以8字節),盡管大多數的芯片組只能支持8GBRAM。

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

       當北橋接收到一個物理內存訪問請求時,它需要決定把這個請求轉發到哪里:是發給RAM?抑或是顯卡?具體發給誰是由內存地址映射表來決定的。映射表知道每一個物理內存地址區域所對應的設備。絕大部分的地址被映射到了RAM,其余地址由映射表來通知芯片組該由哪個設備來響應此地址的訪問請求。這些被映射為設備的內存地址形成了一個經典的空洞,位于PC內存的640KB1MB之間。當內存地址被保留用于顯卡和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實際能夠使用的也就只有大約3GBRAM(有時甚至更少,我曾用過一臺安裝了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:///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端口?,F代的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 引導裝載程序(比如LILOGRUB),甚至可能是病毒。與此不同,分區表則是標準化的:它是一個64字節的區塊,包含416字節的記錄項,描述磁盤是如何被分割的(所以你可以在一個磁盤上安裝多個操作系統或擁有多個獨立的卷)。傳統上,MicrosoftMBR代碼會查看分區表,找到一個(唯一的)標記為活動(active)的分區,加載那個分區的引導扇區(boot sector),并執行其中的代碼。引導扇區是一個分區的第一個扇區,而不是整個磁盤的第一個扇區。如果此時出了什么問題,你可能會收到如下錯誤信息:'Invalid Partition Table'無效分區表或'Missing Operating System'操作系統缺失。這條信息不是來自BIOS的,而是由從磁盤加載的MBR程序所給出的。因此這些信息依賴于MBR的內容。

    隨著時間的推移,引導裝載過程已經發展得越來越復雜,越來越靈活。Linux的引導裝載程序LiloGRUB可以處理很多種類的操作系統,文件系統,以及引導配置信息。他們的MBR代碼不再需要效仿上述'從活動分區來引導'的方法。但是從功能上講,這個過程大致如下:

    1、  MBR本身包含有第一階段的引導裝載程序。GRUB稱之為階段一。

    2  由于MBR很小,其中的代碼僅僅用于從磁盤加載另一個含有額外的引導代碼的扇區。此扇區可能是某個分區的引導扇區,但也可能是一個被硬編碼到MBR中的扇區位置。

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

    4、  此時,引導裝載程序需要啟動操作系統核心。它必須擁有足夠的關于文件系統的信息,以便從引導分區中讀取內核。在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:///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不需要引導裝載程序就可以工作,這段代碼是從那個時候留傳下來的?,F今,如果這個引導扇區被執行,它僅僅給用戶輸出一個'bugger_off_msg'之后就會重啟系統?,F代的引導裝載程序會忽略這段遺留代碼。在引導扇區代碼之后,我們會看到實模式內核頭部(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地址開始的,然而在保護模式中,這個中斷向量表的位置是保存在一個叫IDTRCPU寄存器當中的。與此同時,從邏輯內存地址(在程序中使用)到線性內存地址(一個從0連續編號到內存頂端的數值)的翻譯方法在實模式和保護模式中是不同的。保護模式需要一個叫做GDTR的寄存器來存放內存全局描述符表的地址。所以go_to_protected_mode()調用了setup_idt() setup_gdt(),用于裝載臨時的中斷描述符表和全局描述符表。

    現在我們可以轉入保護模式啦,這是由另一段匯編子程序protected_mode_jump來完成的。這個子程序通過設定CPUCR0寄存器的PE位來使能保護模式。此時,分頁功能還處于關閉狀態;分頁是處理器的一個可選的功能,即使運行于保護模式也并非必要。真正重要的是,我們不再受制于640K的內存邊界,現在可以尋址高達4GBRAM了。這個子程序進而調用壓縮狀態內核的32位內核入口點startup_32startup32會做一些簡單的寄存器初始化工作,并調用一個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位模式的初始化過程:

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

    2、  為內存建立最終的全局描述符表。

    3  建立頁表以便可以開啟分頁功能。

    4、  使能分頁功能。

    5  初始化??臻g。

    6  創建最終的中斷描述符表。

    7、  最后,跳轉執行一個體系結構無關的內核啟動函數: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.exeWinlogon.exe。Winlogon會啟動Services.exe(它會啟動所有的Windows服務程序)、Lsass.exe和本地安全認證子系統。經典的Windows登陸對話框就是運行在Winlogon的上下文中的。

       本文是引導啟動系列話題的最后一篇。感謝每一位讀者,感謝你們的反饋。我很抱歉,有些內容只能點到為止;我打算把它們留在其他文章中深入討論,并盡量保持文章的長度適合blog的風格。下次我打算定期的撰寫關于'Software Illustrated'的文章,就像本系列一樣。最后,給大家一些參考資料:

    ?         最好也最重要的資料是實際的內核代碼,LinuxBSD的都成。

    ?         Intel出版的杰出的軟件開發人員手冊,你可以免費下載到。

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

    ?         對于Windows,目前最好的參考書是《Windows Internals》,作者是David SolomonMark Russinovich,后者是Sysinternals的知名專家。這是本特棒的書,寫的很好而且講解全面。主要的缺點是缺少源代碼的支持。

    參考:

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

    轉: 內存地址轉換與分段

    原文標題:Memory Translation and Segmentation

    原文地址:http:///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),它只能被影響程序執行順序的指令所改變,比如CALLJMP指令。雖然分段功能一直是開啟的,但其在實模式與保護模式下的運作方式并不相同的。

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

     

    實模式分段功能

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

    32位保護模式下,段選擇符不再是一個單純的數值,取而代之的是一個索引編號,用于引用段描述符表中的表項。這個表為一個簡單的數組,元素長度為8字節,每個元素描述一個段??雌饋砣缦拢?/span>

     

    段描述符

    有三種類型的段:代碼,數據,系統。為了簡潔明了,只有描述符的共有特征被繪制出來。基地址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),以后我們還會詳細討論?,F在,需要好好想想了。當CPU運行于32位模式時,不管怎樣,寄存器和指令都可以尋址整個線性地址空間,所以根本就不需要再去使用基地址或其他什么鬼東西。那為什么不干脆將基地址設成0,好讓邏輯地址與線性地址一致呢?Intel的文檔將之稱為'扁平模型'(flat model),而且在現代的x86系統內核中就是這么做的(特別指出,它們使用的是基本扁平模型)?;颈馄侥P停?/span>basic flat model)等價于在轉換地址時關閉了分段功能。如此一來多么美好啊。就讓我們來看看32位保護模式下執行一個跳轉指令的例子,其中的數值來自一個實際的Linux用戶模式應用程序:

     

    保護模式的分段

    段描述符的內容一旦被訪問,就會被cache(緩存),所以在隨后的訪問中,就不再需要去實際讀取GDT了,否則會有損性能。每個段寄存器都有一個隱藏部分用于緩存段選擇符所對應的那個段描述符。如果你想了解更多細節,包括關于LDT的更多信息,請參閱《Intel System Programming Guide3A卷的第三章。2A2B卷講述了每一個x86指令,同時也指明了x86尋址時所使用的各種類型的操作數:16位,16位加段描述符(可被用于實現遠指針),32位,等等。

    Linux上,只有3個段描述符在引導啟動過程被使用。他們使用GDT_ENTRY宏來定義并存儲在boot_gdt數組中。其中兩個段是扁平的,可對整個32位空間尋址:一個是代碼段,加載到cs中,一個是數據段,加載到其他段寄存器中。第三個段是系統段,稱為任務狀態段(Task State Segment)。在完成引導啟動以后,每一個CPU都擁有一份屬于自己的GDT。其中大部分內容是相同的,只有少數表項依賴于正在運行的進程。你可以從segment.hLinux 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:///gustavo/blog/

       [注:本人水平有限,只好挑一些國外高手的精彩文章翻譯一下。一來自己復習,二來與大家分享。]

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

     

    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()用于挑出CPLRPL中特權最低的一個,并與描述符特權級(descriptor privilege level,簡稱DPL)比較。如果DPL的值大于等于它,那么這個訪問就獲得許可了。RPL背后的設計思想是:允許內核代碼加載特權較低的段。比如,你可以使用RPL=3的段描述符來確保給定的操作所使用的段可以在用戶模式中訪問。但堆棧段寄存器是個例外,它要求CPL,RPLDPL3個值必須完全一致,才可以被加載。

    事實上,段保護功能幾乎沒什么用,因為現代的內核使用扁平的地址空間。在那里,用戶模式的段可以訪問整個線形地址空間。真正有用的內存保護發生在分頁單元中,即從線形地址轉化為物理地址的時候。一個內存頁就是由一個頁表項(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)。調用門提供了一個可以用于通常的CALLJMP指令的內核入口點,但是由于調用門用得不多,我就忽略不提了。任務門也不怎么熱門(在Linux上,它們只在處理內核或硬件問題引起的雙重故障時才被用到)。

       剩下兩個有趣的:中斷門和陷阱門,它們用來處理硬件中斷(如鍵盤,計時器,磁盤)和異常(如缺頁異常,0除數異常)。我將不再區分中斷和異常,在文中統一用'中斷'一詞表示。這些門描述符被存儲在中斷描述符表(Interrupt Descriptor Table,簡稱IDT)當中。每一個中斷都被賦予一個從0255的編號,叫做中斷向量。處理器把中斷向量作為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的陷阱門,也可以從用戶模式訪問。除此之外,術語名詞都與本文對得上號。然而,硬件中斷門并不是在這里設置的,而是由適當的驅動程序來完成。

       有三個門可以被用戶模式訪問:中斷向量34分別用于調試和檢查數值運算溢出。剩下的是一個系統門,被設置為SYSCALL_VECTOR。對于x86體系結構,它等于0x80。它曾被作為一種機制,用于將進程的控制轉移到內核,進行一個系統調用system call),然后再跳轉回來。在那個時代,我需要去申請'INT 0x80'這個沒用的牌照 J。從奔騰Pro開始,引入了sysenter指令,從此可以用這種更快捷的方式來啟動系統調用了。它依賴于CPU上的特殊目的寄存器,這些寄存器存儲著代碼段、入口點及內核系統調用處理器所需的其他零散信息。在sysenter執行后,CPU不再進行特權檢查,而是直接進入CPL 0,并將新值加載到與代碼和堆棧有關的寄存器當中(cs,eip,ssesp)。只有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:///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)都含有一條緩存線,由與之對應的目錄單元跟蹤管理。圖中的cache64 組、每組8路,因此有512個含有緩存線的單元,合計32KB的存儲空間。

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

       全相聯緩存fully associative cache)中,內存中的任意一條緩存線都可以被存儲到任意的緩存單元中。這種存儲方式十分靈活,但也使得要訪問它們時,檢索緩存單元的工作變得復雜、昂貴。由于L1L2 cache工作在很強的約束之下,包括功耗,芯片物理空間,存取速度等,所以在多數情況下,使用全相聯緩存并不是一個很好的折中。

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

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

     

    在組中搜索匹配標記

       由于我們只需要去查看某一組中的8路,所以查找匹配標記是非常迅速的;事實上,從電學角度講,所有的標記是同時進行比對的,我用箭頭來表示這一點。如果此時正好有一條具有匹配標簽的有效緩存線,我們就獲得一次緩存命中(cache hit)。否則,這個請求就會被轉發的L2 cache,如果還沒匹配上就再轉發給主系統內存。通過應用各種調節尺寸和容量的技術,IntelCPU配置了較大的L2 cache,但其基本的設計都是相同的。比如,你可以將原先的緩存增加8路而獲得一個64KB的緩存;再將組數增加到4096,每路可以存儲256KB。經過這兩次修改,就得到了一個4MBL2 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)來求出物理內存頁的地址,以便用于緩存標記。與此相反,組索引來自于線性地址的低位,所以不需要轉換就可以使用了(在我們的例子中為第611位)。因此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緩存中,一條緩存線可以為4MESI狀態之一:被修改的(modified),獨占的(exclusive),共享的(shared),無效的(invalid)。Intel緩存是包容式的(inclusive):L1緩存的內容會被復制到L2緩存中。在下一篇討論線程(threading),鎖定(locking)等內容的文章中,這些緩存線狀態將發揮作用。下一次,我們將看看前端總線以及內存訪問到底是怎么工作的。這將成為一個內存研討周。

    (在回復中Dave提到了直接映射緩存(direct-mapped cache)。它們基本上是一種特殊的組相聯緩存,只是只有一路而已。在各種折中方案中,它與全相聯緩存正好相反:訪問非??旖荩蚪M沖突而導致的緩存缺失也非常多。)

    [譯者小結:

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

    2.         一般情況下,除了寄存器和L1緩存可以操作指定字長的數據,下層的內存子系統就不會再使用這么小的單位了,而是直接移動數據塊,比如以緩存線為單位訪問數據。

    3.         對于組沖突,可以這么理解:與上文相似,假設一個緩存,由512條緩存線組成,每條線64字節,容量32KB。

    a)         假如它是直接映射緩存,由于它往往使用地址的低位直接映射緩存線編號,所以所有的32K倍數的地址(32K64K,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:///gustavo/blog/

       [注:本人水平有限,只好挑一些國外高手的精彩文章翻譯一下。一來自己復習,二來與大家分享。]

           內存管理模塊是操作系統的心臟;它對應用程序和系統管理非常重要。今后的幾篇文章中,我將著眼于實際的內存問題,但也不避諱其中的技術內幕。由于不少概念是通用的,所以文中大部分例子取自32x86平臺的LinuxWindows系統。本系列第一篇文章講述應用程序的內存布局。

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

     

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

     

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

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

        進程地址空間中最頂部的段是棧,大多數編程語言將之用于存儲局部變量和函數參數。調用一個方法或函數會將一個新的棧楨stack frame)壓入棧中。棧楨在函數返回時被清理。也許是因為數據嚴格的遵從LIFO的順序,這個簡單的設計意味著不必使用復雜的數據結構來追蹤棧的內容,只需要一個簡單的指針指向棧的頂端即可。因此壓棧(pushing)和退棧(popping)過程非常迅速、準確。另外,持續的重用??臻g有助于使活躍的棧內存保持在CPU緩存中,從而加速訪問。進程中的每一個線程都有屬于自己的棧。

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

           動態棧增長是唯一一種訪問未映射內存區域(圖中白色區域)而被允許的情形。其它任何對未映射內存區域的訪問都會觸發頁故障,從而導致段錯誤。一些被映射的區域是只讀的,因此企圖寫這些區域也會導致段錯誤。

           在棧的下方,是我們的內存映射段。此處,內核將文件的內容直接映射到內存。任何應用程序都可以通過Linuxmmap()系統調用(實現)或WindowsCreateFileMapping() / 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。盡管數據段映射了一個文件,但它是一個私有內存映射,這意味著更改此處的內存不會影響到被映射的文件。也必須如此,否則給全局變量賦值將會改動你硬盤上的二進制鏡像,這是不可想象的。

        下圖中數據段的例子更加復雜,因為它用了一個指針。在此情況下,指針gonzo4字節內存地址)本身的值 存在數據段中。而它所指向的實際字符串則不在這里。這個字符串保存在代碼段中,代碼段是只讀的,保存了你全部的代碼外加零零碎碎的東西,比如字符串字面 值。代碼段將你的二進制文件也映射到了內存中,但對此區域的寫操作都會使你的程序收到段錯誤。這有助于防范指針錯誤,雖然不像在C語言編程時就注意防范來得那么有效。下圖展示了這些段以及我們例子中的變量:

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

        你可以通過nmobjdump命令來察看二進制鏡像,打印其中的符號,它們的地址,段等信息。最后需要指出的是,前文描述的虛擬地址布局在Linux中是一種'靈活布局'(flexible layout),而且以此作為默認方式已經有些年頭了。它假設我們有值RLIMIT_STACK。當情況不是這樣時,Linux退回使用'經典布局'(classic layout),如下圖所示:

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

    參考:

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

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

    原文標題:How The Kernel Manages Your Memory

    原文地址:http:///gustavo/blog/

       [注:本人水平有限,只好挑一些國外高手的精彩文章翻譯一下。一來自己復習,二來與大家分享。]

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

        Linux進程在內核中是由task_struct的實例來表示的,即進程描述符。task_structmm字段指向內存描述符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_structmm_struct的組合。VMAWindows中的對應物時虛擬地址描述符(Virtual Address Descriptor),或簡稱VAD;它們保存在平衡樹中(AVL tree)。你知道WindowsLinux最有趣的地方是什么嗎?就是這些細小的不同點。

               4GB虛擬地址空間被分割為許多page)。x86處理器在32位模式下所支持的頁面大小為4KB,2MB4MB。LinuxWindows都使用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/SR/W標志。然而,同樣的物理內存可以被映射到不同的頁面,甚至可以擁有不同的保護標志。值得注意的是,在PTE中沒有對執行許可(execute permission)的設定。這就是為什么經典的x86分頁可以執行位于stack上的代碼,從而為黑客利用堆棧溢出提供了便利(使用return-to-libc和其他技術,甚至可以利用不可執行的堆棧)。PTE缺少不可執行(no-execute)標志引出了一個影響更廣泛的事實:VMA中的各種許可標志可能會也可能不會被明確的轉換為硬件保護。對此,內核可以盡力而為,但始終受到架構的限制。

           虛擬內存并不存儲任何東西,它只是將程序地址空間映射到底層的物理內存上,后者被處理器視為一整塊來訪問,稱作物理地址空間physical address space)。對物理內存的操作還與總線有點聯系,好在我們可以暫且忽略這些并假設物理地址范圍以字節為單位遞增,從0到最大可用內存數。這個物理地址空間被內核分割為一個個頁幀page frame)。處理器并不知道也不關心這些幀,然而它們對內核至關重要,因為頁幀是物理內存管理的最小單元。LinuxWindows32位模式下,都使用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:///gustavo/blog/

       [注:本人水平有限,只好挑一些國外高手的精彩文章翻譯一下。一來自己復習,二來與大家分享。]

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

    提到文件,操作系統必須解決兩個重要的問題。首先是硬盤驅動器的存取速度緩慢得令人頭疼(相對于內存而言),尤其是進程瀏覽器翻看Windows進程,就會發現大約15MB的共享DLL被加載進了每一個進程。我目前的Windows系統就運行了100個進程,如果沒有共享機制,那將消耗大約1.5GB的物理內存僅僅用于存放公用DLL。這可不怎么好。同樣的,幾乎所有的Linux程序都需要ld.solibc,以及其它的公用函數庫。

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

    在讀取了12KB以后,render的堆以及相關的頁幀情況如下:

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

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

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

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

    現在給你出一個流行的測試題。想象一下,在最后一個render程序的實例退出之時,那些保存了scene.dat的頁面緩存會被立刻清理嗎?人們通常會這樣認為,但這是個壞主意。如果你仔細想想,我們經常會在一個程序中創建一個文件,退出,緊接著在第二個程序中使用這個文件。頁面緩存必須能處理此類情況。如果你再多想想,內核何必總是要舍棄頁面緩存中的內容呢?記住,磁盤比RAM5個數量級,因此一個頁面緩存的命中(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程序的一部分地址空間,還有物理內存。它將我們之前看到的概念都聯系在了一起。

    至此我們完成了內存基礎知識的三部曲系列。我希望這個系列對您有用,并在您頭腦中建立一個好的操作系統模型。

    參考:

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

      本站是提供個人知識管理的網絡存儲空間,所有內容均由用戶發布,不代表本站觀點。請注意甄別內容中的聯系方式、誘導購買等信息,謹防詐騙。如發現有害或侵權內容,請點擊一鍵舉報。
      轉藏 分享 獻花(0

      0條評論

      發表

      請遵守用戶 評論公約

      類似文章 更多

      主站蜘蛛池模板: 精品乱码一区二区三区四区| 国产成人无码区免费内射一片色欲| 又大又粗欧美成人网站| 久久综合九色欧美综合狠狠| 无码人妻久久一区二区三区免费丨| 88国产精品视频一区二区三区| 亚洲伊人久久精品影院| 亚洲 国产 制服 丝袜 一区| 精品午夜福利在线观看| 少妇上班人妻精品偷人| 欧美黑人又大又粗XXXXX| 最新国产精品中文字幕| 国产精品一码二码三码| 国产一区在线播放av| 99久久er热在这里只有精品99 | 精品久久久久久无码人妻热| 久久午夜无码鲁丝片直播午夜精品| 桃花岛亚洲成在人线AV| 国产精品无码专区| 亚洲AVAV天堂AV在线网阿V| 成人久久免费网站| 免费国产午夜理论片不卡| 亚洲av无码乱码在线观看野外 | 亚洲色拍拍噜噜噜最新网站| 国产成人无码AV大片大片在线观看| 国偷自产AV一区二区三区| 国产鲁鲁视频在线观看| AV激情亚洲男人的天堂| 日本成熟少妇激情视频免费看 | 无码A级毛片免费视频下载| 四虎在线播放亚洲成人| 久久亚洲中文字幕伊人久久大 | 青青国产揄拍视频| 中文字幕无码AV激情不卡| 国产玩具酱一区二区三区| 国产精品无码无需播放器| 久久久久波多野结衣高潮| 国产精品日日摸夜夜添夜夜添无码| 亚洲A成人片在线播放| AV在线亚洲欧洲日产一区二区 | 2020年最新国产精品正在播放|