当前位置: 首页 > news >正文

《操作系统真象还原》第五章(2)——启用内存分页机制

文章目录

    • 前言
    • 为什么内存要分页?
    • 一级页表
      • 分段机制和分页机制
      • 分页机制的原理
    • 二级页表
      • 为什么还要有二级表?
      • 二级页表的结构
      • 二级页表如何寻址?
      • 一个具体的例子
      • 页目录项和页表项
    • 启动分页机制
      • 创建页目录和页表
      • 正式进入分页模式
      • 检验分页机制是否正确运行
    • 结语

前言

这是第五章第二部分,主要研究内存分页问题


为什么内存要分页?

核心问题是如果目前连续的物理内存不再够下一个进程载入了,怎么办?

目前我们的线性地址和物理地址都是连续的,并且一一对应,一个进程在一段连续的线性地址上,同时也在一段连续的物理地址上运行,而进程之间的内存碎片无法得到利用。

如果我们能接触物理地址和线性地址的对应就好了,这样进程在逻辑上用一段连续的线性地址,在物理上可以用不连续的物理地址,这样就解决了问题。因此引入内存分页。

内存分页的本质就是:通过建立页表,实现将连续线性地址映射到任意物理地址。


一级页表

分段机制和分页机制

分页机制建立在分段机制之上。分段机制是intel IA32架构的底层机制,无法修改。我们先简要介绍一下分段机制。

实模式下段寄存器里保存段基址,保护模式下段寄存器保存选择子,但归根结底内存访问的核心都是段基址:段内偏移地址,二者组合获得绝对地址,这个绝对地址就是线性地址。在分段机制下,这个线性地址就对应物理地址,可以直接送上地址总线,CPU拿来就能直接用。

分页机制建立在分段机制之上,段部件先照常工作处理出线性地址,如果此时打开了分页机制,这个线性地址就不再是实际的物理地址了,我们称之为虚拟地址,这个虚拟地址对应的物理地址是多少?这就是我们下一步要解决的问题。想要解决这个问题,要先了解分页机制的原理和页表的结构。

分页机制的原理

分页机制有两个作用:1.将线性地址转化为物理地址 2.用大小相等的页代替大小不等的段。

一个页的大小是4KB,页可以看作是地址空间的一种单位,它不是某个具体的物理地址或线性地址。4GB内存可以分成1M个页。一个页表就要有1M项,一个页表项对应一页内存(页表项大小并不是一页!),这个有1M项的页表就是一级页表。

那么,一个虚拟地址怎么对应一个页表的一个页呢?一个虚拟地址有32位,高20位用来对应一个页(220=1024*1024=1M,正好4GB内存分成了1M个页),低12位用来在页内寻址(212=4K)。

有两个注意事项:

  1. 分页机制打开前,要将页表地址(也就是页表项的起始地址)加载到控制寄存器cr3里,这样就把物理地址放到了页表项里。(这也是启用分页机制的先决条件)
  2. 页表的转换过程相当于在关闭分页机制下进行。也就是说页表本身的地址,以及通过页表项内容得到的地址,会被直接送上地址总线处理。(否则就会出现无限递归,结果被再次转换)。

高二十位对应的索引的公式为:高20位*4(一个页表项是4字节,所以乘4)+cr3保存的页表项起始地址。通过这个索引拿到页表项内容地址,这个地址+低12位地址=最终给CPU的物理地址。

来看一下书上的例子:

解释:假如目前有一个地址0:0x1234,经过段部件处理后得到虚拟地址为0x00001234,因为已经打开了分页机制,这个地址被送上页部件。高20位0x00001送入索引计算公式,得到索引,用索引读表,得到物理地址0x9000,再加上低12位0x234,得到最终的物理地址0x9234,这就是虚拟地址转化的物理地址。


二级页表

为什么还要有二级表?

一级表有这些问题:

  1. 一级页表中最多可容纳 1M(1048576)个页表项,每个页表项是 4 字节,如果页表项全满的话,便是 4MB 大小。
  2. 一级页表中所有页表项必须要提前建好,原因是操作系统要占用 4GB 虚拟地址空间的高 1GB,用户进程要占用低 3GB。
  3. 每个进程都有自己的页表,进程一多,光是页表占用的空间就很可观了。

归根结底,我们要解决的是:不要一次性地将全部页表项建好,需要时动态创建页表项。如何解决呢?引入二级页表。

二级页表的结构

首先,一个标准页(内存空间的一种单位)仍然是4KB,我们要映射4GB物理内存,要有1M个标准页。在一级页表中,这1M个标准页的索引被放在一个表里,在二级页表中,1M个标准页索引被分成1K组,每组1K个页的索引,可以认为我们建立了1K张表,每个表有1K个项,每个项储存着一个内存页的索引。

如何组织1K个表?引入一个页目录。页目录同样是一个表,有1K项,每个项对应一个表。一个页表项是4字节大小,1K个项正好是4K字节一个标准页的大小。最终形成了:

页目录(其中存有1K个表的表头地址)->某个表(其中存有1K个标准页的索引)->表项(对应某个具体的标准页)

示意图:

二级页表如何寻址?

首先我们先确定,我们有1K个页表,每个页表1K项,每个项对应1个标准页,标准页大小是4KB,总共就是1024x1024x4028=4GB,对应4GB物理内存。也就是说,一个32位物理内存地址,必然对应某个页表的某个项的物理地址。

32位地址的高10位(31-22位)用来在页目录中的页目录项(简称PDE)定位一个页表;中间10位(21-12位)用来在(页目录项对应的)页表中定位一个页表项(简称PTE);最低12位,用来对应(页表项对应的)具体页4KB内的具体地址。

同一级页表一样,访问任何页表内的数据都要通过物理地址。由于页目录项 PDE 和页表项 PTE 都是4 字节大小,给出了 PDE 和 PTE 索引后,还需要乘以 4,再加上页表物理地址,这才是最终要访问的绝对物理地址。转换过程背后的具体步骤如下。

  1. 用虚拟地址的高 10 位乘以 4,作为页目录表内的偏移地址,加上页目录表的物理地址,所得的和,便是页目录项的物理地址。读取该页目录项,从中获取到页表的物理地址。
  2. 用虚拟地址的中间 10 位乘以 4,作为页表内的偏移地址,加上在第 1 步中得到的页表物理地址,所得的和,便是页表项的物理地址。读取该页表项,从中获取到分配的物理页地址。
  3. 虚拟地址的高 10 位和中间 10 位分别是 PDE 和 PTE 的索引值,所以它们需要乘以 4。但低 12 位就不是索引值啦,其表示的范围是 0~0xfff,作为页内偏移最合适,所以虚拟地址的低 12 位加上第 2 步中得到的物理页地址,所得的和便是最终转换的物理地址。

一个具体的例子

先看一张图片:

假设目前有虚拟地址0:0x01234567,高10位是0x4,中间10位是0x234,低12位是0x567。高10位x4+页目录的物理地址,得到第一个中间量0x1000,这个中间量就是某个页表的页表地址。中间10位x4+0x1000,得到第二个中间量0xfa000,这个值就是某个标准页的物理地址。再加上最后12位指示的页内偏移地址0x567,就是最后给CPU的物理地址0xfa567。

页目录项和页表项

我们之前只是泛泛的谈到这两个部分,说它们是4字节大小,保存物理地址,现在详细介绍一下它们的结构。

我们知道一个标准页是4KB大小,所以每个标准页的页基址一定是4KB的倍数,这样32位中最低的12位(11-0位)一定就是0,我们可以利用这些位来记录一些属性。从低到高介绍一下。

  1. P,Present,意为存在位。若为 1 表示该页存在于物理内存中,若为 0 表示该表不在物理内存中。操作系统的页式虚拟内存管理便是通过 P 位和相应的 pagefault 异常来实现的。
  2. RW,Read/Write,意为读写位。若为 1 表示可读可写,若为 0 表示可读不可写。
  3. US,User/Supervisor,意为普通用户/超级用户位。若为 1 时,表示处于 User 级,任意级别(0、1、2、3)特权的程序都可以访问该页。若为 0,表示处于 Supervisor 级,特权级别为 3 的程序不允许访问该页,该页只允许特权级别为 0、1、2 的程序可以访问。
  4. PWT,Page-level Write-Through,意为页级通写位,也称页级写透位。若为 1 表示此项采用通写方式,表示该页不仅是普通内存,还是高速缓存。此项和高速缓存有关,“通写”是高速缓存的一种工作方式,本位用来间接决定是否用此方式改善该页的访问效率。直接置为 0 就可以。
  5. PCD,Page-level Cache Disable,意为页级高速缓存禁止位。若为 1 表示该页启用高速缓存,为 0 表示禁止将该页缓存。这里咱们将其置为 0。
  6. A,Accessed,意为访问位。若为 1 表示该页被 CPU 访问过啦,所以该位是由 CPU 设置的。这里页目录项和页表项中的 A 位也可以用来记录某一内存页的使用频率(操作系统定期将该位清 0,统计一段时间内变成 1 的次数),从而当内存不足时,可以将使用频率较低的页面换出到外存(如硬盘),同时将页目录项或页表项的 P位置 0,下次访问该页引起 pagefault 异常时,中断处理程序将硬盘上的页再次换入,同时将 P 位置1。
  7. D,Dirty,意为脏页位。当 CPU 对一个页面执行写操作时,就会设置对应页表项的 D 位为 1。此项仅针对页表项有效,并不会修改页目录项中的 D 位。
  8. PAT,Page Attribute Table,意为页属性表位,能够在页面一级的粒度上设置内存属性。比较复杂,将此位置 0 即可。
  9. G,Global,意为全局位。由于内存地址转换也是颇费周折,先得拆分虚拟地址,然后又要查页目录,又要查页表的,所以为了提高获取物理地址的速度,将虚拟地址与物理地址转换结果存储在 TLB(Translation Lookaside Buffer)中,TLB 以后咱们会细说。在此先知道 TLB 是用来缓存地址转换结果的高速缓存就 ok 啦。此 G 位用来指定该页是否为全局页,为 1 表示是全局页,为 0 表示不是全局页。若为全局页,该页将在高速缓存 TLB 中一直保存,给出虚拟地址直接就出物理地址啦,无需那三步骤转换。由于 TLB 容量比较小(一般速度较快的存储设备容量都比较小),所以这里面就存放使用频率较高的页面。
  10. AVL,意为 Available 位,表示软件和操作系统可用该位,CPU 不理会该位的值,我们也不必理会。

启动分页机制

启用分页机制,我们要按顺序做好三件事。

  1. 准备好页目录表及页表。
  2. 将页表地址写入控制寄存器 cr3。
  3. 寄存器 cr0 的 PG 位置 1。

创建页目录和页表

先简单分析一下操作系统和用户进程之间的关系。一个用户进程想要正常运行,必须和操作系统配合,所以操作系统必须要共享给所有的用户进程。我们通过页表实现这一点。我们把虚拟地址空间的3-4GB给操作系统,0-3GB给用户进程。

我们的操作系统大小在1MB以内,MBR,Loader,内核都在物理内存0-0xfffff这1MB空间里。前面说过为了共享,需要把这1mb物理内存对应到0xc0000000-0xc00fffff(3gb到3gb+1mb)这段虚拟线性地址上。

我们的页目录建立在物理内存1mb之后第一个4kb空间上,页目录每一项对应一个页表,一个页表对应4mb内存空间,所以我们页目录项第0项对应的页表对应0-0x003fffff共4mb物理空间,包括了前1mb空间。我们先分配1mb空间,剩下3mb暂时不分配。

在加载内核之前,CPU一直在运行Loader,它必须在分页机制启动前后都可以正常运行。段机制下,这部分线性地址就是0-0x3fffff,分页机制下,这部分虚拟内存是0xc0000000-0xc03fffff,所以我们用两个页表对应它,也就是两个页目录项。

代码如下:

;-------------------- 加载内核到缓冲区 --------------------------------

;-------------------- 启动分页 --------------------------------

;-------------------- 跳转到内核区 --------------------------------

;-------------------- 创建页目录和页表 --------------------------------
;首先先明确一点,那就是我们这里只处理低1mb的页表,目标是让0~1mb和3gb~3gb+1mb都映射低1mb
setup_page:
;先逐字节清0
	mov ecx,4096		;循环4096次
	mov esi,0
.clear_page_dir:
	mov byte [PAGE_DIR_TABLE_POS+esi],0
	inc esi
	loop .clear_page_dir	;清理出4KB的空间
	
;创建两个页目录项
.create_pde:
	mov eax,PAGE_DIR_TABLE_POS
	add eax,0x1000		;起始位置+4KB,指向第一个页表,为后面的创建页表项作准备
	mov ebx,eax
	
	or eax,PG_US_U|PG_RW_W|PG_P	;将后12位和前20位地址拼接在一起,组成一个表项
					;后面三个或后=0x7,代表这个目录项允许任意特权级访问,可写,后续我们会用到init用户级进程。
	
	;将页目录0和0x300项都指向第一个页表的地址,都对应0-4mb的物理地址。
	mov [PAGE_DIR_TABLE_POS+0x0],eax	;0项
	mov [PAGE_DIR_TABLE_POS+0x300],eax	;0x300项转化为10进制是3*256=768
	
	sub eax,0x1000
	mov [PAGE_DIR_TABLE_POS+4092],eax	;在页目录最后一项1023项写入页目录本身的地址,所以是4096-4
	
;创建页表项
	mov ecx,256			;目前我们先安排低1mb内存,1M/4k=256项
	mov esi,0
	mov edx,PG_US_U|PG_RW_W|PG_P	;edx存每个表项的相对地址,初始为0+0x7
.create_pte
	mov [ebx+esi*4],edx		;每过4字节有一项,每项存着一页,一页是4k
	add edx,4096
	inc esi
	loop .create_pte		;循环256次
	
;创建内核其他页表的PDE
;这部分是3gb后的部分,共享给所有用户线程的,所以只关注页目录768项后的项,1023项用来存页目录本身的地址,不需要处理
;这里创建剩下的769~1022=254项
	mov eax,PAGE_DIR_TABLE_POS
	add eax,0x2000		;从第二个页表开始,映射到目录里
	or eax,PG_US_U|PG_RW_W|PG_P
	mov ebx, PAGE_DIR_TABLE_POS 
	mov ecx,254 		;范围为第 769~1022 的所有目录项数量
	mov esi,769 
.create_kernel_pde: 
	mov [ebx+esi*4],eax
	inc esi 
	add eax,0x1000 
	loop .create_kernel_pde 
	ret

正式进入分页模式

这部分代码比较简单,就是所谓的三部曲:

  1. 准备好页目录表及页表。
  2. 将页表地址写入控制寄存器 cr3。
  3. 寄存器 cr0 的 PG 位置 1。

1我们上面已经完成了。

这部分代码如下:

;-------------------- 启动分页 --------------------------------
	call setup_page		;创建页目录和页表
	sgdt [gdt_ptr]		;要将描述符表地址及偏移量写入内存 gdt_ptr,一会儿用新地址重新加载
	
	mov ebx,[gdt_ptr+2]	;先获取gdt基址,基址存在后四位
	or dword [ebx+0x18+4],0xc0000000	;将 gdt描述符中视频段描述符中的段基址+0xc0000000
						;视频段是第 3 个段描述符,每个描述符是 8 字节,24=0x18 
						;段描述符的高 4 字节的最高位是段基址的第 31~24 位
	add dword [gdt_ptr+2],0xc0000000	;将 gdt 的基址加上 0xc0000000 使其成为内核所在的高地址

	add esp,0xc0000000	;将栈指针同样映射到内核地址

	;把页目录地址赋给 cr3 
	mov eax,PAGE_DIR_TABLE_POS 
	mov cr3,eax 

	;打开 cr0的 pg位(第 31 位)
	mov eax,cr0 
	or eax,0x80000000 
	mov cr0,eax 

	lgdt [gdt_ptr]		;在开启分页后,用 gdt 新的地址重新加载
	mov byte [gs:160],'V'	;视频段段基址已经被更新,用字符 v 表示 virtual addr
	
	jmp $

注意我们已经删掉了之前的这部分代码:

	mov ax,SELECTOR_VIDEO
	mov gs,ax 
	mov byte [gs:160], 'P' ;通过显卡打印一个字符,验证是否进入保护模式

检验分页机制是否正确运行

老样子,编译+写入+运行bochs

nasm -I include/ -o loader.bin loader.S
dd if=/home/hongbai/bochs/loader.bin of=/home/hongbai/bochs/bin/c.img bs=512 count=4 seek=2 conv=notrunc
cd bin
./bochs -f bochsrc.disk

输入6和c,结果如下

出现了预定结果V

ctrl+c退出,再输入info gdt

出现了一组报错,大意是gdtr寄存器+8*x指向了一个无效的地址0xc0000903。

我尝试了强制清零0号段描述符,以及先打开分页模式再调整gdt两种方法,都没有解决这个报错,暂且搁置,写完内核后再回来解决。

输入info tab,出现

这部分和书上一致,说明内存分页已经开启。


结语

这是第五章第2部分,开启分页模式。基本完成了任务,还有一个小报错留待后续解决。下一部分就要进入内核了,尽情期待。

http://www.dtcms.com/a/112040.html

相关文章:

  • 蓝桥杯15届 宝石组合
  • 【HC-05蓝牙模块】基础AT指令测试
  • 思维链 Chain-of-Thought(COT)
  • 视野,,地面覆盖,重叠需求,FPS,飞行速度等的计算公式
  • LLM面试题五
  • JVM 有哪些垃圾回收器
  • 【2023】ORIGIN或MATLAB 颜色图,等高图,颜色条——需要拟合补全中间的颜色
  • 算法--最长上升子序列
  • 京东零售首次公开!6B参数时序大模型实现20000款商品自动补货预测
  • Java 搭建 MC 1.18.2 Forge 开发环境
  • 《探索边缘计算:重塑未来智能物联网的关键技术》
  • agent 入门
  • ARM-外部中断,ADC模数转换器
  • Vue3学习二
  • 【Node】一文掌握 Express 的详细用法(Express 备忘速查)
  • 【面试篇】Mysql
  • DHCP之中继 Relay-snooping及配置命令
  • Python_level1_字符串_11
  • 给项目中的用户头像,添加用户的历史头像记录功能
  • 深入理解SQL中的<>运算符:不等于的灵活运用
  • C++20的协程简介
  • 轨迹速度聚类 实战
  • 【C++代码整洁之道】第九章 设计模式和习惯用法
  • VSCode运行,各类操作缓慢,如何清理
  • anaconda3/conda依赖安装、环境配置、关联指定python版本
  • 性能测试之jmeter的基本使用
  • [C++面试] new、delete相关面试点
  • 从软件分层架构视角理解英语学习
  • 为什么有的深度学习训练,有训练集、验证集、测试集3个划分,有的只是划分训练集和测试集?
  • 【YOLO系列(V5-V12)通用数据集-X光包裹内违禁品检测数据集】