1、32 位系统中有 4GB 的虚拟地址空间每个进程有一个地址空间,共 4GB, (具体分为低 2GB 的用户地址空间+高 2GB 的内核地址空间)各个进程的用户地址空间不同,属于各进程专有,内核地址空间部分则几乎完全相同虚拟地址如 0x11111111, 看似这 8 个数字是一个整体,其实是由三部分组成的,是一个三维地址,将这个 32 位的值拆开,高 10 位表示二级页表号,中间 10 位表示二级页表中的页号,最后 12 位表示页内偏移(212=4kb),因此,一个虚拟地址实际上是一个三维地址,指明了本虚拟地址在哪个二级页表,又在哪个页以及页内偏移是多少 这三样信息!【虚拟地址 = 二级页表号.
2、页号.页内偏移】:口诀【页表、页号、页偏移】Cpu 访问物理内存的原理介绍:如高级语言DWORD g_var; /假设这个全局变量被编译器编译为 0x00000004g_var=100; 那么这条赋值语句编译后对应的汇编语句为:mov DWORD PTR0x00000004,100这里 0x00000004 就是一个虚拟地址,简称 VA,那么这条 mov 指令究竟是如何寻址的呢?寻址过程为:CPU 中的虚拟地址转换器也即 MMU,将虚拟地址 0x00000004 转换为物理地址具体转换过程为:根据 CR3 寄存器中记录的当前进程页表的物理地址,找到总页表也即页目录,再根据虚拟地址中的页表号,以
3、页表号为索引,找到总页表中对应的 PDE,再根据 PDE,找到对应的二级页表,再以虚拟地址中的页号部分为索引,找到二级页表中的对应 PTE,再根据这个 PTE 记录的映射关系,找到这个虚拟页面对应的物理页面,最后加上虚拟地址中的页内偏移部分,加上这个偏移值,就得出最后的物理地址。具体用下面的函数可以形象表达寻址转换过程:mov DWORD PTR0x00000004,100 /这条指令的内部原理(没考虑二级缓冲情况)va=0x00000004;/页表号=0,页号=0,页内偏移=4总页表=CR3; /本进程的总页表的物理地址固定保存在 cr3 寄存器中PDE=总页表va.页表号; /PDE 为对
4、应的二级页表描述符二级页表=PDE.PageAddr; /得出本二级页表的地址PTE=二级页表va.页号; /得出到该虚拟地址所在页面的 PTE 映射描述符If(PTE 空白) /PTE 为空表示该虚拟页面尚未建立映射触发 0x0e 号页面访问异常(具体为缺页异常)ElseIf(PTE.bPresent=false) /PTE 的这个字段表示该虚拟页面当前是否映射到了物理内存触发 0x0e 号页面访问异常(具体为缺页异常)ElseIf(CR0.wp=1 /本地址空间的已分配区段表(一个 AVL 树的根)VOID* LowestAddress;/本地址空间的最低地址(用户空间是 0,内核空间是
5、0x80000000)EPROCESS* Process;/本地址空间的所属进程/*一个表,表中每个元素记录了本地址空间中各个二级页表中的 PTE 个数,一旦某个二级页表中的PTE 个数减到了 0,就自动释放该二级页面表本身,体现为稀疏数组特征*/USHORT* PageTableRefCountTable; ULONG PageTableRefCountTableSize;/上面那个表的大小地址空间中所有已分配的区段都记录在一张表中,这个表不是简单的数组,而是一个 AVL 树,用来提高查找效率。每个区段的基址都对齐 64KB 或 4KB(指 64KB 整倍数) ,各个区段之间可以有空隙,区段
6、的分布是很零散的!各个区段之间,夹杂的空隙就是尚未分配的虚拟内存。注:所谓已分配区段,是指已经过 VirtualAlloc 预订(reserve)或提交(commit)后的虚拟内存区段的描述符如下:Struct MEMORY_AREA /区段描述符Void* StartingAddress; /开始地址,普通区段对齐 64KB,其它类型区段对齐 4KBVoid* EndAddress;/结尾地址,EndAddress StartingAddress 就是该区段的大小MEMORY_AREA* Parent;/AVL 树中的父节点MEMORY_AREA* LeftChild;/左边的子节点MEMO
7、RY_AREA* RightChild;/右边的子节点/常见的区段类型有:普通型区段、视图型区段、缓冲型区段(后面文件系统中会讲到)等ULONG type;/本区段的类型ULONG protect;/本区段的保护权限,可读、可写、可执行的组合ULONG flags;/当初分配本区段时的分配标志BOOLEAN DeleteInProgress;/本区段是否标记为了已删除ULONG PageOpCount;UnionStruct /这个 Struct 专用于视图型区段/凡是含有 ROS 字样的函数与结构体都表示是 ReactOS 与 Windows 中不同的实现细节ROS_SECTION_OBJE
8、CT* section; ULONG ViewOffest;/指本视图型区段在所在 Segment 内部的偏移MM_SECTION_SEGMENT* Segment;/所属 SegmentBOOLEAN WriteCopyView;/本视图区段是不是一个写复制区段 SectionData;LIST_ENTRY RegionListHead;/本区段内部的所有 Region 区块,放在一个链表中Data;/end浅谈区段类型:MEMORY_AREA_VIRTUAL_MEMORY:/普通型区段,由 VirtuAlloc 应用层用户分配的区段都是普通区段MEMORY_AREA_SECTION_VIE
9、W:/视图型区段,用于文件映射、共享内存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:/内核空间中用于映射外设内存(如显存)的区
10、段MEMORY_AREA_SHARED_DATA:/内核空间中用于与用户空间共享的区段Struct MM_REGION /区块描述符ULONG type;/指本区块的分配类型(预定型分配、提交型分配) ,又叫映射状态(已映射、尚未映射)ULONG protect;/本区块的访问保护权限,可读、可写、可执行的组合ULONG length;/区块长度,对齐页面大小(4KB)LIST_ENTRY RegionListEntry;/用来挂入所在区段的区块链表内存以区段为分配单位,一个区段内部,又按分配类型、保护属性划分区块。一个区块包含一到多个内存页面,分配类型相同并且保护权限相同的区域组成一个个的区
11、块,因此,称为“同属性区块” 。一个区段内部,相邻区块之间的属性肯定是不相同的(分配类型或保护权限不同) ,若两个相邻区块的属性相同了,会自动合并成一个新的区块。进程,地址空间,区段,区块,页面的逻辑层次关系一个虚拟页面实际上有五级限定:【进程.地址空间.区段.区块.虚拟页面】意为:哪个进程的哪个地址空间中的哪个区段中的哪个区块中的哪个虚拟页面MEMORY_AREA* MmLocateMemoryAreaByAddress(MADDRESS_SPACE* as, void* addr);这个内核函数用于在指定地址空间中查找指定地址所属的已分配区段,如果返回 NULL,表示该地址尚不处于任何已分
12、配区段中,也即表示该地址尚未分配。Void*MmFindGap(MADDRESS_SPACE* as, ULONG len, ULONG AlignGranularity, BOOL TopDown)这个函数在指定地址空间中 查找一块符合 len 长度的空闲(也即未分配)区域,返回找到的空闲区的地址,AlignGranularity 表示该空白区必须的对齐粒度,TopDown 表示是否从高地址端向低地址端搜索MEMORY_AREA*MmLocateMemoryAreaByRegion(MADDRESS_SPACE* as, void* addr, ULONG len)这个函数从指定地址空间的低
13、地址端向高地址段搜索,返回第一个与给点区间( addr,len )有交集的已分配区段NTSTATUSMmCreateMemoryArea(MADDRESS_SPACE* as, type, void* BaseAddr, Len, protect, bFixedAddr, AllocFlags, MEMORY_AREA* Result)Len=Align(Len,4kb);/区段长度都要对齐 4kbUINT BaseAlign;/区段的基址对齐粒度If(type=普通区段)BaseAlign=64KB;ElseBaseAlign =4KB; If(*BaseAddr =NULL Else/el
14、se 只要用户给定了基址,就必须从那儿开始分配*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=t
15、ype;/本区段的初始分配类型(初始时,一个区段内部就一个区块)Area.StartAddr=*BaseAddr;Area.EndAddr=*BaseAddr+Len;Area.protect=protect;/本区段的初始保护属性Area.flags=Allocflags;MmInsertMemoryArea(as,Area);/分配后插入地址空间中的已分配区段表中(AVL 树)*Result=Area;Return succ;上面这个函数用来从指定地址或者让系统自动寻找一块空闲的区域,分配一块指定长度、类型的区段。所谓分配,包含 reserve 型分配(即预定型分配) ,和 commit
16、型分配(即提交型分配)预定:只占用分配一块区段,不建立映射提交:分配一块区段并建立映射(映射到磁盘页文件/物理内存页面/普通文件) MM_REGION*MmFindRegion(void* AreaBaseAddr, LIST_ENTRY* RegionListHead, void* TgtAddr,Void* RegionBaseAddr)这个函数从指定区段的区块链表中,查找给定目标地址 TgtAddr 落在哪一个区块内第一个参数表示区段的基址。函数返回找到的区段并顺便将该区段的基址也存入最后一个参数中返回给调用者MM_REGION*MmSplitRegion(MM_REGION* rgn,
17、 BaseAddr, StartAddr,Len, NewType,NewProtectAlterFunc)这个函数将指定区块内部的指定区域(StartAddr,Len)修改为新的分配类型、保护属性,使原区块分裂,一分为三(特殊情况一分为二) ,然后调用 AlterFunc 跟着修改二级页表中,新区块的那些 PTE,最后再跟着修改物理页面分配情况。函数返回新分出来的那个中间区块。这是一个内部辅助函数。NTSTATUSMmAlterRegion(AreaBaseAddr, RegionListHead, TgtAddr,Len, NewType,NewProtect, AlterFunc)这个函
18、数是个通用函数,用来修改指定区段内部的指定区域的分配类型、保护属性,然后调用调用 AlterFunc 跟着修改二级页表中,目标区域对应的那些 PTE,最后再跟着修改物理页面的分配情况。物理内存讲述:内核中有一个全局的物理页面数组,和 7 个物理页面链表。分别是:PHYSICAL_PAGE MmPageArray;/物理内存有多大,该数组就有多大LIST_ENTRY FreeZeroedPageListHead;/空闲物理页面链表(且物理页面已清 0)LIST_ENTRY FreeUnzeroedPageListHead;/空闲物理页面链表(但物理页面尚未清 0)LIST_ENTRY UsedP
19、ageListHeads4;/细分为 4 大消费用途的忙碌物理页面链表,各链表中按 LRU 顺序LIST_ENTRY BiosPageListHead;/用于 Bios 的物理页面链表物理页面数组是一个物理页面描述符数组,每个元素描述对应的物理页面(数组索引号即物理页号,又叫 pfn) ,每个描述符是一个 PHYSICAL_PAGE 结构体Struct PHYSICAL_PAGE /物理页面描述Type ;/该物理页面的空闲占用状态(1 表示空闲,2 表示已占用,3 表示分给了 BIOS)Consumer;/该物理页面的消费用途(用户/内核分页池/内核非分页池/文件缓冲 四种)Zero;/标志
20、本页面是否已清 0ListEntry;/用来挂入那 7 个链表之一ReferenceCount;/引用计数,一旦减到 0,页面就变为空闲状态,进入空闲链表SWAPENTRY SavedSwapEntry;/对应的来源页文件,用于置换,一般为空 LockCount;/本物理页面的锁定计数(物理页面可锁定在内存中,不许置换到外存)MapCount;/同一个物理页面可以映射到 N 个进程的 N 个虚拟页面MM_RMAP_ENTRY* RmapListHead;/本物理页面映射给的那些虚拟页面,组成的链表 一个物理页面的典型状态转换过程为:起初处于空闲并清 0 的状态,然后应内存分配要求分配给 4 个
21、消费者之一,同时,将该物理页面记录到对应消费者的 UsedPageListHead 链表中,最后用户用完后主动释放,或者因为物理内存紧张,被迫释放换到外存,而重新进入空闲状态,但此时尚未清 0,将进入FreeUnzeroedPageList 链表。然后,内核中有一个守护线程会定时、周期扫描这个空闲链表,将物理内存清 0,转入 FreeZeroedPageList 链表,等候下次被分配。如此周而复返PFN_NUMBERMmAllocPage(ULONG ConsumerType)PFN_NUMBER Pfn;/物理页号PPHYSICAL_PAGE PageDescriptor;BOOLEAN N
22、eedClear = FALSE;/是否需要清零if (FreeZeroedPageList链表 为空)if (FreeUnzeroedPageList 为空)return 0;/如果两个空闲链表都为空就失败PageDescriptor = MiRemoveHeadList(NeedClear = TRUE;elsePageDescriptor = MiRemoveHeadList(/从空闲链表中摘下来一个空闲页面后,初始化MmAvailablePages-;/总的可用物理页数-PageDescriptor-ReferenceCount = 1;/刚分配的物理页面的引用计数为1PageDesc
23、riptor-LockCount=0;/表示可被置换到外存PageDescriptor-MapCount=0;/表示刚分配的物理页面尚未映射到任何虚拟页面/记录到分配链表中InserTailList(if (NeedClear)MiZeroPage(PfnOffset);/清0Pfn = PageDescriptor-MmPageArray;/pfn=数组的索引,就是物理页号return Pfn;这段函数为指定消费者分配一个物理页面,并第一时间将物理页面清 0.然后返回分得的物理页号NTSTATUSMmRequestPageMemoryConsumer(consumer, PFN* pfn)/
24、先检查物理页面配额,超出配额,就自我修剪If(本消费者的分得的物理页面数量 = 本消费者的最大配额)/换出那个消费者的某个物理页面到外存,腾出一个物理页面出来Call 对应消费者的自我页面修剪函数 If(当前系统总的空闲页面总量 储备阀值)If(consumer=非分页池消费者)*pfn = MmAllocPage(consumer);/分完后唤醒系统中的平衡线程去平衡物理页面,填补空白KeSetEvent( Return succ;Else*pfn = 请求平衡线程赶紧从其他消费者手中退出一个物理页面;Return succ;Else*pfn = MmAllocPage(consumer);
25、这个函数,先检查配额,再检查空闲页面阀值,做好准备工作后,再才分配物理页面NTSTATUS MmReleasePageMemory(consumer, pfn)Consumer.UsedPageCount-;/递减本消费者持有的页面计数;pfn.ReferenceCount-;/递减本页面的引用计数If(pfn.ReferenceCount=0)If(有其他分配请求正在等待退让物理页面)将这个 pfn 分给那个 Pending 中的分配请求Else将这个页面挂入系统 FreeUnzeroedPageList 链表;这个函数释放指定消费者占用的指定物理页面,实际上是递减引用计数,引用计数减到 0
26、 后就挂入系统空闲链表虚拟页面与物理页面之间的映射:一个物理页面可以映射到 N 个进程的 N 个虚拟页面中,但一个虚拟页面同一时刻只能映射到一个物理页面。可以这么理解:“一个物理页面当前可能被 N 个虚拟页面映射着” , “本虚拟页面当前映射着一个物理页面”。每个虚拟页面又分四种映射状态:1、 映射着某个物理页面(已分配且已映射)2、 映射着某个磁盘页文件中的某个页面(已分配且已映射)3、 没映射到任何物理存储介质(对应的 PTE=0) ,但是可能被预定了(已分配,但尚未映射)4、 裸奔(尚未分配,以上情况都不满足)一个进程的用户地址空间高达 2GB,分成很多虚拟页面,如果时时刻刻都让这些虚拟
27、页面映射着物理内存,那么物理内存恐怕很快就分完了。所以,同一时刻,只有最频繁访问的那些虚拟页面映射着物理页面(最频繁访问的那些虚拟页面就构成了一个进程的工作集) ,工作集中的所有虚拟页面都映射着物理页面,一旦访问工作集外面的虚拟页面,势必引发缺页异常,系统的缺页异常处理函数会自动帮我们处理这种异常(自动分配一个物理页面,将那个引发缺页异常的虚拟页面映射着的外存页面 以分页读 irp 的形式读入到 新分配的物理页面中,然后修改那个虚拟页面的映射关系,指向那个新分配的物理页面) ,这就是系统的缺页异常处理函数的工作原理,应用程序毫不知情。漫谈页目录、二级页表:前面讲到每个虚拟地址看似是一个整形值,
28、实际上由三部分组成: 页表号.页号.页内偏移 ,为什么不是直接的页号.页内偏移呢,直接采用一个简单的一维数组,记录所有虚拟页面的这样多直观!原因是:一个进程的虚拟地址空间太大,如果为每个虚拟页面都分配一个 PTE,那么将占用大量内存,不信我们算一下:一个进程中总共有 4GB/4KB=220 个虚拟页面,也即 1MB 个虚拟页面,如果直接采用一维数组,描述这个1MB 页面的映射情况,那么整个数组大小=1MB*sizeof(PTE)=4MB,这样,光页表部分就占据了 4MB 的内存。注意页表部分本身占用的内存是非分页内存,也即真金白银地占据着 4MB 物理内存,这 4MB 在现在的机器看来,并不算
29、多,但在早期只有 256MB 物理内存的老爷机上(最多只能同时支持 256MB/4MB 个=64 个进程),已经算够多了!相反,如果采用页目录+二级页表的方式就会节省很多内存!一个二级页表本身有一个页面大小,可以容纳 4KB/sizeof(PTE)=1024 个 PTE,换句话说,一个二级页表可以描述 1024 个页面的映射情况(换算成字节数,一个二级页面能描述 1024*4kb 的地址空间) ,一个进程总共有 4GB 地址空间,那么整个地址空间就有 4GB/(1024*4kb)=1024 个二级页表,那些暂时未映射的一大片虚拟地址,一般是高端的地址,就对应这 1024 个二级页表中靠后的那些
30、二级页表,就可以暂时不用为他们分配物理内存了, 只有在确实要访问那些虚拟页面时,才分配他们对应的二级页表,这样按需分配,就节省了物理内存。另外,32 位系统中每个进程有 1024 个二级页表外加一个页目录。咋一看,似乎系统中有 1025 个页表维持着映射关系,其实不然,因为页目录本身是一个特殊的二级页表,也是那 1024 个二级页表中的一个。概念上,我们把第一个二级页表理解为页目录。这样,系统中实际上共有 1024 个二级页表(包括页目录本身在内,但要注意页目录并不在二级页表区的中的第一个位置,而是在中间的某个位置,后面我会推算页目录本身的虚拟地址在什么地方) 。明白了这个道理,就可以由任意一
31、个虚拟地址推算出他所在的二级页表在页目录中的索引位置。#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 都是空的,实际的二级页面个数往往很少。每个虚拟页面的映射描述符(即
32、 PTE)的位置是固定的,根据虚拟页号可以自然算出那个虚拟页面的映射描述符位置,找到映射描述符的位置后,就可以获得该虚拟页面的当前映射情况(是否已映射,若已映射,是映射到了物理内存还是页文件,又具体映射到了哪个具体的物理页面,这些信息都一一获悉) ,因此PTE 映射描述符是页表的核心,现在看一下 PTE 它的结构。PTE 的结构,PTE 是二级页表中的表项,用来描述一个虚拟页面的映射情况以及其他信息注意 PTE 本身长度为 4B,但我们可以把它当做一个描述符结构体,并不妨碍理解Struct PTEUnionStructBool bPresent;/重点字段,表示该虚拟页面是否映射到了物理内存B
33、ool bWritable;/表示这个虚拟页面是否可写Bool bUser;/表示是否是用户地址空间中的虚拟页面Bool bReaded;/表示本虚拟页面自从上次置换到内存后是否曾被读过Bool bDirty;/表示本虚拟页面自从上次置换到内存后是否曾被写过Bool bGlobal;/表示本 PTE 表项是全局页面的映射描述符,切换进程时不用刷新本 PTEUINT pfn;/关键字段,表示本虚拟页面对应的物理页号 Mem;Struct文件中的页面号;页文件号;/系统中可以支持多个 Pagefile.sys 页文件File; 这样,这个 PTE 如果映射到了内存,就解释为 Mem 结构体,如果映射到了页文件,就解释为 File 结构体。NTSTATUS MmCreateVirtualMapping(process, FirstVirtualPageAddr, VirtualPageCount,PfnArray, PfnCount, PteFlags)If(VirtualPageCount != PfnCount )Return fail;