以内存为开始是正确的学习顺序
内存地址
首先引出三种地址:
- 逻辑地址
包含在机器语言指令中用来指定一个操作数或一条指令的地址。每个逻辑地址都由一个段和偏移量组成,它支持程序员把程序分成若干段。
- 线性地址:
也可以称它为虚拟地址
- 物理地址:
顾名思义
内存控制单元(MMU)通过分段单元把逻辑地址转换为线性地址;通过分页单元把线性地址转换为物理地址。
硬件中的分段
段标识符
逻辑地址由16位的段标识符(也成为段选择符)和一个32位的相对地址偏移量表示
CPU有专门的段处理器存放段选择符(cs,ss,ds,es,fs,gs)。
cs:代码段寄存器
ss:栈段寄存器
ds:数据段寄存器
其余:指向任意数据段
段描述符
找到对应的段,可以通过段描述符了解段的的特征(进程有进程描述符,段自然也有段描述符)。
段描述符放在全局描述符表(GDT)或是局部描述符表(LDT)中。通常只定义一个GDT,每个进程除了存放在GDT中的段之外,如果还需要创建新的段,就可以放在自己的LDT中。
GDT在主存中地址和大小存放在gdtr控制寄存器中;当前被使用的LDT地址和大小放在ldtr控制寄存器中。
之前16位的段选择符前13位表示段描述符在GDT中的下标,所以GDT最多保存2^13 – 1个段描述符,因为一个段描述符是8字节长,加入GDT地址位0x00020000,我们要获取下标为2的段描述符,要访问的地址为0x00020000 + (2 * 8)。
分段单元
分段单元负责将逻辑地址转换为线性地址,过程如下:
- 先检查TI字段,判断段描述符在GDT还是LDT,从对应寄存器拿到GDT或LDT的地址
- 根绝段选择符的索引号在GDT或LDT中得到段描述符地址
- 将逻辑地址中的偏移量 + 段描述符中的base地址,得到线性地址
Linux中的分段
分段可以给每个进程分配不同的线性地址空间,分也可以把一个线性地址空间映射到不同物理空间。
Linux设计目标之一是把它移植到绝大部分流行的处理器平台上,然后RISC对分段的支持有限,所以只有在90×96结构下Linux才使用分段,其实都只是用分页(此处指的是Linux2.6)。
当所有进程使用相同的段寄存器时,内存管理变得更简单。运行在用户态的所有Linux进程都只使用一堆段来对指令和数据寻址:用户代码段;用户数据段,内核态中进程则使用:内核代码段;内核数据段。
Linux下逻辑地址与线性地址是一样的,所有段都从0x00000000开始,逻辑地址的偏移量的值与线性地址的值总是一致的。
硬件中的分页
这块就很熟悉了,页目录 + 页 + 偏移量,目录可以是多级的,还是比较经典的。
分页单元吧所有RAM分成固定长度的页框,每个页框包含一个页。
从线性地址到物理地址需要一个映射表,我们称之为页表,放在主存中。
其余组成比如高速缓存,TLB,一个是为了缩小CPU与RAM之间的速度不匹配,另一个是辅助快速从线性地址翻译成物理地址。
Linux中的分页
Linux从2.6.11版本开始采用四级分页模型:
- 页全局目录(PGD)
- 页上级目录(PUD)
- 页中间目录(PMD)
- 页表(PTE)
至于具体给每个部分分配多少位数,要看计算机体系结构,逻辑上是有的,但实现上可能没有。
内核页表
内核维持着一组自己使用的页表,内核映像刚被装入内存中时,CPU还运行在实模式,分页功能没被启用:
- 内核创建一个有限的地址空间,包括内核的代码段核数据段、初始页表核用于存放动态数据结构共128KB大小的空间
- 用剩余的RAM建立分页表
当进程运行在用户态时,产生的线性地址小于0xc0000000;当进程运行在内核态时,产生的线性地址大于等于0xc0000000,进程在线性地址空间中的偏移量时0xc0000000,也是内核生存空间的开始处。
内核页表所提供的最终映射必须把0xc0000000开始的线性地址转化为从0开始的物理地址。
从网上找了个图,便于了理解,RAM上3GB~4GB是留给内核的。
进一步了解RAM留给内核页表(3GB~4GB中前896MB)的划分:
- 要是计算机有小于896MB的RAM,那直接和物理地址一一对应即可
- 要是896MB~4096MB之间,那32位也可以表示全部的物理地址,只是需要时不时改变页表项
- 超过的话就需要用上预留的的128MB了,做额外的操作(具体操作见书本)
TLB
介绍完了多级页目录,就更能理解TLB优势了。
在硬件上会有一个叫做页表基地址寄存器,它存储PGD页表的首地址。然后PGD->PUD->PMG->PTE,非常繁琐。
TLB可以直接缓存虚拟地址核其映射的物理地址,大大降低了查找地址的延时。
关于TLB flush等概念可以参考链接:https://zhuanlan.zhihu.com/p/108425561,言简意赅。