文章目录
- 前言
- 介绍页表、页框、页目录的概念
- 页框
- 页表
- 页目录
- 页表和页目录的分配
- 一级页表和二级页表
- 一级页表
- 寻址过程
- 二级页表
- 寻址过程
- 一级页表和二级页表的对比
前言
我们知道每个进程都有属于自己的虚拟地址空间,且每个进程的虚拟地址都是统一的。要想通过虚拟地址访问物理地址,我们就需要借助页表来建立映射关系。下面对于如何借助页表来寻址展开详细解释(以32位操作系统为例)。
介绍页表、页框、页目录的概念
页框
页框(Page Frame)是物理内存中的固定大小的块,通常为4KB。在物理内存中,页框是用于存储实际数据的最小单元。假设内存大小为4GB,也就能划分出1024*1024个页框。当进程通过页表来访问物理地址时,其本质就是找到页框的地址。
于是,操作系统对内存的管理,就可以看成是对页框即块的管理。那内核中又是如何描述一个页框的呢?
在Linux内核中,使用struct page
结构来描述内存中的每一页框。这个结构体定义在内核头文件include/linux/mm_types.h
中。该结构体通常有以下字段:
flags
:标志位,表示页面的各种状态mapping
:指向该页框所属的地址空间index
:该页框在地址空间中的偏移量_maocount
:记录该页框到页表条目数_refcount
:页面引用计数。表示有多少用户正在使用该页框
页表
页表是用于存储物理内存与虚拟内存地址之间的映射关系的结构。每个进程都有属于自己的页表。页表中的每一个表项,都对应着一个页框的物理地址。一个页表项(PTE)通常包括以下几个字段:
- 页框基地址:页框的第一个字节的地址
- 控制位:包括存在位(P)、读/写位(R/W)、超级用户位(U/S)等。
给出32位页表项的控制位示例:
(上图来自知乎)
再给出内核中描述页表项的结构体代码,该结构体其实是一个位段,所有变量加起来一共32个bite位,即4字节:
// 页表项(PTE)的定义
typedef struct {
uint32_t present : 1; // 存在位
uint32_t rw : 1; // 读/写位
uint32_t us : 1; // 用户/超级用户位
uint32_t pwt : 1; // 页级写穿透位
uint32_t pcd : 1; // 页级缓存禁用位
uint32_t accessed : 1; // 访问位
uint32_t dirty : 1; // 脏位
uint32_t pat : 1; // 页属性表位
uint32_t global : 1; // 全局位
uint32_t available : 3; // 可用位
uint32_t page_frame_base : 20; // 页框基地址
} __attribute__((packed)) page_table_entry_t;
页表其本质就是这种结构体数组。
页目录
页目录是用来管理页表的结构。页目录中每一表项都指着一张页表的物理地址。一个页目录项(PDE)通常包括以下几个字段:
- 页表基地址:页表的首地址(物理)
- 控制位:包括存在位(P)、读/写位(R/W)、超级用户位(U/S)等。
给出32位页目录项的控制位示例:
(上图来着知乎)
同样,给出内核中描述页目录项的代码:
// 页目录项(PDE)的定义
typedef struct {
uint32_t present : 1; // 存在位
uint32_t rw : 1; // 读/写位
uint32_t us : 1; // 用户/超级用户位
uint32_t pwt : 1; // 页级写穿透位
uint32_t pcd : 1; // 页级缓存禁用位
uint32_t accessed : 1; // 访问位
uint32_t reserved : 1; // 保留位
uint32_t ps : 1; // 页大小(通常为4KB页)
uint32_t ignored : 1; // 忽略位
uint32_t available : 3; // 可用位
uint32_t page_table_base : 20; // 页表基地址
} __attribute__((packed)) page_directory_entry_t;
综上,我们知道了页框、页表、页目录的概念,以及彼此之间的联系。那么在一个进程建立的时候, 页表和页目录的内容是固定的吗? 答案是否定的。
页表和页目录的分配
在Linux内核中,新进程的页目录表的内容是动态增加的。这意味着页目录表和页表的分配更新会根据实际情况动态进行,而不是在创建进程时一次初始化所有的页表项。
大致步骤如下:
- 进程创建,建立并初始化新页目录表。该表大部分内容为空,有部分指向共享内核。
- 访问虚拟地址,如果该地址对应的页表项不存在,触发页故障。内核捕捉这个故障,然后给进程分配对于的页表,并更新页目录表页页表。
一级页表和二级页表
在操作系统中,分页机制可以采用单级页表和多级页表进行地址映射。linux采用的就是多级页表。不同的分页机制会导致寻址的空间成本不同(页表本身也占空间)。下面以一级页表和二级页表为例,分析各自的空间成本。
单级页表:只有一张大的页表来映射虚拟空间和物理空间。结构较为简单,直接映射。
多级页表:分层次的管理地址空间,且一般都有一张页目录表。结构较为复杂,需要多次转换地址。
一级页表
总共就一张表。
- 页表项大小:通常每一个页表项都是4个字节,即32个比特位。
- 页表项数目:因为4GB内存一共有1024*1024个页框,要想映射所有页框。也就需要1024*1024个页表项。
一级页表的总大小:4bite10241024=4MB
当我们采用一级页表。给出一个虚拟地址(32位),如何找到目标的物理地址(某一字节)呢?
寻址过程
将虚拟地址分解为两个部分:
- 页表索引:高20位。因为一张页表的总项数为1024*1024=2^20(页框数),虚拟地址的高20位用来表示页表项的位置。
- 页内偏移量:低12位。通过高20位能找到一个唯一的页表项,即页框的物理地址。一个页框占4096个字节,用12位比特位恰好能表示4096个不同的位置。于是通过低12位比特位的组合,我们就能找到页框内的唯一一个字节。
假设现在有一个虚拟地址为:
0x0000 1005
取出高20位的十进制值为1,低12位的十进制值为5.于是在页表中找到下标为1的页表项,通过这个页表项我们能找到一个唯一的页框。再在该页框中从找到第5个字节的地址。该地址就是0x0000 1005
对应的物理地址。
二级页表
在二级页表中,最外层的页表我们当成一个页目录。页目录中的每一项都指向一张页表。一共有1024个页目录项,即一共有1024张页表,每一张页表有1024个页表项,刚好对应1024*1024个页框。
1.页目录大小计算:
- 页目录项大小:4字节。
- 页目录数量:1024.
页目录总大小:4byte*1024=4KB
2.每个页表的大小计算:
- 页表项大小:4字节
- 页表项数量:每一个页表都有1024个页表项
每个页表的大小:10244byte=4KB
页表的总大小:10244KB=4MB
二级页表的总大小:4MB+4KB约等于4MB
寻址过程
将虚拟地址分为三个部分:
- 页目录索引:高10位用来索引页目录项,10个二进制位最多表示1024个位置。
- 页表索引:中间10位索引页表项
- 页内偏移量:低12位用于表示页内偏移量,12个二进制位最多表示4096个位置,刚好对应页框内每个字节的相对位置。
当我们拿到一个虚拟地址,取出高10位的二进制表示的值,作为页目录的下标找到目标页表。再取出中间10位二进制表示的值,用来作为目标页表的下标,来找到目标页框。最后取出低12位,在目标页框中找到唯一一个字节地址。
假设现在有一个虚拟地址
0x00001005
,得到页目录索引为0,页表索引为1,页内偏移量为5.所以我们就去下标为0的页目录项种去找到页表,并在该页表下标为1的页表项中找到页框,最后在该页框中找到第5个字节的地址。
一级页表和二级页表的对比
尽管从理论上看,一级页表和二级页表的总空间成本差不多。但是二级页表的使用效率会更高。
如果采用一级页表,每个进程都需要分配整个页表的内存,也就意味着每个进程都需要有4MB的空间来存放页表,且页表项很多都是空的。这对大多数应用来说都是一种浪费。如果采用二级页表或者三级页表。每个进程拥有一张页目录,页表的数量会根据实际情况来进行分配,因此实际内存小于4MB。这样一来,避免了为进程分配大量未使用的页表,从而节省了内存。
从时间效率上来看,虽然一级页表直接可以找到页框的地址,而多级页表需要经过多次索引,但是多级页表带来空间上的节省是值得多消耗一些索引时间的