乡下人产国偷v产偷v自拍,国产午夜片在线观看,婷婷成人亚洲综合国产麻豆,久久综合给合久久狠狠狠9

  • <output id="e9wm2"></output>
    <s id="e9wm2"><nobr id="e9wm2"><ins id="e9wm2"></ins></nobr></s>

    • 分享

      Windows內(nèi)存管理

       clover_xian 2012-09-17
      Windows內(nèi)存管理

      本文主要內(nèi)容:
      1.基本概念:物理內(nèi)存、虛擬內(nèi)存;物理地址、虛擬地址、邏輯地址;頁目錄,頁表
      2.Windows內(nèi)存管理
      3.CPU段式內(nèi)存管理
      4.CPU頁式內(nèi)存管理
       
      一、基本概念
      1. 兩個內(nèi)存概念
      物理內(nèi)存:人盡皆知,就是插在主板上的內(nèi)存條。他是固定的,內(nèi)存條的容量多大,物理內(nèi)存就有多大(集成顯卡系統(tǒng)除外)。但是如果程序運行很多或者程序本身很大的話,就會導致大量的物理內(nèi)存占用,甚至導致物理內(nèi)存消耗殆盡。
      虛擬內(nèi)存:簡明的說,虛擬內(nèi)存就是在硬盤上劃分一塊頁面文件,充當內(nèi)存。當程序在運行時,有一部分資源還沒有用上或者同時打開幾個程序卻只操作其中一個程序時,系統(tǒng)沒必要將程序所有的資源都塞在物理內(nèi)存中,于是,系統(tǒng)將這些暫時不用的資源放在虛擬內(nèi)存上,等到需要時在調(diào)出來用。
      2.三個地址概念
      物理地址(physical address):用于內(nèi)存芯片級的單元尋址,與處理器和CPU連接的地址總線相對應。
      ——這個概念應該是這幾個概念中最好理解的一個,但是值得一提的是,雖然可以直接把物理地址理解成插在機器上那根內(nèi)存本身,把內(nèi)存看成一個從0字節(jié)一直到 最大空量逐字節(jié)的編號的大數(shù)組,然后把這個數(shù)組叫做物理地址,但是事實上,這只是一個硬件提供給軟件的抽像,內(nèi)存的尋址方式并不是這樣。所以,說它是“與 地址總線相對應”,是更貼切一些,不過拋開對物理內(nèi)存尋址方式的考慮,直接把物理地址與物理的內(nèi)存一一對應,也是可以接受的。也許錯誤的理解更利于形而上的抽像。
      邏輯地址(logical address):是指由程序產(chǎn)生的與段相關的偏移地址部分。例如,你在進行C語言指針編程中,可以讀取指針變量本身值(&操作),實際上這個值就是邏輯地址,它是相對于你當前進程數(shù)據(jù)段的地址,不和絕對物理地址相干。只有在Intel實模式下,邏輯地址才和物理地址相等(因為實模式?jīng)]有分段或分頁機制,Cpu不進行自動地址轉(zhuǎn)換);邏輯也就是在Intel 保護模式下程序執(zhí)行代碼段限長內(nèi)的偏移地址(假定代碼段、數(shù)據(jù)段如果完全一樣)。應用程序員僅需與邏輯地址打交道,而分段和分頁機制對您來說是完全透明的,僅由系統(tǒng)編程人員涉及。應用程序員雖然自己可以直接操作內(nèi)存,那也只能在操作系統(tǒng)給你分配的內(nèi)存段操作。
      Intel為了兼容,將遠古時代的段式內(nèi)存管理方式保留了下來。邏輯地址指的是機器語言指令中,用來指定一個操作數(shù)或者是一條指令的地址。以上例,我們說的連接器為A分配的0x08111111這個地址就是邏輯地址。
      ——不過不好意思,這樣說,好像又違背了Intel中段式管理中,對邏輯地址要求,“一個邏輯地址,是由一個段標識符加上一個指定段內(nèi)相對地址的偏移量, 表示為 [段標識符:段內(nèi)偏移量],也就是說,上例中那個0x08111111,應該表示為[A的代碼段標識符: 0x08111111],這樣,才完整一些”
      線性地址(linear address)或也叫虛擬地址(virtual address)
      跟邏輯地址類似,它也是一個不真實的地址,如果邏輯地址是對應的硬件平臺段式管理轉(zhuǎn)換前地址的話,那么線性地址則對應了硬件頁式內(nèi)存的轉(zhuǎn)換前地址。
      -------------------------------------------------------------
      每個進程都有4GB的虛擬地址空間
      這4GB分3部分
      (1)一部分映射物理內(nèi)存
      (2)一部分映射硬盤上的交換文件
      (3)一部分什么也不做
      程序中都是使用4GB的虛擬地址,訪問物理內(nèi)存需要使用物理地址,物理地址是放在尋址總線上的地址,以字節(jié)(8位)為單位。
      -------------------------------------------------------------
      CPU將一個虛擬內(nèi)存空間中的地址轉(zhuǎn)換為物理地址,需要進行兩步:首先將給定一個邏輯地址(其實是段內(nèi)偏移量,這個一定要理解?。。。?,CPU要利用其段式內(nèi)存管理單元,先將為個邏輯地址轉(zhuǎn)換成一個線程地址,再利用其頁式內(nèi)存管理單元,轉(zhuǎn)換為最終物理地址。
      這樣做兩次轉(zhuǎn)換,的確是非常麻煩而且沒有必要的,因為直接可以把線性地址抽像給進程。之所以這樣冗余,Intel完全是為了兼容而已。
      3.頁表、頁目錄概念
      使用了分頁機制之后,4G的地址空間被分成了固定大小的頁,每一頁或者被映射到物理內(nèi)存,或者被映射到硬盤上的交換文件中,或者沒有映射任何東西。對于一般程序來說,4G的地址空間,只有一小部分映射了物理內(nèi)存,大片大片的部分是沒有映射任何東西。物理內(nèi)存也被分頁,來映射地址空間。對于32bit的 Win2k,頁的大小是4K字節(jié)。CPU用來把虛擬地址轉(zhuǎn)換成物理地址的信息存放在叫做頁目錄和頁表的結(jié)構(gòu)里。
      物理內(nèi)存分頁,一個物理頁的大小為4K字節(jié),第0個物理頁從物理地址 0x00000000 處開始。由于頁的大小為4KB,就是0x1000字節(jié),所以第1頁從物理地址 0x00001000處開始。第2頁從物理地址0x00002000處開始??梢钥吹接捎陧摰拇笮∈?KB,所以只需要32bit的地址中高20bit來尋址物理頁。
      頁目錄:  一個頁目錄大小為4K字節(jié),放在一個物理頁中。由1024個4字節(jié)的頁目錄項組成。頁目錄項的大小為4 個字節(jié)(32bit),所以一個頁目錄中有1024個頁目錄項。頁目錄中的每一項的內(nèi)容(每項4個字節(jié))高20bit用來放一個頁表(頁表放在一個物理頁中)的物理地址,低12bit放著一些標志。
      頁表:  一個頁表的大小為4K字節(jié),放在一個物理頁中。由1024個4字節(jié)的頁表項組成。頁表項的大小為4個字節(jié) (32bit),所以一個頁表中有1024個頁表項。頁表中的每一項的內(nèi)容(每項4個字節(jié),32bit)高20bit用來放一個物理頁的物理地址,低 12bit放著一些標志。
      對于x86系統(tǒng),頁目錄的物理地址放在CPU的CR3寄存器中。
      4. 虛擬地址轉(zhuǎn)換成物理地址
      一個虛擬地址大小為4字節(jié),其中包含找到物理地址的信息
      虛擬地址分3部分
      (1)31-22位(10位)是頁目錄中的索引
      (2)21-12位(10位)是頁表中的索引
      (2)11-0位(12位)是頁內(nèi)偏移

      轉(zhuǎn)換過程:
      首先通過CR3找到頁目錄所在物理頁-》根據(jù)虛擬地址中的31-22找到該頁目錄項-》通過該頁目錄項找到該虛擬地址對應的頁表地址-》根據(jù)虛擬地址21-12找到物理頁的物理地址-》更具虛擬地址的11-0位作為偏移加上該物理頁的地址就找到了 該虛擬地址對應的物理地址
      CPU把虛擬地址轉(zhuǎn)換成物理地址:一個虛擬地址,大小4個字節(jié)(32bit),包含著找到物理地址的信息,分為3個部分:第22位到第31位這10位(最高10位)是頁目錄中的索引,第 12位到第21位這10位是頁表中的索引,第0位到第11位這12位(低12位)是頁內(nèi)偏移。對于一個要轉(zhuǎn)換成物理地址的虛擬地址,CPU首先根據(jù)CR3 中的值,找到頁目錄所在的物理頁。然后根據(jù)虛擬地址的第22位到第31位這10位(最高的10bit)的值作為索引,找到相應的頁目錄項(PDE, page directory entry),頁目錄項中有這個虛擬地址所對應頁表的物理地址。有了頁表的物理地址,根據(jù)虛擬地址的第12位到第21位這10位的值作為索引,找到該頁表中相應的頁表項(PTE,page table entry),頁表項中就有這個虛擬地址所對應物理頁的物理地址。最后用虛擬地址的最低12位,也就是頁內(nèi)偏移,加上這個物理頁的物理地址,就得到了該虛擬地址所對應的物理地址。
      -------------------------------------------------------------
      一個頁目錄有1024項,虛擬地址最高的10bit剛好可以索引1024項(2的10次方等于1024)。一個頁表也有1024項,虛擬地址中間部分的 10bit,剛好索引1024項。虛擬地址最低的12bit(2的12次方等于4096),作為頁內(nèi)偏移,剛好可以索引4KB,也就是一個物理頁中的每個字節(jié)。
      -------------------------------------------------------------
      一個虛擬地址轉(zhuǎn)換成物理地址的計算過程就是,處理器通過CR3找到當前頁目錄所在物理頁,取虛擬地址的高10bit,然后把這10bit右移2bit(因為每個頁目錄項4個字節(jié)長,右移2bit相當于乘4)得到在該頁中的地址,取出該地址處PDE(4個字節(jié)),就找到了該虛擬地址對應頁表所在物理頁,取虛擬地址第12位到第21位這10位,然后把這10bit右移2bit(因為每個頁表項4個字節(jié)長,右移2bit相當于乘4)得到在該頁中的地址,取出該地址處的PTE(4個字節(jié)),就找到了該虛擬地址對應物理頁的地址,最后加上12bit的頁內(nèi)偏移得到了物理地址。
      -------------------------------------------------------------
      32bit的一個指針,可以尋址范圍0x00000000-0xFFFFFFFF,4GB大小。也就是說一個32bit的指針可以尋址整個4GB地址空間的每一個字節(jié)。一個頁表項負責4K的地址空間和物理內(nèi)存的映射,一個頁表1024項,也就是負責1024*4k=4M的地址空間的映射。一個頁目錄項,對應一個頁表。一個頁目錄有1024項,也就對應著1024個頁表,每個頁表負責4M地址空間的映射。1024個頁表負責1024*4M=4G的地址空間映射。一個進程有一個頁目錄。所以以頁為單位,頁目錄和頁表可以保證4G的地址空間中的每頁和物理內(nèi)存的映射。
      -------------------------------------------------------------
      每個進程都有自己的4G地址空間,從0x00000000-0xFFFFFFFF。通過每個進程自己的一套頁目錄和頁表來實現(xiàn)。由于每個進程有自己的頁目錄和頁表,所以每個進程的地址空間映射的物理內(nèi)存是不一樣的。兩個進程的同一個虛擬地址處(如果都有物理內(nèi)存映射)的值一般是不同的,因為他們往往對應不同的物理頁。

      4G地址空間中低2G,0x00000000-0x7FFFFFFF是用戶地址空間,4G地址空間中高2G,即0x80000000-0xFFFFFFFF 是系統(tǒng)地址空間。訪問系統(tǒng)地址空間需要程序有ring0的權(quán)限。
       
      二. windows內(nèi)存原理
       
      主要的內(nèi)容如下:
      1.概述
      2.虛擬內(nèi)存
      3.物理內(nèi)存
      4.映射

      1.概述:
      windows中 我們一般編程時接觸的都是線性地址 也就是我們所說的虛擬地址,然而很不幸在我不斷成長的過程中發(fā)現(xiàn)原來線性地址是操作系統(tǒng)自己意淫出來的,根本就不是我們數(shù)據(jù)真實存在的地方.換句話說我們在0x80000000(虛擬地址)的地方寫入了"UESTC"這個字符串,但是我們這個字符串并不真實存在于物理地址的0x80000000這里.再說了真實的物理地址是利用一段N長的數(shù)組來定位的(額~看不懂這句話沒關系,一會看到物理地址那你就明白了).但是為什么windows乃至linux都需要采取這種方式來尋址呢?原因很簡單 為了安全.聽說過保護模式吧?顧名思義就是這個模式下加入了保護系統(tǒng)安全的措施,同樣采用線性地址也是所謂的安全措施之一.
          我們假設下如果沒有使用線性地址,那么我們可以直接訪問物理地址,但是這樣的話當我們往內(nèi)存寫東西的時候操作系統(tǒng)無法檢查這塊內(nèi)存是否可寫,換句話說操作系統(tǒng)無法實現(xiàn)對頁面訪問控制.這點是很可怕的事情,就如win9x那樣沒事在應用態(tài)往內(nèi)核地址寫東西,還有沒有天理了~~
          由于操作系統(tǒng)的安全需要,催生了虛擬地址的應用.在CPU中有個叫MMU(應該是Memory Manage Unit 內(nèi)存管理單元)的東西,專門負責線性地址和物理地址之間的轉(zhuǎn)化.我們每次讀寫內(nèi)存,從CPU的結(jié)構(gòu)看來不是都要經(jīng)過ALU么,ALU拿到虛擬地址后扔給MMU轉(zhuǎn)化成物理地址后再把數(shù)據(jù)讀入寄存器中.過程就是如此.

      2.虛擬內(nèi)存
          我們編程時所面對的都是虛擬地址,對于每個進程來說都擁有4G的虛擬內(nèi)存(小補充: 4G虛擬內(nèi)存中,高2G內(nèi)存屬于內(nèi)核部分,是所有進程共有的,低2G內(nèi)存數(shù)據(jù)是進程獨有的,每個進程低2G內(nèi)存都不一樣),但注意的是虛擬地址是操作系統(tǒng)自己意淫出來的,打個比方就是說想法還未付諸實踐,所以不構(gòu)成任何資源損失.比如我們要在0x80000000的地方寫個"UESTC"的時候,操作系統(tǒng)就會將這個虛擬地址映射到一塊物理地址A中,你寫這塊虛擬地址就相當于寫物理地址A.但是加入我們只申請了一段1KB的虛擬內(nèi)存空間,并未讀寫,系統(tǒng)是不會分配任何物理內(nèi)存的,只有當虛擬內(nèi)存要使用時系統(tǒng)才會分配相應的物理空間.
          下面要說些細節(jié)點的了,其實說白了虛擬內(nèi)存的管理是由一堆數(shù)據(jù)結(jié)構(gòu)來實現(xiàn)的,下面給出這些數(shù)據(jù)結(jié)構(gòu):
      (懶得打那么多 就只打出重要的部分~~)
      在EPROCESS中有個數(shù)據(jù)結(jié)構(gòu)如下:
      typedef struct _MADDRESS_SPACE
      {
          PMEMORY_AREA MemoryAreaRoot ; //這個指針指向一顆二叉排序樹,想必學過數(shù)據(jù)結(jié)構(gòu)的朋友都曉得吧~~嘿嘿~~ 主要是這個情況下采用二叉排序樹能加快內(nèi)存的搜索速度
          ...
          ...
          ...
      }MADDRESS_SPACE , *PMADDRESS_SPACE ;

      然而這顆二叉排序樹的節(jié)點結(jié)構(gòu)結(jié)構(gòu)是這樣的:
      typedef struct _MEMORY_AREA
      {
          PVOID StartingAddress ; //虛擬內(nèi)存段的起始地址
          PVOID EndingAddress ;   //虛擬內(nèi)存段的結(jié)束地址
          struct _MEMORY_AREA *Parent ; //該節(jié)點的老爹
          struct _MEMORY_AREA *LeftChild ; //該節(jié)點的左兒子
          struct _MEMORY_AREA *RrightChild ; //該節(jié)點的左兒子
          ...
          ...
          ...

      }MEMORY_AREA , *PMEMORY_AREA ;
          這個節(jié)點內(nèi)主要記錄了已分配的虛擬內(nèi)存空間,如果要申請?zhí)摂M內(nèi)存空間就跑來這里創(chuàng)建個節(jié)點就好了,如果要刪除空間同樣把對應節(jié)點刪除就好了.不過說來簡單,其實還會涉及到很多操作,比如要平衡這棵樹什么的.
          那么我們在分配虛擬內(nèi)存空間時系統(tǒng)會跑去找到這顆樹,然后通過一定算法遍歷這顆樹,找到符合條件的內(nèi)存空隙(未分配的內(nèi)存空間),創(chuàng)建個節(jié)點掛到這顆樹上,返回起始地址就完成了.


      3.物理內(nèi)存
          接下來就到物理內(nèi)存的東東了,其實呢 物理內(nèi)存的管理是基于一個數(shù)組來管理的,聽說過分頁機制吧,下面說下分頁.windows下分頁是4kb一頁 那么假設我們物理內(nèi)存有4GB 那么windows會將這4GB空間分頁,分成4GB/4KB = 1M頁    那么每一頁(就是4KB)的物理空間都由一個叫PHYSICAL_PAGE的數(shù)據(jù)結(jié)構(gòu)管理,這個數(shù)據(jù)結(jié)構(gòu)就不寫啦....一來我寫的手酸 二來看的人也累~~說說思路就好了.
          接著以上假設 4GB的內(nèi)存 操作系統(tǒng)就會相應產(chǎn)生一個PHYSICAL_PAGE數(shù)組,這個數(shù)組有多少個元素呢?有1M個 正好覆蓋了4GB的物理地址.這就是所謂的分頁機制的原型.說白了就是把內(nèi)存按4k劃分來管理.那么物理地址就好定位了,所謂的物理內(nèi)存地址就可以直接以數(shù)組下標來表示,其實這里的物理內(nèi)存地址指的是物理內(nèi)存地址的頁面號... 具體地址還是要根據(jù)虛擬內(nèi)存地址的低12位和物理內(nèi)存的頁面號一起確定的
           再說下物理內(nèi)存的管理吧,在內(nèi)核下有3個隊列 這些隊列內(nèi)的元素就是上邊所說的PHYSICAL_PAGE結(jié)構(gòu)
      它們分別是:
      A.已分配的內(nèi)存隊列 :存放正被使用的內(nèi)存
      B.待清理的內(nèi)存隊列 :存放已被釋放的內(nèi)存,但是這些內(nèi)存未被清理(清零)
      C.空閑隊列 :存放可使用的空閑內(nèi)存

      系統(tǒng)管理流程如下:
      1).每隔一段時間,系統(tǒng)會自動從B隊列中提取隊列元素進行清理,然后放入空閑隊列中.
      2).每當釋放物理內(nèi)存時,系統(tǒng)會將要釋放的內(nèi)存從A隊列提取出來,放入B隊列中.
      3).申請內(nèi)存時,系統(tǒng)將要分配的內(nèi)存從C隊列中提取出來,放入A隊列中

      4.映射
          說到映射得先從虛擬內(nèi)存的32位地址說起.在CPU中存在個CR3寄存器,里面存放著每個進程的頁目錄地址

      我們可以把轉(zhuǎn)換過程分成幾步看
      1.根據(jù)CR3(注:CR3中的值是物理地址)的值我們可以定位到當前進程的頁目錄基址,然后通過虛擬地址的高10位做偏移量來獲得指定的PDE(Page Directory Entry),PDE內(nèi)容有4字節(jié),高20位部分用來做頁表基址,剩下的比特位用來實現(xiàn)權(quán)限控制之類的東西了.系統(tǒng)只要檢測相應的比特位就可以實現(xiàn)內(nèi)存的權(quán)限控制了.
      2.通過PDE提供的基址加上虛擬內(nèi)存的中10位(21-12)做偏移量就找到了頁表PTE(Page Table Entry)地址,然后PTE的高20位就是物理內(nèi)存的基址了(其實就是那個PHYSICAL_PAGE數(shù)組的下標號....),剩下的比特位同樣用于訪問控制之類的.
      3.通過虛擬內(nèi)存的低12位加上PTE中高20位做基址就可以確定唯一的物理內(nèi)存了.

        
      三. CPU段式內(nèi)存管理,邏輯地址如何轉(zhuǎn)換為線性地址
      一個邏輯地址由兩部份組成,段標識符: 段內(nèi)偏移量。段標識符是由一個16位長的字段組成,稱為段選擇符。其中前13位是一個索引號。后面3位包含一些硬件細節(jié),如圖:

      Windows內(nèi)存管理 - 嘯百川 - 嘯百川的博客

      最后兩位涉及權(quán)限檢查,本貼中不包含。

      索引號,或者直接理解成數(shù)組下標——那它總要對應一個數(shù)組吧,它又是什么東東的索引呢?這個東東就是“段描述符(segment descriptor)”,呵呵,段描述符具體地址描述了一個段(對于“段”這個字眼的理解,我是把它想像成,拿了一把刀,把虛擬內(nèi)存,砍成若干的截—— 段)。這樣,很多個段描述符,就組了一個數(shù)組,叫“段描述符表”,這樣,可以通過段標識符的前13位,直接在段描述符表中找到一個具體的段描述符,這個描 述符就描述了一個段,我剛才對段的抽像不太準確,因為看看描述符里面究竟有什么東東——也就是它究竟是如何描述的,就理解段究竟有什么東東了,每一個段描 述符由8個字節(jié)組成,如下圖:

      Windows內(nèi)存管理 - 嘯百川 - 嘯百川的博客

      這些東東很復雜,雖然可以利用一個數(shù)據(jù)結(jié)構(gòu)來定義它,不過,我這里只關心一樣,就是Base字段,它描述了一個段的開始位置的線性地址。

      Intel設計的本意是,一些全局的段描述符,就放在“全局段描述符表(GDT)”中,一些局部的,例如每個進程自己的,就放在所謂的“局部段描述符表 (LDT)”中。那究竟什么時候該用GDT,什么時候該用LDT呢?這是由段選擇符中的T1字段表示的,=0,表示用GDT,=1表示用LDT。

      GDT在內(nèi)存中的地址和大小存放在CPU的gdtr控制寄存器中,而LDT則在ldtr寄存器中。

      好多概念,像繞口令一樣。這張圖看起來要直觀些:

      Windows內(nèi)存管理 - 嘯百川 - 嘯百川的博客

      首先,給定一個完整的邏輯地址[段選擇符:段內(nèi)偏移地址],
      1、看段選擇符的T1=0還是1,知道當前要轉(zhuǎn)換是GDT中的段,還是LDT中的段,再根據(jù)相應寄存器,得到其地址和大小。我們就有了一個數(shù)組了。
      2、拿出段選擇符中前13位,可以在這個數(shù)組中,查找到對應的段描述符,這樣,它了Base,即基地址就知道了。
      3、把Base + offset,就是要轉(zhuǎn)換的線性地址了。

      還是挺簡單的,對于軟件來講,原則上就需要把硬件轉(zhuǎn)換所需的信息準備好,就可以讓硬件來完成這個轉(zhuǎn)換了。OK,來看看Linux怎么做的。

      Linux的段式管理
      Intel要求兩次轉(zhuǎn)換,這樣雖說是兼容了,但是卻是很冗余,呵呵,沒辦法,硬件要求這樣做了,軟件就只能照辦,怎么著也得形式主義一樣。
      另一方面,其它某些硬件平臺,沒有二次轉(zhuǎn)換的概念,Linux也需要提供一個高層抽像,來提供一個統(tǒng)一的界面。所以,Linux的段式管理,事實上只是“哄騙”了一下硬件而已。

      按照Intel的本意,全局的用GDT,每個進程自己的用LDT——不過Linux則對所有的進程都使用了相同的段來對指令和數(shù)據(jù)尋址。即用戶數(shù)據(jù)段,用 戶代碼段,對應的,內(nèi)核中的是內(nèi)核數(shù)據(jù)段和內(nèi)核代碼段。這樣做沒有什么奇怪的,本來就是走形式嘛,像我們寫年終總結(jié)一樣。
      [Copy to clipboard] [ - ]
      CODE:
      #define GDT_ENTRY_DEFAULT_USER_CS 14
      #define __USER_CS (GDT_ENTRY_DEFAULT_USER_CS * 8 + 3)

      #define GDT_ENTRY_DEFAULT_USER_DS 15
      #define __USER_DS (GDT_ENTRY_DEFAULT_USER_DS * 8 + 3)

      #define GDT_ENTRY_KERNEL_BASE 12

      #define GDT_ENTRY_KERNEL_CS (GDT_ENTRY_KERNEL_BASE + 0)
      #define __KERNEL_CS (GDT_ENTRY_KERNEL_CS * 8)

      #define GDT_ENTRY_KERNEL_DS (GDT_ENTRY_KERNEL_BASE + 1)
      #define __KERNEL_DS (GDT_ENTRY_KERNEL_DS * 8)
      把其中的宏替換成數(shù)值,則為:
      [Copy to clipboard] [ - ]
      CODE:
      #define __USER_CS 115        [00000000 1110   0   11]
      #define __USER_DS 123        [00000000 1111   0   11]
      #define __KERNEL_CS 96    [00000000 1100   0   00]
      #define __KERNEL_DS 104 [00000000 1101   0   00]
      方括號后是這四個段選擇符的16位二制表示,它們的索引號和T1字段值也可以算出來了
      [Copy to clipboard] [ - ]
      CODE:
      __USER_CS              index= 14 T1=0
      __USER_DS             index= 15 T1=0
      __KERNEL_CS           index=   12   T1=0
      __KERNEL_DS           index= 13 T1=0
      T1均為0,則表示都使用了GDT,再來看初始化GDT的內(nèi)容中相應的12-15項(arch/i386/head.S):
      [Copy to clipboard] [ - ]
      CODE:
      .quad 0x00cf9a000000ffff /* 0x60 kernel 4GB code at 0x00000000 */
      .quad 0x00cf92000000ffff /* 0x68 kernel 4GB data at 0x00000000 */
      .quad 0x00cffa000000ffff /* 0x73 user 4GB code at 0x00000000 */
      .quad 0x00cff2000000ffff /* 0x7b user 4GB data at 0x00000000 */

      按照前面段描述符表中的描述,可以把它們展開,發(fā)現(xiàn)其16-31位全為0,即四個段的基地址全為0。
      這樣,給定一個段內(nèi)偏移地址,按照前面轉(zhuǎn)換公式,0 + 段內(nèi)偏移,轉(zhuǎn)換為線性地址,可以得出重要的結(jié)論,“在Linux下,邏輯地址與線性地址總是一致(是一致,不是有些人說的相同)的,即邏輯地址的偏移量字段的值與線性地址的值總是相同的。?。?!”
      忽略了太多的細節(jié),例如段的權(quán)限檢查。呵呵。
      Linux中,絕大部份進程并不例用LDT,除非使用Wine ,仿真Windows程序的時候。

      四.CPU頁式內(nèi)存管理

      CPU的頁式內(nèi)存管理單元,負責把一個線性地址,最終翻譯為一個物理地址。從管理和效率的角度出發(fā),線性地址被分為以固定長度為單位的組,稱為頁 (page),例如一個32位的機器,線性地址最大可為4G,可以用4KB為一個頁來劃分,這頁,整個線性地址就被劃分為一個tatol_page [2^20]的大數(shù)組,共有2的20個次方個頁。這個大數(shù)組我們稱之為頁目錄。目錄中的每一個目錄項,就是一個地址——對應的頁的地址。
      另一類“頁”,我們稱之為物理頁,或者是頁框、頁楨的。是分頁單元把所有的物理內(nèi)存也劃分為固定長度的管理單位,它的長度一般與內(nèi)存頁是一一對應的。
      這里注意到,這個total_page數(shù)組有2^20個成員,每個成員是一個地址(32位機,一個地址也就是4字節(jié)),那么要單單要表示這么一個數(shù)組,就要占去4MB的內(nèi)存空間。為了節(jié)省空間,引入了一個二級管理模式的機器來組織分頁單元。文字描述太累,看圖直觀一些:

      Windows內(nèi)存管理 - 嘯百川 - 嘯百川的博客

      如上圖,
      1、分頁單元中,頁目錄是唯一的,它的地址放在CPU的cr3寄存器中,是進行地址轉(zhuǎn)換的開始點。萬里長征就從此長始了。
      2、每一個活動的進程,因為都有其獨立的對應的虛似內(nèi)存(頁目錄也是唯一的),那么它也對應了一個獨立的頁目錄地址?!\行一個進程,需要將它的頁目錄地址放到cr3寄存器中,將別個的保存下來。
      3、每一個32位的線性地址被劃分為三部份,面目錄索引(10位):頁表索引(10位):偏移(12位)
      依據(jù)以下步驟進行轉(zhuǎn)換:
      1、從cr3中取出進程的頁目錄地址(操作系統(tǒng)負責在調(diào)度進程的時候,把這個地址裝入對應寄存器);
      2、根據(jù)線性地址前十位,在數(shù)組中,找到對應的索引項,因為引入了二級管理模式,頁目錄中的項,不再是頁的地址,而是一個頁表的地址。(又引入了一個數(shù)組),頁的地址被放到頁表中去了。
      3、根據(jù)線性地址的中間十位,在頁表(也是數(shù)組)中找到頁的起始地址;
      4、將頁的起始地址與線性地址中最后12位相加,得到最終我們想要的葫蘆;

      這個轉(zhuǎn)換過程,應該說還是非常簡單地。全部由硬件完成,雖然多了一道手續(xù),但是節(jié)約了大量的內(nèi)存,還是值得的。那么再簡單地驗證一下:
      1、這樣的二級模式是否仍能夠表示4G的地址;
      頁目錄共有:2^10項,也就是說有這么多個頁表
      每個目表對應了:2^10頁;
      每個頁中可尋址:2^12個字節(jié)。
      還是2^32 = 4GB

      2、這樣的二級模式是否真的節(jié)約了空間;
      也就是算一下頁目錄項和頁表項共占空間 (2^10 * 4 + 2 ^10 *4) = 8KB。哎,……怎么說呢!?。。ㄕ娴臏p少了嗎?因該是增加了吧,(4 + 2^10 * 4 + 2 ^10 * 2 ^10 *4) = 4100KB+4Byte)

      值得一提的是,雖然頁目錄和頁表中的項,都是4個字節(jié),32位,但是它們都只用高20位,低12位屏蔽為0——把頁表的低12屏蔽為0,是很好理解的,因 為這樣,它剛好和一個頁面大小對應起來,大家都成整數(shù)增加。計算起來就方便多了。但是,為什么同時也要把頁目錄低12位屏蔽掉呢?因為按同樣的道理,只要 屏蔽其低10位就可以了,不過我想,因為12>10,這樣,可以讓頁目錄和頁表使用相同的數(shù)據(jù)結(jié)構(gòu),方便。

      本貼只介紹一般性轉(zhuǎn)換的原理,擴展分頁、頁的保護機制、PAE模式的分頁這些麻煩點的東東就不啰嗦了……可以參考其它專業(yè)書籍。
       
       
       
       Win32通過一個兩層的表結(jié)構(gòu)來實現(xiàn)地址映射,因為每個進程都擁有私有的4G的虛擬內(nèi)存空間,相應的,每個進程都有自己的層次表結(jié)構(gòu)來實現(xiàn)其地址映射。
            第一層稱為頁目錄,實際就是一個內(nèi)存頁,Win32的內(nèi)存頁有4KB大小,這個內(nèi)存頁以4個字節(jié)分為1024項,每一項稱為“頁目錄項”(PDE);
            第二層稱為頁表,這一層共有1024個頁表,頁表結(jié)構(gòu)與頁目錄相似,每個頁表也都是一個內(nèi)存頁,這個內(nèi)存頁以4KB的大小被分為1024項,頁表的每一項被稱為頁表項(PTE),易知共有1024×1024個頁表項。每一個頁表項對應一個物理內(nèi)存中的某一個“內(nèi)存頁”,即共有1024×1024個物理內(nèi)存頁,每個物理內(nèi)存頁為4KB,這樣就可以索引到4G大小的虛擬物理內(nèi)存。
      如下圖所示(注下圖中的頁目錄項和頁表項的大小應該是4個字節(jié),而不是4kB):

            Win32提供了4GB大小的虛擬地址空間。因此每個虛擬地址都是一個32位的整數(shù)值,也就是我們平時所說的指針,即指針的大小為4B。它由三部分組成,如下圖:

            這三個部分的第一部分,即前10位為頁目錄下標,用來尋址頁目錄項,頁目錄項剛好1024個。找到頁目錄項后,找對頁目錄項對應的的頁表。第二部分則是用來在頁表內(nèi)尋址,用來找到頁表項,共有1024個頁表項,通過頁表項找到物理內(nèi)存頁。第三部分用來在物理內(nèi)存頁中找到對應的字節(jié),一個頁的大小是4KB,12位剛好可以滿足尋址要求。
      具體的例子:
      假設一個線程正在訪問一個指針(Win32的指針指的就是虛擬地址)指向的數(shù)據(jù),此指針指為0x2A8E317F,下圖表示了這一個過程:

      0x2A8E317F的二進制寫法為0010101010_0011100011_000101111111,為了方便我們把它分為三個部分。
      首先按照0010101010尋址,找到頁目錄項。因為一個頁目錄項為4KB,那么先將0010101010左移兩位,001010101000(0x2A8),用此下標找到頁目錄項,然后根據(jù)此頁目錄項定位到下一層的某個頁表。
      然后按照0011100011尋址,在上一步找到頁表中尋找頁表項。尋址方法與上述方法類似。找到頁表項后,就可以找到對應的物理內(nèi)存頁。
      最后按照000101111111尋址,尋找頁內(nèi)偏移。
            上面的假設的是此數(shù)據(jù)已在物理內(nèi)存中,其實判斷訪問的數(shù)據(jù)是否在內(nèi)存中也是在地址映射過程中完成的。Win32系統(tǒng)總是假設數(shù)據(jù)已在物理內(nèi)存中,并進行地址映射。頁表項中有一位標志位,用來標識包含此數(shù)據(jù)的頁是否在物理內(nèi)存中,如果在的話,就直接做地址映射,否則,拋出缺頁中斷,此時頁表項也可標識包含此數(shù)據(jù)的頁是否在調(diào)頁文件中(外存),如果不在則訪問違例,程序?qū)顺觯绻?,頁表項會查出此?shù)據(jù)頁在哪個調(diào)頁文件中,然后將此數(shù)據(jù)頁調(diào)入物理內(nèi)存,再繼續(xù)進行地址映射。為了實現(xiàn)每個進程擁有私有4G的虛擬地址空間,也就是說每個進程都擁有自己的頁目錄和頁表結(jié)構(gòu),對不同進程而言,即使是相同的指針(虛擬地址)被不同的進程映射到的物理地址也是不同的,這也意味著在進程之間傳遞指針是沒有意義的。


      Linux的頁式內(nèi)存管理
      原理上來講,Linux只需要為每個進程分配好所需數(shù)據(jù)結(jié)構(gòu),放到內(nèi)存中,然后在調(diào)度進程的時候,切換寄存器cr3,剩下的就交給硬件來完成了(呵呵,事實上要復雜得多,不過偶只分析最基本的流程)。

      前面說了i386的二級頁管理架構(gòu),不過有些CPU,還有三級,甚至四級架構(gòu),Linux為了在更高層次提供抽像,為每個CPU提供統(tǒng)一的界面。提供了一個四層頁管理架構(gòu),來兼容這些二級、三級、四級管理架構(gòu)的CPU。這四級分別為:

      頁全局目錄PGD(對應剛才的頁目錄)
      頁上級目錄PUD(新引進的)
      頁中間目錄PMD(也就新引進的)
      頁表PT(對應剛才的頁表)。

      整個轉(zhuǎn)換依據(jù)硬件轉(zhuǎn)換原理,只是多了二次數(shù)組的索引罷了,如下圖:

      Windows內(nèi)存管理 - 嘯百川 - 嘯百川的博客

      那么,對于使用二級管理架構(gòu)32位的硬件,現(xiàn)在又是四級轉(zhuǎn)換了,它們怎么能夠協(xié)調(diào)地工作起來呢?嗯,來看這種情況下,怎么來劃分線性地址吧!
      從硬件的角度,32位地址被分成了三部份——也就是說,不管理軟件怎么做,最終落實到硬件,也只認識這三位老大。
      從軟件的角度,由于多引入了兩部份,,也就是說,共有五部份?!尪蛹軜?gòu)的硬件認識五部份也很容易,在地址劃分的時候,將頁上級目錄和頁中間目錄的長度設置為0就可以了。
      這樣,操作系統(tǒng)見到的是五部份,硬件還是按它死板的三部份劃分,也不會出錯,也就是說大家共建了和諧計算機系統(tǒng)。

      這樣,雖說是多此一舉,但是考慮到64位地址,使用四層轉(zhuǎn)換架構(gòu)的CPU,我們就不再把中間兩個設為0了,這樣,軟件與硬件再次和諧——抽像就是強大呀?。。?/p>

      例如,一個邏輯地址已經(jīng)被轉(zhuǎn)換成了線性地址,0x08147258,換成二制進,也就是:
      0000100000 0101000111 001001011000
      內(nèi)核對這個地址進行劃分
      PGD = 0000100000
      PUD = 0
      PMD = 0
      PT = 0101000111
      offset = 001001011000

      現(xiàn)在來理解Linux針對硬件的花招,因為硬件根本看不到所謂PUD,PMD,所以,本質(zhì)上要求PGD索引,直接就對應了PT的地址。而不是再到PUD和 PMD中去查數(shù)組(雖然它們兩個在線性地址中,長度為0,2^0 =1,也就是說,它們都是有一個數(shù)組元素的數(shù)組),那么,內(nèi)核如何合理安排地址呢?
      從軟件的角度上來講,因為它的項只有一個,32位,剛好可以存放與PGD中長度一樣的地址指針。那么所謂先到PUD,到到PMD中做映射轉(zhuǎn)換,就變成了保 持原值不變,一一轉(zhuǎn)手就可以了。這樣,就實現(xiàn)了“邏輯上指向一個PUD,再指向一個PDM,但在物理上是直接指向相應的PT的這個抽像,因為硬件根本不知 道有PUD、PMD這個東西”。

      然后交給硬件,硬件對這個地址進行劃分,看到的是:
      頁目錄 = 0000100000
      PT = 0101000111
      offset = 001001011000
      嗯,先根據(jù)0000100000(32),在頁目錄數(shù)組中索引,找到其元素中的地址,取其高20位,找到頁表的地址,頁表的地址是由內(nèi)核動態(tài)分配的,接著,再加一個offset,就是最終的物理地址了。
       
       
      五. 存儲方式
      保護模式現(xiàn)代操作系統(tǒng)的基礎,理解他是我們要翻越的第一座山。保護模式是相對實模式而言的,他們是處理器的兩種工作方式。很久以前大家使用的dos就是運行在實模式下,而現(xiàn)在的windows操作系統(tǒng)則是運行在保護模式下。兩種運行模式有著較大的不同,
      實模式由于是由8086/8088發(fā)展而來因此他更像是一個運行單片機的簡單模式,計算機啟動后首先進入的就是實模式,通過8086/8088只有20根地址線所以它的尋址范圍只有2的20次冪,即1M。內(nèi)存的訪問方式就是我們熟悉的seg:offset邏輯地址方式,例如我們給出地址邏輯地址它將在cpu內(nèi)轉(zhuǎn)換為20的物理地址,即將seg左移4位再加上offset值。例如地址1000h:5678h,則物理地址為10000h+5678h=15678h。實模式在后續(xù)的cpu中被保留了下來,但實模式的局限性是很明顯的,由于使用seg:offset邏輯地址只能訪問1M多一點的內(nèi)存空間,在擁有32根地址線的cpu中訪問1M以上的空間則變得很困難。而且隨著計算機的不斷發(fā)展實模式的工作方式越來越不能滿足計算機對資源(存儲資源和cpu資源等等)的管理,由此產(chǎn)生了新的管理方式——保護模式。
      80386及以上的處理器功能要大大超過其先前的處理器,但只有在保護模式下,處理器才能發(fā)揮作用。在保護模式下,全部32根地址線有效,可尋址4G的物理地址空間;擴充的存儲分段機制和可選的存儲器分頁機制,不僅為存儲器共享和保護提供了硬件支持,而且為實現(xiàn)虛擬存儲器提供了硬件支持;支持多任務;4個特權(quán)級和完善的特權(quán)級檢查機制,實現(xiàn)了數(shù)據(jù)的安全和保密。計算機啟動后首先進入的就是實模式,通過設置相應的寄存器才能進入保護模式(以后介紹)。保護模式是一個整體的工作方式,但分步討論由淺入深更利于學習。

      存儲方式主要體現(xiàn)在內(nèi)存訪問方式上,由于兼容和IA32框架的限制,保護模式在內(nèi)存訪問上延用了實模式下的seg:offset的形式(即:邏輯地址),其實seg:offset的形式在保護模式下只是一個軀殼,內(nèi)部的存儲方式與實模式截然不同。在保護模式下邏輯地址并不是直接轉(zhuǎn)換為物理地址,而是將邏輯地址首先轉(zhuǎn)換為線性地址,再將線性地址轉(zhuǎn)換為物理地址。

      線性地址是個新概念,但大家不要把它想的過于復雜,簡單的說他就是0000000h~ffffffffh(即0~4G)的線性結(jié)構(gòu),是32個bite位能表示的一段連續(xù)的地址,但他是一個概念上的地址,是個抽象的地址,并不存在在現(xiàn)實之中。線性地址地址主要是為分頁機制而產(chǎn)生的。處理器在得到邏輯地址后首先通過分段機制轉(zhuǎn)換為線性地址,線性地址再通過分頁機制轉(zhuǎn)換為物理地址最后讀取數(shù)據(jù)。
       
      分段機制是必須的,分頁機制是可選的,當不使用分頁的時候線性地址將直接映射為物理地址,設立分頁機制的目的主要是為了實現(xiàn)虛擬存儲(分頁機制在后面介紹)。先來介紹一下分段機制,以下文字是介紹如何由邏輯地址轉(zhuǎn)換為線性地址。
      分段機制在保護模式中是不能被繞過得,回到我們的seg:offset地址結(jié)構(gòu),在保護模式中seg有個新名字叫做“段選擇子”(seg..selector)。段選擇子、GDT、LDT構(gòu)成了保護模式的存儲結(jié)構(gòu),GDT、LDT分別叫做全局描述符表和局部描述符表,描述符表是一個線性表(數(shù)組),表中存放的是描述符。
      “描述符”是保護模式中的一個新概念,它是一個8字節(jié)的數(shù)據(jù)結(jié)構(gòu),它的作用主要是描述一個段(還有其他作用以后再說),用描述表中記錄的段基址加上邏輯地址(sel:offset)的offset轉(zhuǎn)換成線性地址。描述符主要包括三部分:段基址(Base)、段限制(Limit)、段屬性(Attr)。一個任務會涉及多個段,每個段需要一個描述符來描述,為了便于組織管理,80386及以后處理器把描述符組織成表,即描述符表。在保護模式中存在三種描述符表 “全局描述符表”(GDT)、“局部描述符表”(LDT)和中斷描述符表(IDT)(IDT在以后討論)。
      (1)全局描述符表GDT(Global Descriptor Table)在整個系統(tǒng)中,全局描述符表GDT只有一張,GDT可以被放在內(nèi)存的任何位置,但CPU必須知道GDT的入口,也就是基地址放在哪里,Intel的設計者門提供了一個寄存器GDTR用來存放GDT的入口地址,程序員將GDT設定在內(nèi)存中某個位置之后,可以通過LGDT指令將GDT的入口地址裝入此積存器,從此以后,CPU就根據(jù)此寄存器中的內(nèi)容作為GDT的入口來訪問GDT了。GDTR中存放的是GDT在內(nèi)存中的基地址和其表長界限。

      (2)段選擇子(Selector)由GDTR訪問全局描述符表是通過“段選擇子”(實模式下的段寄存器)來完成的,如圖三①步。段選擇子是一個16位的寄存器(同實模式下的段寄存器相同)

      段選擇子包括三部分:描述符索引(index)、TI、請求特權(quán)級(RPL)。他的index(描述符索引)部分表示所需要的段的描述符在描述符表的位置,由這個位置再根據(jù)在GDTR中存儲的描述符表基址就可以找到相應的描述符(如圖三①步)。然后用描述符表中的段基址加上邏輯地址(SEL:OFFSET)的OFFSET就可以轉(zhuǎn)換成線性地址(如圖三②步),段選擇子中的TI值只有一位0或1,0代表選擇子是在GDT選擇,1代表選擇子是在LDT選擇。請求特權(quán)級(RPL)則代表選擇子的特權(quán)級,共有4個特權(quán)級(0級、1級、2級、3級)。例如給出邏輯地址:21h:12345678h轉(zhuǎn)換為線性地址
      a. 選擇子SEL=21h=0000000000100 0 01b 他代表的意思是:選擇子的index=4即100b選擇GDT中的第4個描述符;TI=0代表選擇子是在GDT選擇;左后的01b代表特權(quán)級RPL=1
      b. OFFSET=12345678h若此時GDT第四個描述符中描述的段基址(Base)為11111111h,則線性地址=11111111h+12345678h=23456789h
      (3)局部描述符表LDT(Local Descriptor Table)局部描述符表可以有若干張,每個任務可以有一張。我們可以這樣理解GDT和LDT:GDT為一級描述符表,LDT為二級描述符表。如圖五

      LDT和GDT從本質(zhì)上說是相同的,只是LDT嵌套在GDT之中。LDTR記錄局部描述符表的起始位置,與GDTR不同LDTR的內(nèi)容是一個段選擇子。由于LDT本身同樣是一段內(nèi)存,也是一個段,所以它也有個描述符描述它,這個描述符就存儲在GDT中,對應這個表述符也會有一個選擇子,LDTR裝載的就是這樣一個選擇子。LDTR可以在程序中隨時改變,通過使用lldt指令。如圖五,如果裝載的是Selector 2則LDTR指向的是表LDT2。舉個例子:如果我們想在表LDT2中選擇第三個描述符所描述的段的地址12345678h。
      1. 首先需要裝載LDTR使它指向LDT2 使用指令lldt將Select2裝載到LDTR
      2. 通過邏輯地址(SEL:OFFSET)訪問時SEL的index=3代表選擇第三個描述符;TI=1代表選擇子是在LDT選擇,此時LDTR指向的是LDT2,所以是在LDT2中選擇,此時的SEL值為1Ch(二進制為11 1 00b)。OFFSET=12345678h。邏輯地址為1C:12345678h
      3. 由SEL選擇出描述符,由描述符中的基址(Base)加上OFFSET可得到線性地址,例如基址是11111111h,則線性地址=11111111h+12345678h=23456789h
      4. 此時若再想訪問LDT1中的第三個描述符,只要使用lldt指令將選擇子Selector 1裝入再執(zhí)行2、3兩步就可以了(因為此時LDTR又指向了LDT1)
      由于每個進程都有自己的一套程序段、數(shù)據(jù)段、堆棧段,有了局部描述符表則可以將每個進程的程序段、數(shù)據(jù)段、堆棧段封裝在一起,只要改變LDTR就可以實現(xiàn)對不同進程的段進行訪問。
      存儲方式是保護模式的基礎,學習他主要注意與實模式下的存儲模式的對比,總的思想就是首先通過段選擇子在描述符表中找到相應段的描述符,根據(jù)描述符中的段基址首先確定段的位置,再通過OFFSET加上段基址計算出線性地址.

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

        0條評論

        發(fā)表

        請遵守用戶 評論公約

        類似文章 更多