1. 程式人生 > >windows核心情景分析--記憶體管理

windows核心情景分析--記憶體管理

32位系統中有4GB的虛擬地址空間

每個程序有一個地址空間,共4GB,(具體分為低2GB的使用者地址空間+高2GB的核心地址空間)

各個程序的使用者地址空間不同,屬於各程序專有,核心地址空間部分則幾乎完全相同

虛擬地址如0x11111111,  看似這8個數字是一個整體,其實是由三部分組成的,是一個三維地址,將這個32位的值拆開,高10位表示二級頁表號,中間10位表示二級頁表中的頁號,最後12位表示頁內偏移(2^12=4kb),因此,一個虛擬地址實際上是一個三維地址,指明瞭本虛擬地址在哪個二級頁表,又在哪個頁以及頁內偏移是多少  這三樣資訊!

【虛擬地址 = 二級頁表號.頁號.頁內偏移】:口訣【頁表、頁號、頁偏移】

Cpu訪問實體記憶體的原理介紹:

如高階語言

DWORD  g_var;  //假設這個全域性變數被編譯器編譯為0x00000004

g_var=100; 

那麼這條賦值語句編譯後對應的彙編語句為:mov DWORD PTR[0x00000004],100

這裡0x00000004就是一個虛擬地址,簡稱VA,那麼這條mov 指令究竟是如何定址的呢?

定址過程為:CPU中的虛擬地址轉換器也即MMU,將虛擬地址0x00000004轉換為實體地址

具體轉換過程為:

根據CR3暫存器中記錄的當前程序頁表的實體地址,找到總頁表也即頁目錄,再根據虛擬地址中的頁表號,以頁表號為索引,找到總頁表中對應的PDE,再根據PDE,找到對應的二級頁表,再以虛擬地址中的頁號部分為索引,找到二級頁表中的對應PTE,再根據這個PTE記錄的對映關係,找到這個虛擬頁面對應的物理頁面,最後加上虛擬地址中的頁內偏移部分,加上這個偏移值,就得出最後的實體地址。具體用下面的函式可以形象表達定址轉換過程:

mov DWORD PTR[0x00000004],100 //這條指令的內部原理(沒考慮二級緩衝情況)

{

va=0x00000004;//頁表號=0,頁號=0,頁內偏移=4

      總頁表=CR3;  //本程序的總頁表的實體地址固定儲存在cr3暫存器中

      PDE=總頁表[va.頁表號];  //PDE為對應的二級頁表描述符

      二級頁表=PDE.PageAddr;  //得出本二級頁表的地址

      PTE=二級頁表[va.頁號];   //得出到該虛擬地址所在頁面的PTE對映描述符

      If(PTE空白)  //PTE為空表示該虛擬頁面尚未建立對映

         觸發0x0e號頁面訪問異常(具體為缺頁異常)

      Else

      If(PTE.bPresent==false) //PTE的這個欄位表示該虛擬頁面當前是否對映到了實體記憶體

         觸發0x0e號頁面訪問異常(具體為缺頁異常)

      Else

      If(CR0.wp==1  &&  PTE.Writable==false) //已開啟頁面防寫功能,就檢查這個頁面是否可寫

         觸發0x0e號頁面訪問異常(具體為頁面訪問保護越權異常)

      Else

         實體地址pa =cs.base + PTE.PageAddr + va.頁內偏移  //得出對應的實體地址

      將得到的pa放到地址總線上,100放在資料匯流排上,經由FSB->北橋->記憶體匯流排->記憶體條 寫入記憶體

}

PTE是二級頁表中的表項,記錄了對應虛擬頁面的對映情況,這個PTE實際上可以看做一個描述符。

上面的過程比較簡單,由於每次訪問記憶體都要先訪問一次PTE獲取該虛擬頁面對應的物理頁面,再訪問物理頁面讀得對應的資料,因此實際上訪問了兩次實體記憶體,如果類似於每條這樣的Mov指令都要訪問實體記憶體兩次,才能獲得資料,效率就很低。因此,cpu晶片中專門開闢了一個二級緩衝,用來儲存那些頻繁訪問的PTE,這樣,cpu每次去查物理頁面時,就先嚐試在二級緩衝中查詢對應的PTE,如果找不到,再才去訪問記憶體中的PTE。這樣,效率就比較高,實際上絕大數情況就可以在二級緩衝中一次性找到對應的PTE。

另外有一個問題需要說明下:va---->pa的轉換過程實際上是va->la->pa,實際上PTE.PageAddr表示的是相對於cs段的偏移,加上cs段的base基址,就得到了該頁面的la線性地址。

(線性地址=段.基地址 + 段內偏移),但是由於Windows採取了Flat也即所謂的平坦分段機制,使得每個段的基地址都在0x00000000處,長度為4GB,也即相當於Windows沒有采取分段機制。前面講過,cs是GDT表中的索引,指向GDT表中的cs段描述符,由於Windows不分段,因此GDT中每個段描述符的基址=0,長度=4GB,是固定的!這樣一來,由於不分段,線性地址就剛好是實體地址,所以本來是由虛擬地址->線性地址->實體地址的轉換就可以直接看做虛擬地址->實體地址。

(注:在做SSDT hook、IDT hook時,由於SSDT與IDT這兩張表各自所在的頁面都是隻讀的,也即他們的PTE中標誌位標示了該頁面不可寫。因此,一修改SSDT、IDT就會報異常,一個簡單的處理方法是是關閉CRO中的wp即防寫位,這樣就可以修改了)

前文說了,每個程序有兩個地址空間,一個使用者地址空間,一個核心地址空間,該地址空間的核心結構體定義為:

Struct  MADDRESS_SPACE  //地址空間描述符

{

   MEMORY_AREA*  MemoryRoot;//本地址空間的已分配區段表(一個AVL樹的根)

   VOID*  LowestAddress;//本地址空間的最低地址(使用者空間是0,核心空間是0x80000000)

   EPROCESS* Process;//本地址空間的所屬程序

/*一個表,表中每個元素記錄了本地址空間中各個二級頁表中的PTE個數,一旦某個二級頁表中的PTE個數減到了0,就自動釋放該二級頁面表本身,體現為稀疏陣列特徵*/

   USHORT* PageTableRefCountTable; 

   ULONG PageTableRefCountTableSize;//上面那個表的大小

}

地址空間中所有已分配的區段都記錄在一張表中,這個表不是簡單的陣列,而是一個AVL樹,用來提高查詢效率。每個區段的基址都對齊64KB或4KB(指64KB整倍數),各個區段之間可以有空隙,

區段的分佈是很零散的!各個區段之間,夾雜的空隙就是尚未分配的虛擬記憶體。

注:所謂已分配區段,是指已經過VirtualAlloc預訂(reserve)或提交(commit)後的虛擬記憶體

區段的描述符如下:

Struct  MEMORY_AREA    //區段描述符

{

   Void* StartingAddress; //開始地址,普通區段對齊64KB,其它型別區段對齊4KB

   Void* EndAddress;//結尾地址,EndAddress – StartingAddress就是該區段的大小

   MEMORY_AREA*  Parent;//AVL樹中的父節點

   MEMORY_AREA*  LeftChild;//左邊的子節點

   MEMORY_AREA*  RightChild;//右邊的子節點

//常見的區段型別有:普通型區段、檢視型區段、緩衝型區段(後面檔案系統中會講到)等

   ULONG type;//本區段的型別

   ULONG protect;//本區段的保護許可權,可讀、可寫、可執行的組合

   ULONG flags;//當初分配本區段時的分配標誌

   BOOLEAN DeleteInProgress;//本區段是否標記為了‘已刪除’

   ULONG PageOpCount;

  Union

{

    Struct //這個Struct專用於檢視型區段

    {

       //凡是含有ROS字樣的函式與結構體都表示是ReactOS與Windows中不同的實現細節

       ROS_SECTION_OBJECT*  section; 

       ULONG ViewOffest;//指本檢視型區段在所在Segment內部的偏移

       MM_SECTION_SEGMENT* Segment;//所屬Segment

       BOOLEAN WriteCopyView;//本檢視區段是不是一個寫複製區段     

    }SectionData;

LIST_ENTRY  RegionListHead;//本區段內部的所有Region區塊,放在一個連結串列中

}Data;

}//end

淺談區段型別:

MEMORY_AREA_VIRTUAL_MEMORY://普通型區段,由VirtuAlloc應用層使用者分配的區段都是普通區段

MEMORY_AREA_SECTION_VIEW://檢視型區段,用於檔案對映、共享記憶體

MEMORY_AREA_CACHE_SEGMENT://用於檔案緩衝的區段(一個簇大小)

MEMORY_AREA_PAGED_POOL://核心分頁池中的區段

MEMORY_AREA_KERNEL_STACK://用於核心棧中的區段

MEMORY_AREA_PEB_OR_TEB://用於PEB、TEB的區段

MEMORY_AREA_MDL_MAPPING://核心中專用於建立MDL對映的區段

MEMORY_AREA_CONTINUOUS_MEMORY://對應的物理頁面也連續的區段

MEMORY_AREA_IO_MAPPING://核心空間中用於對映外設記憶體(如視訊記憶體)的區段

MEMORY_AREA_SHARED_DATA://核心空間中用於與使用者空間共享的區段

Struct  MM_REGION  //區塊描述符

{

   ULONG type;//指本區塊的分配型別(預定型分配、提交型分配),又叫對映狀態(已對映、尚未對映)

   ULONG protect;//本區塊的訪問保護許可權,可讀、可寫、可執行的組合

   ULONG length;//區塊長度,對齊頁面大小(4KB)

   LIST_ENTRY RegionListEntry;//用來掛入所在區段的區塊連結串列

}

記憶體以區段為分配單位,一個區段內部,又按分配型別、保護屬性劃分區塊。一個區塊包含一到多個記憶體頁面,分配型別相同並且保護許可權相同的區域組成一個個的區塊,因此,稱為“同屬性區塊”。一個區段內部,相鄰區塊之間的屬性肯定是不相同的(分配型別或保護許可權不同),若兩個相鄰區塊的屬性相同了,會自動合併成一個新的區塊。

程序,地址空間,區段,區塊,頁面的邏輯層次關係

一個虛擬頁面實際上有五級限定:

【程序.地址空間.區段.區塊.虛擬頁面】

意為:哪個程序的哪個地址空間中的哪個區段中的哪個區塊中的哪個虛擬頁面

MEMORY_AREA*   MmLocateMemoryAreaByAddress(MADDRESS_SPACE* as, void* addr);

這個核心函式用於在指定地址空間中查詢指定地址所屬的已分配區段,如果返回NULL,表示該地址尚不處於任何已分配區段中,也即表示該地址尚未分配。

Void*

MmFindGap(MADDRESS_SPACE* as, ULONG len, ULONG AlignGranularity,  BOOL TopDown)

這個函式在指定地址空間中 查詢一塊符合len長度的空閒(也即未分配)區域,返回找到的空閒區的地址,AlignGranularity表示該空白區必須的對齊粒度,TopDown表示是否從高地址端向低地址端搜尋

MEMORY_AREA*

MmLocateMemoryAreaByRegion(MADDRESS_SPACE* as, void* addr, ULONG len)

這個函式從指定地址空間的低地址端向高地址段搜尋,返回第一個與給點區間(addr,len)有交集的已分配區段

NTSTATUS

MmCreateMemoryArea(MADDRESS_SPACE* as, type,  void** BaseAddr,  Len,  protect, bFixedAddr, AllocFlags,   MEMORY_AREA**  Result)

{

Len=Align(Len,4kb);//區段長度都要對齊4kb

   UINT BaseAlign;//區段的基址對齊粒度

   If(type==普通區段)

      BaseAlign=64KB;

   Else

      BaseAlign =4KB; 

   If(*BaseAddr ==NULL  &&  !bFixedAddr)//if 使用者不要求從固定地址處開始分配

   {

     *BaseAddr=MmFindGap(as,Len, BaseAlign,  AllocFlags要求TopDown?);

   }

   Else//else只要使用者給定了基址,就必須從那兒開始分配

   {

       *BaseAddr=Align(*BaseAddr, BaseAlign);

        If(要分配的區域沒有完全落在指定地址空間內部)

           Return fail;

        If(MmLocateMemoryAreaByRegion(as,*BaseAddr,Len)!=0)//if 這段範圍已分配過

           Return fail;    

   }

   //找到了一個空閒區域後/指定的地址滿足分配要求,就把這塊區域分配出去

   Memory_Area* Area=ExAllocatePool(NonPagePool, sizeof(*Area),tag);

   ZeroMemory(Area);

   Area.type=type;//本區段的初始分配型別(初始時,一個區段內部就一個區塊)

   Area.StartAddr=*BaseAddr;

   Area.EndAddr=*BaseAddr+Len;

   Area.protect=protect;//本區段的初始保護屬性

   Area.flags=Allocflags;

   MmInsertMemoryArea(as,Area);//分配後插入地址空間中的已分配區段表中(AVL樹)

   *Result=Area;

   Return succ;

}

上面這個函式用來從指定地址或者讓系統自動尋找一塊空閒的區域,分配一塊指定長度、型別的區段。所謂分配,包含reserve型分配(即預定型分配),和commit型分配(即提交型分配)

預定:只佔用分配一塊區段,不建立對映

提交:分配一塊區段並建立對映(對映到磁碟頁檔案/實體記憶體頁面/普通檔案)                                                                      

MM_REGION*

MmFindRegion(void* AreaBaseAddr,  LIST_ENTRY*  RegionListHead,  void* TgtAddr,

Void** RegionBaseAddr)

這個函式從指定區段的區塊連結串列中,查詢給定目標地址TgtAddr落在哪一個區塊內

第一個引數表示區段的基址。函式返回找到的區段並順便將該區段的基址也存入最後一個引數中返回給呼叫者

MM_REGION*

MmSplitRegion(MM_REGION* rgn, BaseAddr,    StartAddr,Len,  NewType,NewProtect

AlterFunc)

這個函式將指定區塊內部的指定區域(StartAddr,Len)修改為新的分配型別、保護屬性,使原區塊分裂,一分為三(特殊情況一分為二),然後呼叫AlterFunc跟著修改二級頁表中,新區塊的那些PTE,最後再跟著修改物理頁面分配情況。函式返回新分出來的那個中間區塊。這是一個內部輔助函式。

NTSTATUS

MmAlterRegion(AreaBaseAddr, RegionListHead,   TgtAddr,Len,   NewType,NewProtect, AlterFunc)

這個函式是個通用函式,用來修改指定區段內部的指定區域的分配型別、保護屬性,然後

呼叫呼叫AlterFunc跟著修改二級頁表中,目標區域對應的那些PTE,最後再跟著修改物理

頁面的分配情況。

實體記憶體講述:

核心中有一個全域性的物理頁面陣列,和7個物理頁面連結串列。分別是:

PHYSICAL_PAGE  MmPageArray[];//實體記憶體有多大,該陣列就有多大

LIST_ENTRY  FreeZeroedPageListHead;//空閒物理頁面連結串列(且物理頁面已清0)

LIST_ENTRY  FreeUnzeroedPageListHead;//空閒物理頁面連結串列(但物理頁面尚未清0)

LIST_ENTRY  UsedPageListHeads[4];//細分為4大消費用途的忙碌物理頁面連結串列,各連結串列中按LRU順序

LIST_ENTRY  BiosPageListHead;//用於Bios的物理頁面連結串列

物理頁面陣列是一個物理頁面描述符陣列,每個元素描述對應的物理頁面(陣列索引號即

物理頁號,又叫pfn),每個描述符是一個PHYSICAL_PAGE結構體

Struct  PHYSICAL_PAGE  //物理頁面描述

{

   Type ;//該物理頁面的空閒佔用狀態(1表示空閒,2表示已佔用,3表示分給了BIOS)

   Consumer;//該物理頁面的消費用途(使用者/核心分頁池/核心非分頁池/檔案緩衝 四種)

   Zero;//標誌本頁面是否已清0

   ListEntry;//用來掛入那7個連結串列之一

   ReferenceCount;//引用計數,一旦減到0,頁面就變為空閒狀態,進入空閒連結串列

   SWAPENTRY  SavedSwapEntry;//對應的來源頁檔案,用於置換,一般為空 

LockCount;//本物理頁面的鎖定計數(物理頁面可鎖定在記憶體中,不許置換到外存)

MapCount;//同一個物理頁面可以對映到N個程序的N個虛擬頁面

MM_RMAP_ENTRY*  RmapListHead;//本物理頁面對映給的那些虛擬頁面,組成的連結串列  

}

一個物理頁面的典型狀態轉換過程為:

起初處於空閒並清0的狀態,然後應記憶體分配要求分配給4個消費者之一,同時,將該物理

頁面記錄到對應消費者的UsedPageListHead連結串列中,最後使用者用完後主動釋放,或者因為物

理記憶體緊張,被迫釋放換到外存,而重新進入空閒狀態,但此時尚未清0,將進入

FreeUnzeroedPageList連結串列。然後,核心中有一個守護執行緒會定時、週期掃描這個空閒連結串列,

將實體記憶體清0,轉入FreeZeroedPageList連結串列,等候下次被分配。如此周而復返…

PFN_NUMBER

MmAllocPage(ULONG ConsumerType)

{

   PFN_NUMBER Pfn;//物理頁號

   PPHYSICAL_PAGE PageDescriptor;

   BOOLEAN NeedClear = FALSE;//是否需要清零

   if (FreeZeroedPageList連結串列 為空)

   {

       if (FreeUnzeroedPageList 為空)

          return 0;//如果兩個空閒連結串列都為空就失敗

       PageDescriptor = MiRemoveHeadList(&MmFreePageListHead);

       NeedClear = TRUE;

   }

   else

      PageDescriptor = MiRemoveHeadList(&MmZeroedPageListHead);

   //從空閒連結串列中摘下來一個空閒頁面後,初始化

MmAvailablePages--;//總的可用物理頁數--

   PageDescriptor->ReferenceCount = 1;//剛分配的物理頁面的引用計數為1

   PageDescriptor->LockCount=0;//表示可被置換到外存

   PageDescriptor->MapCount=0;//表示剛分配的物理頁面尚未對映到任何虛擬頁面

   //記錄到分配連結串列中

   InserTailList(&UsedPageListHeads[ConsumerType], PageDescriptor);

   if (NeedClear)

      MiZeroPage(PfnOffset);//清0

Pfn = PageDescriptor-MmPageArray;//pfn=陣列的索引,就是物理頁號

   return Pfn;

}

這段函式為指定消費者分配一個物理頁面,並第一時間將物理頁面清0.然後返回分得的物理頁號

NTSTATUS

MmRequestPageMemoryConsumer(consumer, PFN* pfn)

{

   //先檢查物理頁面配額,超出配額,就自我修剪

   If(本消費者的分得的物理頁面數量 = 本消費者的最大配額)

   {

//換出那個消費者的某個物理頁面到外存,騰出一個物理頁面出來

      Call 對應消費者的自我頁面修剪函式   

   }

   If(當前系統總的空閒頁面總量 < 儲備閥值)

   {

      If(consumer==非分頁池消費者)

      {

         *pfn = MmAllocPage(consumer);

//分完後喚醒系統中的平衡執行緒去平衡物理頁面,填補空白

         KeSetEvent(&MiBalancerEvent); 

         Return succ;

      }

      Else

      {

         *pfn = 請求平衡執行緒趕緊從其他消費者手中退出一個物理頁面;

          Return succ;

      }

   }

   Else

     *pfn = MmAllocPage(consumer);

}

這個函式,先檢查配額,再檢查空閒頁面閥值,做好準備工作後,再才分配物理頁面

NTSTATUS  MmReleasePageMemory(consumer, pfn)

{

   Consumer.UsedPageCount--;//遞減本消費者持有的頁面計數;

   pfn.ReferenceCount--;//遞減本頁面的引用計數

   If(pfn.ReferenceCount==0)

   {

      If(有其他分配請求正在等待退讓物理頁面)

         將這個pfn分給那個Pending中的分配請求

      Else

         將這個頁面掛入系統 FreeUnzeroedPageList 連結串列;

   }

}

這個函式釋放指定消費者佔用的指定物理頁面,實際上是遞減引用計數,引用計數減到0後就掛入系統空閒連結串列

虛擬頁面與物理頁面之間的對映:

一個物理頁面可以對映到N個程序的N個虛擬頁面中,但一個虛擬頁面同一時刻只能對映到一個物理頁面。可以這麼理解:“一個物理頁面當前可能被N個虛擬頁面對映著”,“本虛擬頁面當前對映著一個物理頁面”。

每個虛擬頁面又分四種對映狀態:

1、 對映著某個物理頁面(已分配且已對映)

2、 對映著某個磁碟頁檔案中的某個頁面(已分配且已對映)

3、 沒對映到任何物理儲存介質(對應的PTE=0),但是可能被預定了(已分配,但尚未對映)

4、 裸奔(尚未分配,以上情況都不滿足)

一個程序的使用者地址空間高達2GB,分成很多虛擬頁面,如果時時刻刻都讓這些虛擬頁面對映著實體記憶體,那麼實體記憶體恐怕很快就分完了。所以,同一時刻,只有最頻繁訪問的那些虛擬頁面對映著物理頁面(最頻繁訪問的那些虛擬頁面就構成了一個程序的工作集),工作集中的所有虛擬頁面都對映著物理頁面,一旦訪問工作集外面的虛擬頁面,勢必引發缺頁異常,系統的缺頁異常處理函式會自動幫我們處理這種異常(自動分配一個物理頁面,將那個引發缺頁異常的虛擬頁面對映著的外存頁面 以分頁讀irp的形式讀入到 新分配的物理頁面中,然後修改那個虛擬頁面的對映關係,指向那個新分配的物理頁面),這就是系統的缺頁異常處理函式的工作原理,應用程式毫不知情。

漫談頁目錄、二級頁表:

前面講到每個虛擬地址看似是一個整形值,實際上由三部分組成:頁表號.頁號.頁內偏移,為什麼不是直接的頁號.頁內偏移呢,直接採用一個簡單的一維陣列,記錄所有虛擬頁面的這樣多直觀!原因是:一個程序的虛擬地址空間太大,如果為每個虛擬頁面都分配一個PTE,那麼將佔用大量記憶體,不信我們算一下:

一個程序中總共有4GB/4KB=2^20個虛擬頁面,也即1MB個虛擬頁面,如果直接採用一維陣列,描述這個1MB頁面的對映情況,那麼整個陣列大小=1MB*sizeof(PTE)=4MB,這樣,光頁表部分就佔據了4MB的記憶體。注意頁表部分本身佔用的記憶體是非分頁記憶體,也即真金白銀地佔據著4MB實體記憶體,這4MB在現在的機器看來,並不算多,但在早期只有256MB實體記憶體的老爺機上(最多隻能同時支援256MB/4MB個=64個程序),已經算夠多了!

相反,如果採用頁目錄+二級頁表的方式就會節省很多記憶體!

一個二級頁表本身有一個頁面大小,可以容納4KB/sizeof(PTE)=1024 個PTE,換句話說,一個二級頁表可以描述1024個頁面的對映情況(換算成位元組數,一個二級頁面能描述1024*4kb的地址空間),一個程序總共有4GB地址空間,那麼整個地址空間就有4GB/(1024*4kb)=1024個二級頁表,那些暫時未對映的一大片虛擬地址,一般是高階的地址,就對應這1024個二級頁表中靠後的那些二級頁表,就可以暫時不用為他們分配實體記憶體了, 只有在確實要訪問那些虛擬頁面時,才分配他們對應的二級頁表,這樣按需分配,就節省了實體記憶體。

另外,32位系統中每個程序有1024個二級頁表外加一個頁目錄。咋一看,似乎系統中有1025個頁表維持著對映關係,其實不然,因為頁目錄本身是一個特殊的二級頁表,也是那1024個二級頁表中的一個。概念上,我們把第一個二級頁表理解為頁目錄。這樣,系統中實際上共有1024個二級頁表(包括頁目錄本身在內,但要注意頁目錄並不在二級頁表區的中的第一個位置,而是在中間的某個位置,後面我會推算頁目錄本身的虛擬地址在什麼地方)。明白了這個道理,就可以由任意一個虛擬地址推算出他所在的二級頁表在頁目錄中的索引位置。

#define  ADDR_TO_PDE_OFFSET(addr)    (  v/(1024*4kb)  )

#define  ADDR_TO_PAGE_TABLE(addr)   ADDR_TO_PDE_OFFSET(addr)

這樣,每個程序的頁表不再是個簡單的陣列,而變成了一個稀疏陣列。

頁目錄中的每個PDE描述了每個二級頁表本身的實體地址。如果PDE=0,就表示那個二級頁表尚未分配,體現為‘稀疏陣列’特徵。實際上,一個程序很少使用到整個4GB地址空間,因此,頁目錄中的絕大多數PDE都是空的,實際的二級頁面個數往往很少。

每個虛擬頁面的對映描述符(即PTE)的位置是固定的,根據虛擬頁號可以自然算出那個虛擬頁面的對映描述符位置,找到對映描述符的位置後,就可以獲得該虛擬頁面的當前對映情況(是否已對映,若已對映,是對映到了實體記憶體還是頁檔案,又具體對映到了哪個具體的物理頁面,這些資訊都一一獲悉),因此PTE對映描述符是頁表的核心,現在看一下PTE它的結構。

PTE的結構,PTE是二級頁表中的表項,用來描述一個虛擬頁面的對映情況以及其他資訊

注意PTE本身長度為4B,但我們可以把它當做一個描述符結構體,並不妨礙理解

Struct  PTE

{

Union

{

  Struct

  {

      Bool  bPresent;//重點欄位,表示該虛擬頁面是否對映到了實體記憶體

      Bool  bWritable;//表示這個虛擬頁面是否可寫

      Bool  bUser;//表示是否是使用者地址空間中的虛擬頁面

      Bool  bReaded;//表示本虛擬頁面自從上次置換到記憶體後是否曾被讀過

      Bool  bDirty;//表示本虛擬頁面自從上次置換到記憶體後是否曾被寫過

      Bool  bGlobal;//表示本PTE表項是全域性頁面的對映描述符,切換程序時不用重新整理本PTE

      UINT  pfn;//關鍵欄位,表示本虛擬頁面對應的物理頁號 

}Mem;

Struct

{

   檔案中的頁面號;

   頁檔案號;//系統中可以支援多個Pagefile.sys頁檔案

}File;

  } 

}

這樣,這個PTE如果對映到了記憶體,就解釋為Mem結構體,如果對映到了頁檔案,就解釋為File結構體。 

NTSTATUS 

MmCreateVirtualMapping(process,  FirstVirtualPageAddr,  VirtualPageCount,

PfnArray,          PfnCount,   PteFlags)

{

   If(VirtualPageCount != PfnCount )

      Return  fail;

   DWORD  NewPTE=ConstructPte(PteFlags);//拷貝PTE中的那些Bool標誌位

   Void* CurPageAddr = FirstVirtualPageAddr;//當前虛擬頁面的地址

   PTE* Pt;//當前虛擬頁面的PTE在二級頁表中對應的位置

   For(int i=0; i< VirtualPageCount;i++)//遍歷每個要建立對映的虛擬頁面

   {

       //這個函式下文有解析

       Pt = MmGetPageTableForProcess(process, CurPageAddr);//找到這個頁面的pte位置 

       OldPte = *Pt;//記錄這個虛擬頁面原來的PTE

       If(OldPte對映到了頁檔案)

    return fail;

       If(OldPte對映到了實體記憶體) 

 撤銷原來的對映;

       NewPTE.pfn = PfnArray[i];//關鍵,將這個虛擬頁面對映到指定的物理頁面

       *pt = NewPTE;//修改原PTE表項

       //遞增對應二級頁表中的PTE個數,這個函式其實是建立PTE,不是修改PTE

       Process.地址空間.PageTableRefCountTable[ ADDR_TO_PAGE_TABLE(CurPageAddr) ]++;

       If(OldPte對映到了某實體記憶體頁面)

          MiFlushTlb(pt, CurPageAddr);//同步更新cpu二級緩衝中的PTE

       CurPageAddr+=4KB;//下一個虛擬頁面

   }

}

如上,這個函式用來為指定的一段連續虛擬頁面,批量建立PTE,建立到各個物理頁面的對映。注意虛擬頁面一定是連續的,物理頁面陣列中的物理頁面是可以零散分佈的。

MmDeleteVirtualMapping(process, PageAddr,  bFreePhysicalPage,  BOOL* bDirty, PFN* pfn)

{

   PTE* pt= MmGetPageTableForProcess(process, CurPageAddr);//找到這個頁面的pte位置 

   PTE OldPte=*pt;

   *pt=0;//全0就表示刪除PTE

/*注意,一個物理頁面可能還可能被其他虛擬頁面對映著,應該等到該物理頁面的MapCount減到0時才釋放這個物理頁面*/

If(bFreePhysicalPage)

       MmReleasePageMemoryConsumer(pfn); 

//遞減對應二級頁表中的PTE個數

Process.地址空間.PageTableRefCountTable[ ADDR_TO_PAGE_TABLE(CurPageAddr) ] --;

If(Process.地址空間.PageTableRefCountTable[ ADDR_TO_PAGE_TABLE(CurPageAddr) ]  = 0)

    MmReleasePageTable(process,PageAddr);//釋放對應的整個二級頁表,體現稀疏陣列特徵

*bDirty=OldPte.bDirty;

*pfn=OldPte.pfn;//返回原來對映的物理頁面號

}

Windows中,不管是應用程式還是核心程式,都不能直接訪問實體記憶體,如Mov eax,DWORD PTR[實體地址],

是不允許的,不支援的。所有非IO指令都只能訪問虛擬記憶體地址,如Mov eax, DWORD PTR[虛擬地址]形式,但是,有時候,我們明明已經知道了某個東西固定在實體記憶體條某處,假如系統時間的值永遠固定存放在實體記憶體條的實體地址0x80000000處,我們已經知道了實體地址,如何訪問獲得系統時間值呢?這是個問題!Windows為了解決這樣的直接訪問實體記憶體操作提供了手段!其中之一便是:“為物理頁面建立臨時對映”,也即可以將某個物理頁面對映到系統地址空間中的那段專用於臨時頁面對映的保留區域。

具體的:系統地址空間中專用於臨時對映的那段保留區的起始虛擬地址為:

#define  HYPERSPACE   0xC0400000

保留區的大小為:1024個虛擬頁面,也即1024*4KB=4MB大小

下面這個函式用來將指定物理頁面 臨時 對映到保留區中的某個虛擬頁面,返回得到的虛擬頁面地址

Void*  MmCreateHyperspaceMapping(pfn)

{

   PTE* Pte=臨時對映保留區的對映描述符們所在的二級頁表;//也即第一個臨時頁面的對映描述符

   Pte+=pfn%1024;//從這個虛擬頁面的對映描述符開始,向後搜尋第一個尚未對映的虛擬頁面

   For(i=pfn%1024; i<1024; i++,Pte++)//先遍歷後面的那些PTE

   {

      If(*pte == 空白)

      {

        *pte.pfn=pfn;

        Break;

      }  

   }

   If(i==1024)//如果後面部分未找到一個空閒PTE,又從前面部分開始查詢

   {

      PTE* Pte=臨時對映保留區的對映描述符們所在的二級頁表;//回到開頭

      For(i=0; i<pfn%1024;i++,Pte++)

      {

         If(*pte == 空白)

         {

            *pte.pfn=pfn;

            Break;

         }

      }//end for

   }//end if(i==1024)

   //上面是一個簡單的閉式hash表的查詢過程,找到一個尚未對映的臨時保留虛擬頁面後,就返回

   Return HYPERSPACE + i*4kb;

}

既然叫臨時對映,那用完後,就得撤銷對映

MmCreateHyperspaceMapping(pfn);//這個函式就是用來刪除以前建立的臨時對映,省略

要想查詢一個虛擬頁面的對映情況(有沒有對映,有的話,又對映到了什麼地方  這些資訊),唯一的辦法就是要找到這個虛擬頁面的PTE對映描述符,那麼如何查詢呢?

#define PAGETABLE_MAP  0xC0000000

如前文所述,每個程序的頁表區都真槍實彈的佔據著對應的實體記憶體,系統為了方便,把每個程序的頁表區都事先固定對映到了虛擬地址0xC0000000處,長度為1024個頁表 * 每個頁表本身的大小(即4KB)=4MB。因此,各個程序的頁表區也都是被系統對映到了同一段虛擬空間(0xC0000000---0xC0000000+4MB)處。

這段區域用來對映二級頁表們

整個4GB空間的佈局:

使用者空間  系統空間開頭  二級頁表 二級頁表 … 頁目錄  … 二級頁表  二級頁表 系統空間結尾

#define PAGEDIR_MAP  (PAGETABLE_MAP + PAGETABLE_MAP/1024)

PAGEDIR_MAP表示頁目錄本身所在的虛擬頁面的地址,這是怎麼得來的呢?是經過下面這樣推算出來的

PAGEDIR_MAP= PAGETABLE_MAP + idx*每個二級頁面的大小

           = PAGETABLE_MAP + idx*4kb

           = PAGETABLE_MAP + (PAGETABLE_MAP偏移/每個二級頁表描述的長度範圍) * 4kb

           = PAGETABLE_MAP + (PAGETABLE_MAP/(1024*4kb)) * 4kb

           = PAGETABLE_MAP + PAGETABLE_MAP/1024

因此,只要知道了頁表區中第一個二級頁面的虛擬地址,就可以推算出頁目錄本身的虛擬地址

進一步:

#define ADDR_TO_PDE(PageAddr)  PAGEDIR_MAP + PageAddr/(1024*1024)  //直接推算PDE的地址

#define ADDR_TO_PTE(PageAddr)  PAGETABLE_MAP + PageAddr/1024  //直接推算PTE的地址

這兩個巨集我就不想多說了

下面這個函式用來找到指定程序中的指定虛擬頁面的對映描述符位置:

PTE* MmGetPageTableForProcess(process, PageAddr)

{

ULONG PDE_IDX = ADDR_TO_PDE_OFFSET(PageAddr);//計算該虛擬頁面的對映描述符在哪個二級頁表中

PDE* PageDir;//頁目錄的虛擬地址

If(process!=當前程序 && PageAddr<2GB)//if PageAddr是其他程序的使用者空間中的某個虛擬頁面

{

    PFN pfn=process.pcb.DirectoryTableBase;//獲得那個程序的頁目錄所在的物理頁面號

    PageDir=MmCreateHyperspaceMapping(pfn);//臨時對映那個物理頁面,以便訪問它的頁表

    If(PageDir[PDE_IDX]==空白)

       Return NULL;//若整個二級頁面尚未分配,返回NULL

    Pfn= PageDir[PDE_IDX].pfn;//獲得二級頁表所在的物理頁號

    MmDeleteHyperspaceMapping(PageDir);//不用在訪問頁目錄了,撤銷臨時對映

    PTE* pte= MmCreateHyperspaceMapping(Pfn);//再臨時對映二級頁表本身,以便訪問它

    Return pte+ADDR_TO_PTE_OFFSET(PageAddr);//OK,返回那個頁面的對映描述符

}

Else//反之,若那個虛擬頁面就在本程序的使用者地址空間或公共的系統地址空間中,就直接推算頁目錄的虛擬地址,免得為其建立臨時對映,以提高效率

{

    PageDir=ADDR_TO_PDE(PageAddr);//直接推算的頁目錄的虛擬地址

    If(PageDir[PDE_IDX]==空白)

       Return NULL;//若整個二級頁面尚未分配,返回NULL

    Return ADDR_TO_PTE(PageAddr);//直接推算得到這個虛擬頁面的對映描述符地址    

}

}

前面說過,各個程序的使用者地址空間是私有的,各不相同的,核心地址空間部分則幾乎完全相同,為什麼是幾乎呢,而不是全部呢?那就是因為核心地址空間中,每個程序的二級頁表區和臨時對映區,沒有對映到相同的物理頁面。

MmUpdatePageDir(process, KernePagelAddr,PageCount)

每當核心地址空間中的某組頁面的對映發生變化,系統就會呼叫這個函式將核心地址空間中從KernePagelAddr開始的一組核心虛擬頁面,從系統的公共核心頁表中同步複製這些頁面的PTE到各個程序的對應頁表中,這樣,就使得每個程序的核心頁面對映都相同,落到同一個物理頁面或者檔案頁面中。

但是,系統絕不會同步修改各個程序的二級頁表區和臨時對映區中那些虛擬頁面的對映描述符,因為那部分虛擬頁面由每個程序自己單獨維護對映,各不相同。

也即每個程序的核心頁表部分都copy自系統,使用者頁表部分各不相同。

綜上:【各個程序的使用者地址空間各不相同,核心地址空間相同,但頁表區、臨時對映區除外】

下面看一下普通的記憶體分配流程:

Void*  Kernel32.VirtualAlloc(void* BaseAddr, Len, AllocType, protect)

{

   Void* addr=BaseAddr;

   NTDLL.NtVirtualAlloc(&addr, Len, AllocType, protect)

   {

       Mov eax,服務號

       Lea edx,[esp+4] //記錄使用者區引數地址

       Sysenter

       --------------------使用者模式與核心模式分界線-----------------------

          KiFastCallEntry()

          {

             …

             NtAllocateVirtualMemory(hCurProcess,&BaseAddr, &Len, AllocType, protect);

             ….

             Sysexit

          }

       Return  status;

   }

Return addr;

}

如上,應用層的這個API呼叫核心服務函式,從指定程序的使用者空間中分配一塊指定特徵的區段,最後返回區段的地址。

核心中的服務函式如下:

NTSTATUS 

NtAllocateVirtualMemory(hProcess, void** BaseAddr, int* Len, AllocType, protect)

{

  If(引數不合法)//一般SST中的核心服務函式入口處都會對使用者空間傳下來的引數進行合法性檢查

      Return fail;

  *BaseAddr=Align(*BaseAddr,64kb);

  *Len=Align(*Len,4kb);

  EPROCESS* process;//該程序物件的核心結構

  ObReferenceObjectByHandle(hProcess,PROCESS_VM_OPERATION,UserMode,&process,…)//獲得物件

  Type=(AllocType & MEM_COMMIT)?MEM_COMMIT:MEM_RESERVE;//提交型分配或預定型分配

  MADDRESS_SPACE* As = process->VadRoot;//VadRoot表示該程序的使用者地址空間

  If(*BaseAddr!=NULL)//if 使用者給定了分配的起始地址,必須從那兒分配

  {

     MEMORY_AREA* Area=MmLocateMemoryAreaByAddress(As,*BaseAddr);

     If(Area!=NULL)//如果該地址落在事先已經分配的某個區段中

     {

        AreaLen=Area->EndAddress – Area->StartingAddress;

//如果使用者要求分配的這塊區域完全落在那個已分配區段中,就修改分配型別、保護屬性

//然後呼叫AlterFunc執行合併、拆分、修改頁面對映等相關工作

        If(AreaLen >= *Len) 

        {

           MmAlterRegion(As,Area->StratingAddr, Area->區塊連結串列, *BaseAddr,*Len,Type,protect

AlterFunc=MmModifyAttributes);

           Return succ;

        }

        Else

          Return fail;

     }//end If(Area!=NULL)

  }//end if(*BaseAddr!=NULL)

  //若使用者沒指定地址,或者即使指定了地址,但那個地址尚未落入任何已分配區段中,就分配區段

  MmCreateMemoryArea(As,普通型區段,BaseAddr,Len,protect,…);

  MmInitializeRegion(Area);//每個區段初始分配時,內部就初始化為:包含一個區塊。

  Return succ;   

}//end func

注意,上面函式分配的區段尚未建立對映,既沒有對映到實體記憶體,也沒有對映到頁檔案,但是,該區段已經分配,會被記錄到地址空間的已分配區段表中(AVL樹).由於尚未對映,此時該區段中各個頁面的PTE對映描述符是空的,cpu一訪問這個頁面就會引發缺頁異常

頁面訪問異常:

當cpu訪問一個虛擬頁面時,如果:

1、 該虛擬頁面尚未對映到物理頁面,觸發典型的0x0e號缺頁異常

2、 該虛擬頁面對映著了某個物理頁面,但是讀寫訪問許可權不匹配,觸發0x0e越權異常

不管是缺頁異常還是越權異常,都叫頁面訪問異常。一旦發生異常,cpu自動從當前cpu的IDT[異常號]位置找到對應的異常處理函式(簡稱epr),epr最終將呼叫MmAccessFault函式處理該異常

注意發生異常時,cpu還會自動把具體的異常原因號(非異常號)壓入核心棧幀中,然後跳轉到對應的epr,該epr是_KiTrap14函式,該epr在內部構造好異常Trap幀後(也即儲存暫存器現場),Jmp到KiTrap0EHandler異常處理函式,這個函式從CR2暫存器讀取發生異常的記憶體單元地址,然後呼叫下面的函式

{

   …

   //異常碼的最低位表示是因缺頁引起的異常還是防寫引起的異常

Status = MmAccessFault(TrapFrame->ErrCode & 1, Cr2,TrapFrame->SegCs & MODE_MASK, TrapFrame);

   …

}

NTSTATUS  MmAccessFault(bool bProtect, MemoryAddr, Mode, void* TrapInfo)

{

   If(bProtect)

      Return MmpAccessFault(Mode, MemoryAddr, TrapInfo?TRUE:FALSE);

   Else

      Return MmNotPresentFault(Mode, MemoryAddr, TrapInfo?TRUE:FALSE);     

}

bProtect表示是越權引起的異常還是缺頁引起的異常,MemoryAddr表示訪問的記憶體單元地址,Mode表示該指令位於哪個模式空間

看缺頁異常是怎麼處理的:

NTSTATUS

MmNotPresentFault(Mode,Address)

{

   MADDRESS_SPACE AddressSpace;

   If(Mode==KernelMode)

      AddressSpace =MmGetKernelAddressSpace();

   Else

      AddressSpace =當前程序的使用者地址空間;

   do

   {

      MemoryArea = MmLocateMemoryAreaByAddress(AddressSpace, Address);

      //如果一個頁面尚未對映,那麼它的PTE==0,這種情況引發缺頁異常,如果該地址落在了一個已經分配的區段中,那麼MemoryArea不會為NULL,否則MemoryArea為NULL。如果既未建立對映,也未分配,就不會進行缺頁置換處理,而是直接返回失敗,丟擲Win32異常,通知上層應用程式去處理 。相反,如果已經分配過了,並且分配型別是commit,就由系統自己進行缺頁處理,調入頁面。

      if (MemoryArea == NULL || MemoryArea->DeleteInProgress)

          return (STATUS_ACCESS_VIOLATION);

      switch (MemoryArea->Type)

      {

         case MEMORY_AREA_PAGED_POOL://分頁池中的區段

              Status = MmCommitPagedPoolAddress(Address);

              break;

         case MEMORY_AREA_SECTION_VIEW://檢視型區段

              Status = MmNotPresentFaultSectionView(AddressSpace,MemoryArea,Address);

              break;

         case MEMORY_AREA_VIRTUAL_MEMORY://普通型區段

              Status = MmNotPresentFaultVirtualMemory(AddressSpace,MemoryArea,Address);

              break;

       }

   }while (Status == STATUS_MM_RESTART_OPERATION);

}

如上,只有這幾種區段中的頁面才有可能被置換到外存去,各種型別的區段的缺頁處理都不同,我們看典型的普通型區段的缺頁處理:

NTSTATUS 

MmNotPresentFaultVirtualMemory(AddressSpace,MemoryArea,Address)

{

   NTSTATUS win32ExcepCode;//由cpu異常碼轉換後的win32異常碼

   Region=MmFindRegion(MemoryArea->StratinngAddress, MemoryArea->區塊連結串列,Address);

   If(Region->Type==MEM_RESERVE || Region->Protect == PAGE_NO_ACCESS)

   {

      win32ExcepCode==STATUS_ACCESS_VIOLATION;

      return win32ExcepCode;

   }

   If(當前正有其他執行緒在處理這個頁面異常,正在做置換工作)

      等待那個執行緒處理完缺頁異常,return succ;

   MmRequestPageMemoryConsumer(USER,&pfn);//分配一個空閒物理頁面

   If(MmIsPageSwapEntry(Address))//if 這個虛擬地址所在的虛擬頁面對映到了外存

   {

      MmDeletePageFileMapping(Address,&SwapEntry);//返回原對映的那個外存頁面,然後刪除原對映

      MmReadFromSwapPage();//將外存頁面讀入新分配的物理頁面中

      Pfn.SavedSwapEntry=SwapEntry;//記錄本物理頁面,當初是從這個頁檔案調入的

   }

   //建立對映,將該虛擬頁面改對映到新分配的物理頁面

   MmCreateVirtualMapping(AddressSpace->process, Address, Region->Protect, &pfn陣列,1個元素)

//將這個虛擬頁面插入那個物理頁面的對映連結串列中(多個虛擬頁面可對映到同一物理頁面)

   MmInsertRmap(pfn, AddressSpace->process,Align(Address,4kb));

Return  succ;

}

NTSTATUS  MmReadFromSwapEntry(SwapEntry,pfn)

{

   MDL mdl;

   …

   MmBuildMdlFromPages(mdl,pfn);//將物理頁面pfn對映到系統的mdl對映區中

   FileNo=SwapEntry.FileNo;

   FileOffset=SwapEntry.PageNo * 4kb;

   //這個函式內部會構造一個分頁讀irp發往檔案系統,最後發給磁碟驅動,讀入頁檔案中對應的頁面

   Status=IoPageRead(PagingFileList[FileNo]->FileObject, FileOffset,mdl,…);//讀入到物理頁面

   if (Status == STATUS_PENDING)

   {

      KeWaitForSingleObject(&Event, Executive, KernelMode, FALSE,

 NULL //看到沒,Timeout引數=NULL,表示一直等到磁碟頁面讀入完成

);

      Status = Iosb.Status;

   }

   …

Return status;

}

由於涉及到磁碟I/O,因此,置換過程有點耗時!頻繁的缺頁異常往往會造成系統性能瓶頸,這就是時間換空間帶來的副作用。

另外:由於MmReadFromSwapEntry這個函式會在內部呼叫KeWaitForSingleObject一直等到頁面讀入到記憶體後才返回原處,繼續執行。但是KeWaitForSingleObject這個函式,如果是要等待的話,只能執行在DISPATCH_LEVEL irql以下,否則,藍屏。這就是為什麼在DISPATCH_LEVEL及其以上irql時,千萬不能訪問分頁記憶體。因為分頁記憶體可能在磁碟中,這樣,一觸發缺頁中斷,在這個irql嘗試去讀取磁碟頁面時,就會因為KeWaitForSingleObject的問題而崩潰。

【換言之,根源是DISPATCH中斷級的程式碼不能呼叫KeWaitForSingleObject等待任意物件】

下面引自DDK原話:“Callers of KeWaitForSingleObject must be running at IRQL <= DISPATCH_LEVEL. However, if Timeout = NULL or *Timeout != 0, the caller must be running at IRQL <= APC_LEVEL and in a nonarbitrary thread context.”

看到沒,只有在Timeout != NULL && *Timeout==0  的情況下,才可以在DISPATCH_LEVEL等待

每當一個消費者持有的物理頁面數量超過自身配額, 消費者會主動自我修剪一部分物理頁面,置換到外存。

每當系統總體空閒實體記憶體緊張時(即小於最低空閒頁數閥值也即64個頁面時),核心中的那個平衡執行緒也會強制修剪某些物理頁面,置換到外存,以騰出一個物理頁面出來。注意並不是實體記憶體完全耗盡後才開始發生置換操作,而是實體記憶體快要用完(小於64個頁面)時,系統就開始著手置換操作了。

下面是置換函式原理:

NTSTATUS

MmPageOutVirtualMemory(MADDRESS_SPACE* as,  MEMORY_AREA* Area,  PageAddr)

{

   PTE* pt= MmGetPageTableForProcess(process, CurPageAddr);//找到這個頁面的pte位置

   PTE pte=*pt;

   PFN pfn=pte.pfn;

   SavedSwapEntry = pfn.SavedSwapEntry;

   If(pte.bDirty == false)//如果該頁面未髒,那好辦

   {

      MmDeleteVirtualMapping(as.process, PageAddr, …);//刪除該虛擬頁面對應的原PTE

      If(SavedSwapEntry != 0 )//if 該物理頁面是從頁檔案調入的,就直接使用那個頁檔案

      {

          //將該虛擬頁面對應的PTE重定向對映到原先的頁檔案中的那個頁面

          MmCreatePageFileMapping(as.process, PageAddr, SavedSwapEntry);

          Pfn.SavedSwapEntry = 0;

      }

      MmReleasePageMemoryConsumer(USER, pfn);//既然換到外存了,那就釋放物理頁面變成空閒狀態

      Return succ;

   }

   Else//如果已經髒了,工作有點多

   {

      If(SavedSwapEntry == 0 )//if 該物理頁面是從頁檔案調入的,就直接使用那個頁檔案

         NewSwapEntry= MmAllocSwapPage();//從磁碟上的頁檔案中分配一個檔案頁面

      Else

         NewSwapEntry= SavedSwapEntry;//沿用原來的頁檔案頁面

      MmWriteToSwapPage(pfn ---> NewSwapEntry);//以分頁寫irp的形式將物理頁面內容寫入外存頁面

      MmDeleteVirtualMapping(as.process, PageAddr, …);//刪除該虛擬頁面對應的原PTE

      MmCreatePageFileMapping(as.process, PageAddr, NewSwapEntry);//重定向

      Pfn.SavedSwapEntry = 0;

      MmReleasePageMemoryConsumer(USER, pfn);//既然換到外存了,那就釋放物理頁面變成空閒狀態

   }

}

MiBalancerThread()

{

   WaitObjects[0]=&MiBalanceEvent;

   WaitObjects[1]=&MiBalancerTimer;

   Whilr(true)

   {

      Status=KeWaitForMultipleObjects(2,WaitObjects,WaitAny,Executive,KernelMode,…);

      If(status==STATUS_SUCCESS)//如果收到了核心發來的一個平衡請求

      {

         While(系統總空閒頁數 < 閥值+5)

              呼叫各個消費者的修剪函式;

      }

      Else//定時醒來

      {

         For(遍歷每個消費者)

         {

           If(該消費者佔有的物理頁數是否超過了自己的配額 || 系統空閒物理頁數小於了閥值)

呼叫它的修剪函式; 

          }  

      }

   }//end while

}

系統中整個分四大消費者:檔案緩衝,使用者空間,核心分頁池,核心非分頁池

看下典型的User消費者是如何修剪自己的物理頁面的

NTSTATUS

MmTrimUserMemory(ToTrimPageCount, ULONG* ActualTrimPageCount)

    *ActualTrimPageCount=0;

Pfn=MmGetLRUFirstUserPage();//根據LRU演算法找到要換出去的物理頁面

While(pfn!=0 && ToTrimPageCount>0)

{

   MmPageOutPhysicalAddress(pfn);//換出去

   *ActualTrimPageCount++;

   Pfn=MmGetLRUNextUserPage(pfn);//獲得下一個要換出去的物理頁面

}

Return succ;

}

置換演算法是LRU,最近以來最少被訪問到的物理頁面優先換出去。講述作業系統原理的書籍一般都有介紹,在此不解釋。

NTSTATUS MmPageOutPhysicalAddress(pfn)

{

   //獲得第一個對映到本物理頁面的虛擬頁面

   FirstEntry=MmGetRmapListHeadPage(pfn);

   Process=FirstEntry->process;

   PageAddr=FirstEntry->address;

   If(PageAddr>2GB)

      AddressSpace=核心地址空間;

   Else

      AddressSpace=process->VadRoot;//目標程序的使用者空間

   MemoryArea = MmLocateMemoryAreaByAddress(AddressSpace, PageAddr);

   If(MemoryArea->Type == 檢視型區段)//表示if這個物理頁面是一個共享頁面,被多個程序對映共享

   {

     遍歷pfn.對映連結串列,一一處理;//特別處理

Return succ;

   }

   Else if(MemoryArea->Type == 普通型區段)

   {

       …

       MmPageOutVirtualMemory(…);

       …

   }

}

記憶體對映檔案與共享實體記憶體:(二者原理相通)

相信編寫過應用程式的朋友都知道“記憶體對映檔案”一說。簡單地講,記憶體對映檔案就是把磁碟上的檔案當做實體記憶體使用。這樣,要讀寫檔案時,不用再原始地呼叫ReadFile,WriteFile函式讀寫檔案。可以直接把檔案對映到虛擬記憶體,然後直接讀寫虛擬記憶體即可對檔案進行讀寫。

當一個檔案對映到虛擬記憶體後,一讀寫對應的虛擬記憶體,勢必引發缺頁異常,系統的缺頁異常處理函式自動處理,把檔案頁面調入讀入實體記憶體。這樣,就間接地對檔案進行了IO。

除了普通的純資料檔案可以對映到記憶體外,exe、dll等可執行檔案和磁碟中的頁檔案也是以記憶體對映檔案的方式進行訪問的。

應用層的CreateFileMapping這個API就是專用來建立檔案對映用的。

除此之外,兩個程序也可以共享實體記憶體,只要把同一個物理頁面對映到這兩個程序的地址空間即可,實體記憶體共享也是靠記憶體對映檔案機制實現的,只不過對映的不是普通磁碟檔案,而是頁檔案。

核心相關結構定義:

Struct  ROS_SECTION_OBJECT

{

   CSHORT  type;//本結構體的型別

   CSHORT  size;//本結構體的實際長度(結構體後面經常可以銜接其他資料,size包含了那部分的長度)

   ULONG  protect;//section的保護許可權

   ULONGLONG  MaxSize;//section的最大長度

   ULONG  AllocationAttributes;//包含了本section的檔案型別

   FILE_OBJECT*  FileObject;//建立本section的那個檔案物件(檔案控制代碼)

   Union

   {

      MM_SECTION_SEGMENT*  Segment;//資料檔案section中的唯一segment

      MM_IMAGE_SECTION_OBJECT*  ImageSegments;//映象檔案中的Segment陣列

   }; 

};

如上,普通資料檔案section內部就包含一個segment,可執行映象檔案(統稱PE檔案)section中一般包含多個segment,對應PE檔案中的每個“節”,如.TEXT,  .DATA,  .RSRC

struct  MM_IMAGE_SECTION_OBJECT

{

    ULONG_PTR ImageBase;

    ULONG_PTR StackReserve;

    ULONG_PTR StackCommit;

    ULONG_PTR EntryPoint;

    USHORT Subsystem;

    USHORT ImageCharacteristics;

    USHORT MinorSubsystemVersion;

    USHORT MajorSubsystemVersion;

    USHORT Machine;

    BOOLEAN Executable;

    ULONG NrSegments;// 本ImageSection中的segment個數,也即‘節’個數

    ULONG ImageSize;

    PMM_SECTION_SEGMENT Segments;//本ImageSection中的segment陣列

};

PE檔案頭的節表區中每個節的格式定義為://參考《Windows  PE權威指南》一書

Struct  PE_SEGMENT

{

  BYTE  Name[IMAGE_SIZEOF_SHORT_NAME=8]; //8個位元組的節名 如".text"    ".rdata"     ".data"    

  DWORD  VirtualSize;//該節未對齊前的原始資料大小  DWORD VirtualAddress; //該節的RVA

  DWORD  SizeOfRawData;  //該節的FAS也即檔案對齊大小,一般指對齊512B後的大小

  DWORD  PointerToRawData; //該節的FOA,即檔案偏移

  DWORD  PointerToRelocations; //專用於obj檔案

  DWORD  PointerToLinenumbers; //用於除錯

  WORD   NumberOfRelocations;   //專用於obj檔案

  WORD   NumberOfLinenumbers;   //用於除錯

  DWORD  Characteristics;  //該節的屬性(可讀、可寫、可執行、可共享、可丟棄、可分頁等屬性)

} IMAGE_SECTION_HEADER;

每個節的Characteristics的特徵屬性包括下面幾個:

IMAGE_SCN_CNT_CODE    該節中包含有程式碼 如.text

IMAGE_SCN_CNT_INITIALIZED_DATA   該節中包含有已初始化的資料 如.data

IMAGE_SCN_CNT_UNINITIALIZED_DATA  該節中包含有尚未初始化的資料,如.bss .data?

IMAGE_SCN_MEM_DISCARDABLE 該節載入到記憶體後是可拋棄的,如dll中的.reloc重定位節就是可以拋棄的

IMAGE_SCN_MEM_NOT_CACHED  節中資料不會經過緩衝

IMAGE_SCN_MEM_NOT_PAGED   該節不準交換到頁檔案中,sys檔案中的節(除.page)都不可換出

IMAGE_SCN_MEM_SHARED  這個節可以被多個程序共享,如dll中的共享節。也即表示本節是否允許寫複製。(預設允許)

IMAGE_SCN_MEM_EXECUTE 本節可執行

IMAGE_SCN_MEM_READ  本節可讀

IMAGE_SCN_MEM_WRITE 本節可寫

在核心中,每個節的結構體定義則如下:

Struct  MM_SECTION_SEGMENT

{

   LONG   FileOffset;