我们知道linux下编程cpu接触的都是虚拟地址,而最终访问的实际物理内存。因此需要在虚拟地址和实际物理地址之间建立一种映射关系,即页表。
使用虚拟地址有以下几个优势:
- 进程之间地址隔离
因为每个进程都有独立的4G虚拟地址空间,且包括一套自己的虚拟地址到物理地址映射页表。因此各个进程可以使用相同的虚拟地址,只不过最终映射到不同的物理内存上(共享内存除外),互不干扰,提高系统安全性。
- 管理进程对内存的访问权限
通过页表,标识进程对该内存的读写访问,保证进程对内存的正确访问
虚拟地址到物理地址之间映射通常采用4级页表实现,注意所有的页表均放在内存中,且页表中保存的地址均为物理地址。
CPU读取内存数据过程如下:
CPU访问内存时通过MMU的硬件将虚拟地址转化为物理地址以提高效率。MMU首先从映射关系缓存器TLB中查找,若存在则直接获取对应物理地址并访问。若TLB中没有找到,则通过Table walk unit单元进行虚拟地址到物理地址的转化。下面以arm64位系统为例进行说明具体转化过程。
arm64位地址为64位,其中用户空间使用的范围为0x0000 0000 0000 0000 ~ 0x0000 FFFF FFFF FFFF。线性地址转换物理地址过程如下图所示:
64位系统通常采用四级页目录映射,其中涉及名词解释:
PGD:页全局目录---Page global directory(L0)
PUD:页上级目录---Page Upper directory(L1)
PMD:页中级目录---Page middle directory(L2)
PT:直接页表---Page table(L3)
64位系统用户态进程只使用低位的48位,这48位线性地址被分为9位PDG页表索引、9位PUD也表索引、9位PMD页表索引,9位PT也表索引和12位的物理页内地址偏移。如上图所示。
以上图中线性虚拟地址为例,说明具体的转化过程。图中红色数字表示对应的区间bit位数值。
- 首先根据页表基地址寄存器获取当前进程的页全局目录PDG页表地址(发生进程切换时该寄存器值需要更新)。
- 根据线性地址bit39-bit47对应的数值0x008=8找到PDG页表的偏移为8的项,即图中标黄的Entry 8,该项存放了下一级页目录PUD页表的基地址,如图中蓝色实线所指示。
- 根据线性地址bit30-bit38对应的数值0x012=18找到步骤2中获取的PUD页表中偏移为18的项,即图中标黄的Entry 18,该项存放了下一级页目录PMD页表的基地址,如图中蓝色实线所指示。
- 和步骤2、3类似,逐步通过PMD页表、PT页表基地址,最终获取对应的物理页的基地址,即上图中PT页表的Entry 642项所存放的地址,也即最终物理地址的bit12-bit47位。
- 根据步骤4中所获取的实际物理页地址,再根据页内偏移(即线性地址的bit0-bit11对应的值0x1DE=478)找到物理页的第478个字节,即图中标黄的Byte 478为线性虚拟地址对应的物理地址位置。自此完成虚拟地址到物理地址转化过程。
arm32的32位线性虚拟地址与上述类似,其分别为PD(Page directory, bit10) + PT(Page table, bit10) + Offset(bit12)。
采用多级页表有如下好处:
- 节省内存。实际进程运行过程中不需要为所有虚拟地址分配对应的页表保存映射空间。
- 可以离散存储。通过多级页表,可以将进程的地址映射关系保存在多个分散页表中,而不需要一整块很大的连续内存。如32位系统,如果只采用一级页表,则该页表一次性得保存2^20项物理页地址,也即2^20 * 4 = 4M页表。这对一次分配这么大空间有较大压力。
- 可以按需分配各级页表。