手写操作系统(5)——CPU工作模式与虚拟地址(下)

本文涉及的产品
公网NAT网关,每月750个小时 15CU
简介: 手写操作系统(5)——CPU工作模式与虚拟地址

手写操作系统(5)——CPU工作模式与虚拟地址(上):https://developer.aliyun.com/article/1508574

长模式

长模式最早是由AMD指定的标准。相比于保护模式,长模式进一步将地址拓宽到了64位,并弱化了内存的段管理机制,采用页面管理的方式并引进了MMU进行内存地址转换。

内存寻址

长模式下的寄存器最大可使用64位,最小可使用8位,如下:

长模式下段描述符格式如下:

在长模式下,CPU 不再对段基址和段长度进行检查,只对 DPL 权限进行相关的检查。

开启长模式

来看看如何实现保护模式切换到长模式:

  • 准备长模式全局段描述符
ex64_GDT: 
null_dsc:  dq 0 ;第一个段描述符CPU硬件规定必须为0 
c64_dsc:dq 0x0020980000000000  ;64位代码段 
;无效位填0
;D/B=0,L=1,AVL=0 ;P=1,DPL=0,S=1;T=1,C=0,R=0,A=0
;段长度和段基址都是无效的填充为 0,CPU 不做检查。
;但是上面段描述符的 DPL=0,这说明需要最高权限即 CPL=0 才能访问。
d64_dsc:dq 0x0000920000000000  ;64位数据段 
;数据段的话,G、D/B、L 位都是无效的
eGdtLen   equ $ - null_dsc  ;GDT长度 
eGdtPtr:dw eGdtLen - 1  ;GDT界限 
dq ex64_GDT 
  • 准备MMU页表
mov eax, cr4
bts eax, 5   ;CR4.PAE = 1
mov cr4, eax ;开启 PAE
mov eax, PAGE_TLB_BADR ;页表物理地址,假设页表数据已经准备好了
mov cr3, eax
;上面的操作是为了指定页表位置

提一句,长模式有关内存的保护都由MMU来进行,而MMU主要根据页表对内存地址进行转换,CPU的CR3寄存器指向页表

  • 加载GDT到GDTR寄存器
lgdt [eGdtPtr]
  • 开启长模式
;开启 64位长模式
mov ecx, IA32_EFER
rdmsr
bts eax, 8  ;IA32_EFER.LME =1
wrmsr
;开启 保护模式和分页模式
mov eax, cr0
bts eax, 0    ;CR0.PE =1
bts eax, 31
mov cr0, eax 
  • 加载段寄存器,刷新CS寄存器
jmp 08:entry64 ;entry64为程序标号即64位偏移地址

同保护模式的作用。

虚拟地址

基本概念

在多道程序的场景下,关于内存有四个核心问题

  • 多个程序之间如何保证内存地址不冲突?由操作系统决定还是多个程序来决定?
  • 如何保证多个程序之间不会访问彼此的内存?保护模式?
  • 如何解决内存容量的问题?即内存装不下了。
  • 每台计算机的内存是不一样的,如何保证程序在这些计算机上兼容运行?

解决上述问题的一个方案是:每个程序都享有一个从0到最大内存的地址空间,这个地址是程序之间独立的,每个程序所私有的。

上述方案就是虚拟地址空间

如何形象化地理解虚拟地址空间?关键就在于映射。

假设实际物理内存地址空间大小从0~999

我们让程序A、B、C觉得自己能够使用的内存地址空间也为0~999,它们可以随意访问这些地址。

等等……这样三个程序难道不会冲突么?要是三个程序同时访问599地址怎么办?

关键来了,那就是地址映射表。三个程序分别有三个表,其中索引为每个程序访问的地址,结果为实际物理内存地址,假设程序A的映射表如下:

索引(虚拟地址) 结果(实际地址)
599 123
123 599

每个程序都有一个上面这样的映射表,表示自己的地址与实际地址的一一对应关系,我们将这个映射表称为页表。不用考虑会不会出现两个程序映射的实际地址是一样的情况,放心,不存在的,操作系统已经协调好嘞!

PS:程序自身的地址其实是由链接器生成的,链接阶段就是对多个模块进行地址的重排和引用。

页表

值得注意的是,如果页表中存储的是每一个虚拟地址到物理地址的映射关系,那么整个内存只能全部用来存储这个映射关系了。

整个地址空间存储的都是每个虚拟地址对应的实际地址,那么内存还有空余么?

我们采用一种折中的方式,**将地址空间分成一个一个小块,**每一块的大小可以为1KB、2KB、4KB甚至1GB等,其中小块也称为页,这就是分页模型。将原本每一个地址与地址的映射关系,改为虚拟也与物理页的对应关系,于是这样的映射关系如下:

MMU下一节会讲,简单理解它就是一个加快映射的硬件。

经过上面的讨论,也许会认为分页模型就是简单的一张表,从而实现虚拟地址到物理地址的转换,但实际设计中却并没有这么简单。

假设有4GB的内存(32位机),将分页大小设为4KB,于是一共有1M个内存页。

那么存储这些对应关系需要多少内存?

如果一个页表映射关系4B,1MB个内存页映射就需要4MB连续的内存空间,当以后内存地址空间增大,用于存储映射关系的空间还会继续膨胀,浪费内存空间

因此,实际的页表使用一般是采用分级的思想,类似于查字典,先查部首或者拼音首字母,再查询接下来的部分,如下图是一个三级页表的概念图

以一次三级页表查询为例,将虚拟地址分为了四段

  • 第一次,MMU使用虚拟地址第一段的中间页目录索引去访问顶级页目录,获得了中级页目录的地址;
  • 第二次,MMU使用第二段的页表地址索引去访问中级页目录,获得了页表的地址
  • 第三次,MMU使用第三段的物理页地址去访问页表,获得了物理内存页的地址
  • 第四次,MMU使用第四段的偏移地址在物理内存页找到真实的内存地址从而完成一次内存访问

从上述流程可以看出,多级页表有一个显著缺点——增加内存访问次数

原本我只想访问一个内存地址,但是由于三级页表的存在,我需要先进行多次中间页表的获取,才能够进行最终的内存访问。

它的优点也很明显,每一级页表所占用的空间减小,可以离散存储页表,并且在某种程度上节省页表的内存空间

PS:因为对于一些页,甚至不需要构建它的完整页表,因为没有使用。


比如hello world程序,这样一个几kb的程序却需要4MB的内存空间是很浪费的。如果采用二级页表,那么一级页表只需要4KB的空间用来索引二级页表的地址,像hello world这样的程序可能只需要一个物理页,那么只需要一条记录就可以了,故对于二级页表也只要4KB就足够了,而一级页表中的其他表项可能为空,所以这样只需要8KB就能解决问题。


但是如果因为节约内存而增加内存访问次数,似乎总感觉有点不得劲……

工程师为了解决这个问题,给MMU配了一个伙伴——TLB(快表,或者叫做页表缓存、旁路转址缓存),它是CPU的一个cache,主要工作就是缓存最近使用的页表,不用再次去查询内存,由于cache速度跟CPU相差无几,因此页表的查询效率有了很大的提升。

MMU

上面提到了MMU,那么MMU到底是什么?

MMU译作内存管理单元,是一个硬件设备,它大都集成在CPU中,也可以作为独立芯片置于CPU与总线之间,其主要工作是通过虚拟地址与页表(地址转换关系表)获得物理地址

MMU只能在保护模式或者长模式下才能够开启,在保护模式下也必须使用保护模式平坦模型,使得分段形同虚设,详细内容见之前的保护模式,分段模型下的分页模式如下:


接下来来看看在保护模式和长模式下的MMU是怎么工作的,如何完成地址转换?

保护模式

保护模式下CPU位数为32,地址空间从0~4GB-1。

假设分页大小为4KB采用二级页表的方式,于是32位的虚拟地址就被划分为3段:页目录索引、页表索引、页内偏移


它们之间的对应关系为:页目录中有1024个页表,每个页表中有1024页,每页的大小为4KB,则空间为1K*1K*4K=4GB

保护模式下,页目录存储在CPU的一个CR3寄存器中,MMU正是据此找到页目录的


CR3寄存器中值、页目录表、页表这三者分别的格式如下:


image.png


仔细分析上图三个格式可以看到低10位被用作页面相关属性,这是由于三者每一个都是32位4字节,1024项正好是4KB,从而在物理地址中4K对齐,从而低10位其实不影响其地址计算,可以另做它用,这与保护模式的段选择子的设置有异曲同工之妙。

若是将分页大小设为4MB,则只有一级页表,同样要进行4K对齐,方便查找与设置页面属性。

长模式

长模式下4K的分页模型,将64位的虚拟地址分为了6段:保留段、顶级目录索引、页目录指针索引、页目录索引、页表索引、页内偏移,也就是四级页表结构,见下:

从上图可以看出,每个目录项有512个表项,每个表项的大小为64位8字节,总共可以表示的大小为:0.5K0.5K0.5K0.5K4K=0.25P!但其实实际使用中没有利用这么大的内存,只是有这个潜力。

来看看此时的CR3、顶级页目录项、页目录指针项等的格式:

总之,不管是保护模式还是长模式,在使用分页模型下的虚拟地址时,都是根据自身位数以及层级的关系来确定最终的物理地址的,为方便查找,往往使用4KB对齐的方式。

页缺失

有没有可能MMU出现转换失败?

比如页表中不存在对应关系、或是权限不足等情况?

这些情况是可能存在的,当出现这种情况时,MMU会触发中断——页中断的东西,而后让CPU来处理相应的逻辑,当逻辑处理完毕之后,再由MMU来进行地址转换,这个部分留到第7章来讲。

进程隔离

在分页模型下,由于没有了分段模型对各个段的保护,那么进程之间是如何进行地址隔离的呢?

——每一个进程在运行的时候,页表数据都是不一样的!

也就是说每一个进程都有一个独立的页表,当进程切换时,页表数据也会随之切换,这种方式就将进程的地址进行了隔离,从而不会造成冲突。


如何获取内存视图?

前面我们一直在讲内存相关的知识,那么当电脑开机的时候,如何获取当前电脑中的物理内存状态呢?比如如何知道物理内存有多少GB?值得注意的是,给出一个物理地址并不能准确地定位到内存空间,内存空间只是映射物理地址空间中的一个子集,物理地址空间中可能有空洞,有 ROM,有内存,有显存,有 I/O 寄存器,所以获取内存有多大没用,关键是要获取哪些物理地址空间是可以读写的内存

在第4章介绍计算机启动的过程中提到,BIOS启动阶段会设置计算机的中断向量表,恰好其中有一个中断就可以用来获取内存视图,即INT 15H,该中断需要在实模式下运行,因为切换到保护模式时,中断向量表会重设(变成中断门了)

_getmemmap:  xor ebx,ebx ;ebx设为0  mov edi,E80MAP_ADR ;edi设为存放输出结果的1MB内的物理内存地址loop:  mov eax,0e820h ;eax必须为0e820h;输出结果数据项的大小为20字节=;8字节内存基地址,8字节内存长度,4字节内存类型  mov ecx,20   mov edx,0534d4150h ;edx必须为0534d4150h  int 15h ;执行中断  jc error ;如果flags寄存器的C位置1,则表示出错  add edi,20;更新下一次输出结果的地址  cmp ebx,0 ;如ebx为0,则表示循环迭代结束  jne loop  ;还有结果项,继续迭代    reterror:;出错处理

上述迭代过程中的中断每次执行都会输出20字节大小的数据项,最后这些数据项形成一个数组,我们用C语言结构体来表示一下这个数据项,便于理解之后的源码:

#define RAM_USABLE 1 //可用内存#define RAM_RESERV 2 //保留内存不可使用#define RAM_ACPIREC 3 //ACPI表相关的#define RAM_ACPINVS 4 //ACPI NVS空间#define RAM_AREACON 5 //包含坏内存typedef struct s_e820{    u64_t saddr;    /* 内存开始地址 */    u64_t lsize;    /* 内存大小 */    u32_t type;    /* 内存类型 */}e820map_t;

补充:缓存

上面提到加快页表查询的硬件cache——TPL快表。

现代计算机,由于程序局部性的存在以及CPU处理速度与内存速度的脱节,cache已经成为标配,并且在多核CPU中,cache还是分级的:


1级2级cache是单个核心独有的,3级cache是多个核心共享的。但是这也会引发一个问题——数据一致性,比如两个核心都对某一个数据进行了修改,由于cache的存在就可能会造成数据不一致。


为了解决这个问题,出现了MESI和MOESI协议。以MESI协议为例,它将cache中的内容分为了四种状态:

  • M(modified,修改的)
  • E(Elusive,独占的)
  • S(Shared,共享的)
  • I(Invalid,无效的)

更详细一点(来自):

最开始只有一个核读取了A数据此时状态为E独占,数据是干净的;

后来另一个核又读取了A数据,此时状态为S共享,数据还是干净的

接着其中一个核修改了数据A,此时会向其他核广播数据已被修改,让其他核的数据状态变为I失效,而本核的数据还没回写内存,状态则变为M已修改

等待后续刷新缓存后,数据变回E独占,其他核由于数据已失效,读数据A时需要重新从内存读到高速缓存,此时数据又共享了。

X86 CPU默认将cache关闭,可以通过将CR0寄存器的CD设置为0(打开cache)、NW设置为0(维护内存数据一致性)

mov eax, cr0
;开启 CACHE    
btr eax,29 ;CR0.NW=0
btr eax,30  ;CR0.CD=0
mov cr0, eax

参考链接

05 | CPU工作模式:执行程序的三种模式

06 | 虚幻与真实:程序中的地址如何转换?

07 | Cache与内存: 程序放在哪儿?

IDTR、TSS、GDT、LDT详解

BTS汇编指令

相关实践学习
每个IT人都想学的“Web应用上云经典架构”实战
基于阿里云,构建一个企业web应用上云经典架构,让IT从业者体验企业级架构的实战训练。
相关文章
|
5天前
|
虚拟化 iOS开发 MacOS
客户机操作系统已禁用 CPU。请关闭或重置虚拟机。解决方法
客户机操作系统已禁用 CPU。请关闭或重置虚拟机。解决方法
8 0
|
2月前
|
存储 算法 Linux
【计算机操作系统】深入探究CPU,PCB和进程工作原理
【计算机操作系统】深入探究CPU,PCB和进程工作原理
|
2月前
|
存储 缓存 Linux
手写操作系统(5)——CPU工作模式与虚拟地址(上)
手写操作系统(5)——CPU工作模式与虚拟地址
33 0
|
2月前
|
安全 编译器 程序员
CPU处理器模式与异常
CPU处理器模式与异常
91 0
|
2月前
|
Linux
Linux操作系统调优相关工具(一)查看CPU负载相关工具 找出系统中使用CPU最多的进程?
Linux操作系统调优相关工具(一)查看CPU负载相关工具 找出系统中使用CPU最多的进程?
35 0
|
25天前
|
存储 Linux 数据处理
探索Linux操作系统的内核与文件系统
本文深入探讨了Linux操作系统的核心组件,包括其独特的内核结构和灵活的文件系统。文章首先概述了Linux内核的主要功能和架构,接着详细分析了文件系统的工作原理以及它如何支持数据存储和检索。通过比较不同的文件系统类型,本文旨在为读者提供一个关于如何根据特定需求选择合适文件系统的参考框架。
|
16天前
|
存储 缓存 安全
Linux基础——冯诺依曼体系结构与操作系统
Linux基础——冯诺依曼体系结构与操作系统
42 1
Linux基础——冯诺依曼体系结构与操作系统
|
12天前
|
Linux 网络安全 虚拟化
Linux操作系统第一章(Linux操作系统的带入,vmware-17虚拟化软件运用,FinalShell远程连接Linux )
Linux操作系统第一章(Linux操作系统的带入,vmware-17虚拟化软件运用,FinalShell远程连接Linux )
|
17天前
|
Linux API 云计算
探索Linux操作系统的模块化设计
【6月更文挑战第12天】本文深入探讨了Linux操作系统的模块化设计,揭示了其背后的技术原理与实践应用。通过分析Linux内核的模块化特性,我们了解到这一设计如何促进了系统的灵活性、可维护性以及安全性。文章将详细阐述模块加载机制、模块间的通信方式以及模块化给系统开发和维护带来的影响。
|
17天前
|
网络协议 Linux 数据安全/隐私保护
【Linux操作系统】权限管理和粘滞位
【Linux操作系统】权限管理和粘滞位
【Linux操作系统】权限管理和粘滞位

热门文章

最新文章