虚存页面的映射
有了对一个虚存空间的管理和对物理页面的分配管理。下一步自然就是建立二者之间的映射了。页面映射,指从虚存页面到物理页面的映射。多个虚存页面可以映射到同一个物理页面上,但是同一时间内不可能会有多个物理页面对应于同一个空间的同一个虚存页面上。
虚存页面顾名思义是“虚”的,必须通过映射落实到某种形式的物理存储介质上。最理想的物理存储介质自然是物理内存,但是物理内存因为一些因素空间是比外存要少很多。所以有时候当物理内存不足时,外存特别是磁盘或磁盘上的文件作为后盾!!!,与物理内存相结合构成一个二级的物理存储系统,那么物理内存就相当于作为“后盾”的外存的一个高速缓冲。这样的话,只需要把当前正在受到访问以及很可能受到访问的页面保存到物理内存上(这些页面的集合称为一个进程的“工作集(Working Set)”),而大量暂时不会受到访问的页面,例如不属于当前进程的页面,以及属于当前进程但是很少受到访问的页面,则可以保存在后备的“倒换文件”中。即使判断有误,本以为不会受到访问的页面受到了访问,那也不要紧,可以将其临时倒换进来。其效率也不会严重受到影响,因为这样的页面毕竟是属于少数的。
每个进程都有个“页面映射表”。从原理上讲,他可以是个“页面映射表项”(PTE)的一维数组。PTE指明了一个也虚存页面在物理内存的起点,即页面号。那样一来数组的长度就是220长度的大小,即1MB。然而,在4GB的地址空间中,实际分配使用的页面却通常只是很小一部分,所以数组里面很大一部分都是空洞,如果真的使用大小为1MB就会造成很大的浪费。所以,页面映射表所采用的是**“稀疏数组”的结构,即把整个页面映射表分为两层**。
其顶层是“页面目录”,这是一个“页面目录项”PDE的数组,实质上是个指针数组,如果指针非空就指向一个二级页面。
二级页面表是较小的PTE数组。
页面目录和二级页面表的大小都是1024(每个表项又是4B),所以正好都占一个页面->4KB。在实际映射的过程中,MMU以虚拟地址的最高10位为下标在页面目录中找到指向目标页面所在二级页面表的指针,
如果这个指针非空就进而以次10位作为下标在二级页面表中找到页面表项PTE,这就找到了目标页面的起始地址,虚拟地址的低12位在是在目标页面的偏移。这样,在实际使用中,页面目录中的大量指针其实都是空指针,这就省却了许多二级页面表,从而节省了二级页面表所占用的物理页面空间。当然,凡是已经分配使用的虚存页面在本进程的页面映射表中都有个PTE,这个PTE说明了具体页面的映射。
最坏的情况下,一个进程占满了4GB的虚存空间,那其所有的二级页面表加在一起就是1M个PTE,占1024个页面。一个有趣的问题是,此时整个页面映射表即页面目录加上所有的二级页面表共占多少页面。表面看起来是1025个页面,因为页面目录也要占一个页面。其实不是,实际上仍旧是1024个页面。这是因为,页面目录实际上同时是1024个二级页面表之一,也就是说,必有一个二级目录表中的一个PTE指向该页面目录表PDE数组的起始。
这个二级页面表无疑同时又起着页面目录的作用。所以,整个页面映射表的实际构成是:一个页面目录和最多1023个二级页面表,每个页面表各占一个页面。如果认为最多有1024个二级页面表,那么其中之一就起着页面目录的作用。假若PDE[1023]指向管理页面,若指向了它本身,则它本身描述的是PTE数组,所以这个时候指向了其他1023个页面以及它本身。
映射的几种情况
对于一个具体的进程,给定一个虚拟地址,CPU中的“内存管理单元”MMU就能根据该地址的高20位找到所属虚拟页面的PTE,其内容决定了该页面的映射。一个虚拟页面的映射有下列几种可能
- 无物理映射。这样的虚存页面自然是无法使用,如果访问会发生缺页异常。
- 有物理映射,但不是映射到物理内存页面上,而是映射到某个倒换文件中。此时我们称该页面有映射但不在内存,或者说这个页面已经被“倒出(Swap Out)”到页面倒换文件或设备上了。访问这样的页面自然也会引起缺页异常,但是可以根据地址判定这个页面已经分配并建立映射。如果是,就可以把这个页面“倒入(Swap In)”内存,即为其分配物理页面并从外存读入。此时PTE的内容表明后备页面所在的位置。
- 有物理映射,并且被映射到某个页面上。此时我们称该(虚存)页面在内存中,或者说这个页面已被倒入内存。只要访问权限和保护模式相符,访问这样的页面是不会发生异常的。
凡是有物理映射,并且被映射到真实的物理内存上(而没有倒出的表项),其高20位持有所映射物理页面的页面号。
其低8位代表的含义如下
#define PA_BIT_PRESENT (0) //为1表示所映射的页面在内存中
#define PA_BIT_READWRITE (1) //为1表示可写可读,为0表示只读
#define PA_BIT_USER (2) //为1表示是用户空间页面
#define PA_BIT_WT (3) //用于高速缓存,为1表示“穿透写(Write-Through)”
#define PA_BIT_CD (4) //为1时关闭高速缓冲
#define PA_BIT_ACCESSED (5) //只要访问该页面,MMU就自动将该位置设为1
#define PA_BIT_DIRTY (6) //只要写入该页面,MMU就自动将该位置置1
#define PA_BIT_GLOBAL (8) //该位为1的页面映射表项不受冲刷
#define PA_PRESENT (1 << PA_BIT_PRESENT)
#define PA_READWRITE (1 << PA_BIT_READWRITE)
#define PA_USER (1 << PA_BIT_USER)
#define PA_DIRTY (1 << PA_BIT_DIRTY)
#define PA_WT (1 << PA_BIT_WT)
#define PA_CD (1 << PA_BIT_CD)
#define PA_ACCESSED (1 << PA_BIT_ACCESSED)
#define PA_GLOBAL (1 << PA_BIT_GLOBAL)
#define PTE_TO_PFN(X) ((X) >> PAGE_SHIFT) //PTE转页面号
#define PFN_TO_PTE(X) ((X) << PAGE_SHIFT) //页面号转PTE
中间留下4位,Intel手册中称为Avail,这是让程序员自由处置使用的。
所以PFN_TO_PTE(Page) | PA_PRESENT | PA_READ_WRITE就是一个表项的内容,表示把虚存页面映射到物理页面号Page,页面的映像在内存中,并且可读可写。
最低位PA_PRESENT是关键,如果这一位是0,就表示所映射的页面不在内存中,CPU的内存管理单元MMU会立即产生一次缺页异常。此时别的标志位及页面号对于MMU而言就都失去了意义。这样,操作系统就可以用该表项指示页面在倒换设备或文件中的位置。就Windows而言,表项的最高8位用于倒换文件号,低24位用于倒换文件内部的页面号加1。
MmCreateVirtualMapping
就映射的建立而言,最为一般的操作是MmCreateVirtualMapping
作用是给定一组物理页面,将某个给定进程从虚拟地址Address开始的一个区块映射到这组物理页面上
/*
Process指明是哪个进程,若Process为NULL,表明属于系统空间,不属于任何进程!!!
Address是需要建立映射的虚存区块的起始位置
flProtect是这些页面的保护模式
Pages指向一个页面号Pfn的数组,系统准备好的与虚存页面一一对应的物理页面数组,物理页面并不一定连续
PageCount是这组页面的大小,这些物理页并不一定连续,所以要用数组存储!
*/
NTSTATUS
NTAPI
MmCreateVirtualMapping(PEPROCESS Process,
PVOID Address,
ULONG flProtect,
PPFN_TYPE Pages,
ULONG PageCount)
{
ULONG i;
for (i = 0; i < PageCount; i++)//首先检测这组物理页面是否都是空闲页面
{
if (!MmIsUsablePage(Pages[i]))
{
DPRINT1("Page at address %x not usable\n", PFN_TO_PTE(Pages[i]));
KEBUGCHECK(0);
}
}
return(MmCreateVirtualMappingUnsafe(Process,
Address,
flProtect,
Pages,
PageCount));
}
可以看出实际操作是由MmCreateVirtualMappingUnsafe完成的,这里只是检查了这个Pages数组中每个页面的合法性如何
MmCreateVirtualMappingUnsafe
NTSTATUS
NTAPI
MmCreateVirtualMappingUnsafe(PEPROCESS Process,
PVOID Address,
ULONG flProtect,
PPFN_TYPE Pages,
ULONG PageCount)
{
ULONG Attributes;
PVOID Addr;
ULONG i;
ULONG oldPdeOffset, PdeOffset;
BOOLEAN NoExecute = FALSE;
DPRINT("MmCreateVirtualMappingUnsafe(%x, %x, %x, %x (%x), %d)\n",
Process, Address, flProtect, Pages, *Pages, PageCount);
if (Process == NULL)//表示该虚存区块是属于系统空间的
{
if (Address < MmSystemRangeStart)//如果范围在系统空间之外,则严重错误
{
DPRINT1("No process\n");
KEBUGCHECK(0);
}
if (PageCount > 0x10000 ||//0x10000是20bit为的全集,这是最大的物理页面数,自然不能超过,这是即Pages给出的数组个数不可能超过0x10000,并且该虚拟地址所代表的页面号的最大值也不能超过0x100000,否则无法分配页面号
(ULONG_PTR) Address / PAGE_SIZE + PageCount > 0x100000)
{
DPRINT1("Page count to large\n");
KEBUGCHECK(0);
}
}
else//此时在用户空间的某个具体进程中
{
if (Address >= MmSystemRangeStart)//仍然先检测要映射的区域是否合法
{
DPRINT1("Setting kernel address with process context\n");
KEBUGCHECK(0);
}
if (PageCount > (ULONG_PTR)MmSystemRangeStart / PAGE_SIZE ||//检测映射物理页面数量和虚拟地址块页面数量是否合法
(ULONG_PTR) Address / PAGE_SIZE + PageCount >
(ULONG_PTR)MmSystemRangeStart / PAGE_SIZE)
{
DPRINT1("Page Count to large\n");
KEBUGCHECK(0);
}
}
Attributes = ProtectToPTE(flProtect);//将参数转换成PTE格式保护模式
if (Attributes & 0x80000000)//若最高位为1,表示不可执行,设置NoExecute属性,即flProtect中的PAGE_IS_EXECUTABLE设置为0
{
NoExecute = TRUE;
}
Attributes &= 0xfff;//将高20bit清零
if (Address >= MmSystemRangeStart)//对于系统空间
{
Attributes &= ~PA_USER;//属性要设置为ring0才可访问
if (Ke386GlobalPagesEnabled)//若设置了不被冲刷,则页面属性也会添加该项
{
Attributes |= PA_GLOBAL;
}
}
else//否则说明就是用户空间中的页,设置成PA_USER,这样用户就可以访问该页了
{
Attributes |= PA_USER;
}
//上述是检测参数的合法性以及对flProtect转换成PTE的一个过程
Addr = Address;
if (Ke386Pae)//其中Ke386Pae这个布尔值代表系统是32位还是64位 这里我们不考虑Pae(Page Address Extension 物理地址扩展)存在的情况
{
...
}
else
{
PULONG Pt = NULL;//指向页表的指针
ULONG Pte;
oldPdeOffset = ADDR_TO_PDE_OFFSET(Addr) + 1;//获取pde下标
for (i = 0; i < PageCount; i++, Addr = (PVOID)((ULONG_PTR)Addr + PAGE_SIZE))
{
if (!(Attributes & PA_PRESENT) && Pages[i] != 0)
{
DPRINT1("Setting physical address but not allowing access at address "
"0x%.8X with attributes %x/%x.\n",
Addr, Attributes, flProtect);
KEBUGCHECK(0);
}
PdeOffset = ADDR_TO_PDE_OFFSET(Addr);//获得该虚拟页面对应的pde下标
if (oldPdeOffset != PdeOffset)//若原先的pde下标跟现在的pde下标不相等
{
MmUnmapPageTable(Pt);//此时Pt == NULL 解除该页表映射
Pt = MmGetPageTableForProcess(Process, Addr, TRUE);//重新为该进程申请一个二级页表,此时返回指向该二级页表首个PTE指针
if (Pt == NULL)//申请失败 说明出现了严重错误
{
KEBUGCHECK(0);
}
}
else//若下标相等 则指向该二级页表的下一项PTE
{
Pt++;
}
oldPdeOffset = PdeOffset;//之后PdeOffset就会跟oldPedOffset相等
Pte = *Pt;
MmMarkPageMapped(Pages[i]);