在没有引入虚拟内存概念之前,程序的寻址范围是有限的, 比如32位系统的固定寻址范围是 4GB,如果没有虚拟内存,每打开一个进程都要分配 4GB 的物理内存,那么所有的内存资源就会被很快消耗完,如果此时再有新的进程被创建,就只能进入等待状态。
另外由于指令都是直接访问物理内存的,那么我这个进程就可以修改其他进程的数据,甚至会修改内核地址空间的数据,这是不安全的。
引入虚拟内存后,每个进程被创建后都会被分配一个4GB的虚拟内存, 通俗点讲就是每个进程都认为自己拥有4G的空间,但实际在虚拟内存对应的物理内存上,可能只对应的一点点的物理内存。并且进程认为它被分配的4GB的内存是一块连续的内存,而实际上,它通常是被分隔成多个物理内存碎片,还有一部分存储在外部磁盘存储器上,在需要时进行数据交换。
虚拟地址和物理地址的转换
● 虚拟地址指的是 ip 寄存器的值。
● cs寄存器里保存的一个选择子,通过选择子可以到 GDT 或 LDT 表里得到一个基址,然后加上 ip 的值,得到线性地址。
● 线性地址分成页目录,页表,偏移三个部分,线性地址再通过页目录,页表信息得到物理地址。
虚拟地址的组成
对于 32位系统,虚拟地址由三部分组成:
位置 | 内容 |
---|---|
高10位 | 二级页表号 |
中间10位 | 页号 |
低12位 | 页内偏移 |
由于 Windows 不分段,每个段的基地址都是0x0,长度为4GB,虚拟地址->线性地址->物理地址 = 虚拟地址->物理地址。简单来说,Windows 的线性地址就刚好是物理地址
看一个例子:
//这条指令的内部原理(没考虑二级缓冲情况)
mov DWORD PTR[0x00000004],100
{
va=0x00000004; // 页表号=0,页号=0,页内偏移=4
总页表=CR3; // 本进程的总页表的物理地址固定保存在cr3寄存器中
PDE=总页表[va.页表号]; // PDE为对应的二级页表描述符
二级页表=PDE.PageAddr; // 得出本二级页表的地址
PTE=二级页表[va.页号]; // 得出到该虚拟地址所在页面的PTE映射描述符
If(PTE空白) // PTE为空表示该虚拟页面尚未建立映射
触发0x0e号页面访问异常(具体为缺页异常)
If(PTE.bPresent==false) // PTE的这个字段表示该虚拟页面当前是否映射到了物理内存
触发0x0e号页面访问异常(具体为缺页异常)
If(CR0.wp==1 && PTE.Writable==false) // 已开启页面写保护功能,就检查这个页面是否可写
触发0x0e号页面访问异常(具体为页面访问保护越权异常)
物理地址pa =cs.base + PTE.PageAddr + va.页内偏移 //得出对应的物理地址
将得到的pa放到地址总线上,100放在数据总线上,经由FSB->北桥->内存总线->内存条 写入内存
}
PTE是二级页表中的表项,记录了对应虚拟页面的映射情况,由于每次访问内存都要先访问一次PTE获取该虚拟页面对应的物理页面,导致效率很低,因此引入了二级缓存技术,用来保存那些频繁访问的PTE,这样,cpu每次去查物理页面时,就先尝试在二级缓冲中查找对应的PTE,如果找不到,再才去访问内存中的PTE,效率大大提高。
// 地址空间描述符
Struct MADDRESS_SPACE
{
MEMORY_AREA* MemoryRoot; // 本地址空间的已分配区段表(一个AVL树的根)
VOID* LowestAddress; // 本地址空间的最低地址(用户空间是0,内核空间是0x80000000)
EPROCESS* Process; // 本地址空间的所属进程
USHORT* PageTableRefCountTable; // 一个表,表中每个元素记录了本地址空间中各个二级页表中的PTE个数,一旦某个二级页表中的PTE个数减到了0,就自动释放该二级页面表本身,体现为稀疏数组特征
ULONG PageTableRefCountTableSize; // 上面那个表的大小
}
// 区段描述符
Struct MEMORY_AREA
{
Void* StartingAddress; // 开始地址,普通区段对齐64KB,其它类型区段对齐4KB
Void* EndAddress; // 结尾地址,EndAddress – StartingAddress就是该区段的大小
MEMORY_AREA* Parent; // AVL树中的父节点
MEMORY_AREA* LeftChild; // 左边的子节点
MEMORY_AREA* RightChild; // 右边的子节点
ULONG type; // 本区段的类型(普通型区段、视图型区段、缓冲型区段等)
/*
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: //内核空间中用于与用户空间共享的区段
*/
ULONG protect; // 本区段的保护权限,可读、可写、可执行的组合
ULONG flags; // 当初分配本区段时的分配标志
BOOLEAN DeleteInProgress; // 本区段是否标记为了 "已删除"
ULONG PageOpCount;
Union
{
// 专用于视图型区段
Struct
{
// 凡是含有ROS字样的函数与结构体都表示是ReactOS与Windows中不同的实现细节
ROS_SECTION_OBJECT* section;
ULONG ViewOffest; // 指本视图型区段在所在Segment内部的偏移
MM_SECTION_SEGMENT* Segment; // 所属Segment
BOOLEAN WriteCopyView; // 本视图区段是不是一个写复制区段
}SectionData;
LIST_ENTRY RegionListHead; // 本区段内部的所有Region区块,放在一个链表中
}Data;
}
// 区块描述符
Struct MM_REGION
{
ULONG type; // 指本区块的分配类型(预定型分配、提交型分配),又叫映射状态(已映射、尚未映射)
ULONG protect; // 本区块的访问保护权限,可读、可写、可执行的组合
ULONG length; // 区块长度,对齐页面大小(4KB)
LIST_ENTRY RegionListEntry; // 用来挂入所在区段的区块链表
}
// 物理页面描述
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; // 本物理页面映射给的那些虚拟页面,组成的链表
}
// 在指定地址空间中查找指定地址所属的已分配区段
MEMORY_AREA* MmLocateMemoryAreaByAddress(MADDRESS_SPACE* as, void* addr);
// 在指定地址空间中查找一块符合len长度的空闲(也即未分配)区域,返回找到的空闲区的地址
Void* MmFindGap(MADDRESS_SPACE* as, ULONG len, ULONG AlignGranularity, BOOL TopDown);
// 从指定地址空间的低地址端向高地址段搜索,返回第一个与给点区间(addr,len)有交集的已分配区段
MEMORY_AREA* MmLocateMemoryAreaByRegion(MADDRESS_SPACE* as, void* addr, ULONG len);
// 从指定地址或者让系统自动寻找一块空闲的区域,分配一块指定长度、类型的区段。
NTSTATUS MmCreateMemoryArea(MADDRESS_SPACE* as, type, void** BaseAddr, Len, protect, bFixedAddr, AllocFlags, MEMORY_AREA** Result);
// 从指定区段的区块链表中,查找给定目标地址TgtAddr落在哪一个区块内
MM_REGION* MmFindRegion(void* AreaBaseAddr, LIST_ENTRY* RegionListHead, void* TgtAddr,
Void** RegionBaseAddr);
// 将指定区块内部的指定区域(StartAddr,Len)修改为新的分配类型、保护属性,使原区块分裂
MM_REGION* MmSplitRegion(MM_REGION* rgn, BaseAddr, StartAddr,Len, NewType,NewProtect
AlterFunc);
// 修改指定区段内部的指定区域的分配类型、保护属性
NTSTATUS MmAlterRegion(AreaBaseAddr, RegionListHead, TgtAddr,Len, NewType,NewProtect, AlterFunc);
// 为指定消费者分配一个物理页面,并第一时间将物理页面清0.然后返回分得的物理页号
PFN_NUMBER MmAllocPage(ULONG ConsumerType);
// 先检查配额,再检查空闲页面阀值,做好准备工作后,再才分配物理页面
NTSTATUS MmRequestPageMemoryConsumer(consumer, PFN* pfn);
// 释放指定消费者占用的指定物理页面,实际上是递减引用计数,引用计数减到0后就挂入系统空闲链表
NTSTATUS MmReleasePageMemory(consumer, pfn);
一个物理页面可以映射到N个进程的N个虚拟页面中,但一个虚拟页面同一时刻只能映射到一个物理页面。
每个虚拟页面又分四种映射状态:
1.映射着某个物理页面(已分配且已映射)
2.映射着某个磁盘页文件中的某个页面(已分配且已映射)
3.没映射到任何物理存储介质(对应的PTE=0),但是可能被预定了(已分配,但尚未映射)
4.裸奔(尚未分配,以上情况都不满足)
同一时刻,只有最频繁访问的那些虚拟页面映射着物理页面(否则物理内存早就用完了),这些虚拟页面被保存在一个工作集中,当访问工作集以外的虚拟页面时就会产生缺页异常,此时缺页异常处理函数会分配一个物理页面并将这个虚拟页面映射到这个物理页面中。