内存地址
x86架构包括三种地址:
- 逻辑地址
- 用于机器语言寻址,包括操作数的地址及指令地址等。逻辑地址包括短地址以及偏移地址。
- 线性地址(虚拟地址)
- 32位无符号整数,0x00000000~0xffffffff。最多寻址4GB。
- 0x00000000到0xbfffffff,用户态内核态进程均可寻址,0xc0000000到0xffffffff,只有内核态进程才可寻址
- 物理地址
- 用于在存储芯片中寻址存储单元。对应从CPU地址引脚发送到内存总线上的电信号。物理地址表示为32或64位无符号整数。
内存管理单元(MMU)通过分段单元将逻辑地址转换为线性地址,接着又通过页单元将线性地址转换为物理地址。
多CPU系统中,所有CPU共享相同地内存。所以RAM芯片可能同时被多个独立的CPU访问。由于对RAM芯片的读操作和写操作都必须串行地执行,在总线和每个内存芯片之间添加了一个叫做内存仲裁器的硬件电路。这个电路的作用是:当RAM芯片处于空闲状态时,授权给CPU访问:当RAM芯片处理其他处理器的请求时,延迟其他CPU访问。甚至在单处理器系统中,也会使用内存仲裁器。这是因为这些系统包括了DMA控制器,它与CPU也存在并发操作。当然,多处理器系统中的内存仲裁器电路更为复杂,因为它有更多的输入端口。从编程的角度看,仲裁器是不可见的,因为它完全硬件电路管理。
硬件分段
段选择符与段寄存器
逻辑地址包括两部分:段标识符
和段内相对偏移地址
。段标识符是16-bit的域,称为段选择符;偏移地址是32-bit。
- index:指明GDT或LDT中的段描述符项。由于段描述符是8字节,它在GDT或LDT中的相对地址通过index字段乘8获得。addr = gdtr(ldtr) + 8*index。GDT最多可以保存2^13-1个段描述符。
- TI:0,GDT;1,LDT
- RPL:当段选择符加载到cs寄存器时,指明CPL;当访问数据段时,也用来选择性地减弱CPL。
为了方便快速检索段选择符,处理器提供了6个专用分段寄存器来保存段选择符,它们是cs,ss,ds,es,fs和gs. 程序可以通过S/L来
重用段寄存器。
- cs:代码段寄存器,指向含有程序指令的段;cs寄存器还有一个重要的功能:它包含一个指定当前CPU特权级(CPL)的的2-bit域。0表示最高优先级;3表示最低优先级。 Linux只使用了0和3,以区分内核模式和用户模式
- ss:堆栈段寄存器,指向包含当前程序栈的段
- ds:数据段寄存器,指向包含静态和全局数据的段
其它三个是通用分段寄存器,可以指向任意类型的段。
段描述符
每个段都由8字节的段描述符表示,它描述了段的特征。段描述符要么存储在全局描述符表(GDT)里,要么存储在本地描述符表(LDT)里。通常只定义了一个GDT,如果进程需要额外创建除了GDT中之外的段,每个进程也允许有自己的LDT。主存里GDT的地址和大小都包含在gdtr控制寄存器中,而当前正在使用的LDT的地址和大小则包含在ldtr控制寄存器中。
- Base:段首字节的线性地址
- G:如果为0,段大小用字节表示,否则用4096字节的倍数表示
- Limit:段中最后一个内存单元的偏移地址,即段的长度。G为0时,段长度为11MB;G为1时,长度为4KB4GB
- S:如果置为0,这个段是系统段,保存关键的数据结构;否则是普通的代码段或数据段
- Type:指明段的类型及访问权限,包括代码段,数据段,任务状态段以及LDT段
- DPL: 描述符特权级,指明了访问此段所需的最小CPL。0级DPL只能被0级CPL访问;3级DPL可以被任意等级CPL访问
- P:0代表此段不在主存中。Linux不会将整个段交换到硬盘,所有这一字段总是1
- D or B:1代表段偏移是32位,0代表16位
- AVL:Linux中不使用
段描述符的快速访问
前面提到,逻辑地址由16位段选择符和32位段偏移构成,并且段寄存器只用来存储段选择符。
为了加速逻辑地址想线性地址的转换,x86处理器为6个段寄存器各自添加了一个不可编程寄存器,存储了段寄存器中段选择符所指定的段描述符。当段选择符加载到段寄存器时,段描述符被加载到不可编程的段寄存器中。因此,逻辑地址东侧转换不用访问主存中的GDT或LDT。只有当段寄存器改变时,才需要访问GDT或LDT。
分段单元
分段单元通过以下操作将逻辑地址转化为线性地址。
- 检查TL字段,决定从gdtr还是ldtr获取基址
- index*8加上基址得到段描述符的地址
- 将段描述符Base字段加上offset得到线性地址
由于不可变成寄存器的存在,只有当段寄存器发送改变时才需要做前两步。
Linux以极为有限的方式使用分段,而更喜欢使用分页。
- 如果所有进程使用相同的段寄存器,即共享一套线性地址时,内存管理更为容易
- Linux的设计目标是对广泛架构提供可移植性,RISC架构对分段支持有限
2.6版本仅在x86架构中使用分段。所有用户态进程使用同样的一对指令段和数据段进行寻址。同样,所用内核态的进程也是如此。对应的段选择符通过4个宏定义:__USER_CS, __USER_DS, __KERNEL_CS,和__KERNEL_DS。与这些分段对应的线性地址都从0起始,并且寻址限制长度为2^32-1,即无论用户态和内核态进程都使用相同的逻辑地址。并且逻辑地址的offset字段与对应的线性地址一致。
分段与分页一定程度上是冗余的,它们都可以为不同的进程区分物理地址空间。分段将不同的线性地址空间赋给每个进程;分页将相同的线性地址空间映射到不同的物理地址空间。分页单元将线性地址转换为物理地址。分页单元的主要任务是检查访问类型与访问权限是否匹配。如果内存访问是无效的,将会产生页错误异常。
出于高效,线性地址被分组为固定长度的区间,称为页。同一页中的连续线性地址被映射为连续的物理地址。这样,内核可以指定页的物理地址和访问权限而不用指定页中包含的所有线性地址的物理地址和访问权限。通常,“页”既指一组线性地址,也包含这组地址中的数据。
分页单元将全部RAM分成固定大小的页框(物理页)。每个页框包含一个页。页框是主存的组成部分,因此也是存储区域。页和页框的区别在于,前者只是一个数据块,可以存放在任何页框或硬盘上。将线性地址映射为物理地址的数据结构称为页表。页表存储在主存中,并且在启用分页单元之前应当被内核初始化。从80386开始,x86处理器支持分页,通过置位cr0寄存器的PG标志启用,若PG为0,线性地址被解释为物理地址。
常规分页
从80386开始,分页单元处理4KB的页。32位线性地址被分为3个字段。
- 目录:高10位
- 页表:中间10位
- 偏移:低12位
- 线性地址的转换由两步完成,第一步是页目录,第二步是页表。两级模式减少了每个进程页表所需的RAM空间。一级模式若进程使用整个4G地址空间,则需要2^20个表项,二级模式可以只为进程实际使用的虚拟内存区请求页表。每个活动的进程都要有页目录,不需要马上为所有页表分配内存,只有当进程实际需要页表的时候再为其分配内存会更加有效率。
- 正在使用的页目录的物理地址存在cr3寄存器中。线性地址中的Dirtecory字段决定页目录中的目录项,目录项指向页表。Table字段决定页表中的表项,包含页框所在的物理地址。Offset字段决定页框中的相对位置。
页目录和页表具有相同的结构。目录项与页表项分配给进程的物理地址。其余项均填0,访问这些项的线性地址将产生缺页异常:
- Present:如果置位,表明所指的页或页表包含在主存中;如果为0,页不在主存,其他位可被操作系统使用。若需要地址转换的目录项或页表项中Present被重置,线性地址存在cr2中,产生缺页异常
- Accessed:当分页单元对页框进行寻址时置位。用于操作系统换出页。重置由操作系统完成
- Dirty:只用于页表项。当页框执行写操作时置位,用于操作系统换出页。重置由操作系统完成
- Read/Write:页或页表的读写权限。若为0,只读,否则,可读写
- User/Supervisor:访问页或页表所需的特权级。若为0,CPL小于3(内核态)才能对页寻址,否则,总能对页寻址
- PCD和PMT:控制硬件高速缓存处理页或页表的方式
- Page Size:只用于页目录项,如果置位,页目录项指的是2M或4M页框
- Global:只用于页表项,防止常用页从TLB高速缓存中刷新出去
扩展分页
x86微处理器提供扩展分页,允许页框大小达到4MB,要求Page Size字段置位。内核不使用中间页表,节省内存并保留了TLB项。
- Directroy:高10位
- Offset:低22位
物理地址扩展分页机制
处理器支持的RAM容量受地址总线上的地址引脚数量限制。Intel将引脚数量由32扩展到36位支持64GBRAM,作为32位x86架构的扩展。需要引进新的分页机制将32位线性地址转换为36位物理地址。
- 64GBRAM分为2^24个页框。页表项的页字段由20位扩展到24位。因此页表项有32位变为64位。
- 引入新级别页目录指针表(PDPT),由4个64位表项组成
- cr3存储27-bit的PDPT基址
硬件高速缓存
微处理器时钟频率有几个GHz,DRAM存取时间是周期的数百倍。为了缩小CPU与RAM速度不匹配,引入硬件高速缓存内存,基于局部性原理。使用小而快的内存存放最近最常用的代码与数据。当访问RAM时,CPU分为命中高速缓存与没有命中。当命中时,控制器从高速缓存行中取数据到CPU寄存器。对于写操作,分为通写和回写。通写既写高速缓存行也谢RAM。回写只更新高速缓存行,只有当CPU执行刷新指令或FLUSH硬件信号(通常不命中)产生写回到RAM中。Linux对所有页框都启用高速缓存,对所有写操作采取回写策略。在多处理器系统中,每个处理器都有单独的硬件高速缓存,如果一个CPU修改了高速缓存,其他包含这块数据的CPU需要保持数据同步,称为高速缓存侦听。
TLB:转换后援缓冲器,用于加快线性地址的转换。当线性地址通过计算得出的物理地址存放在TLB表项中。多处理器系统中,每个CPU都有自己的TLB,无需同步。当cr3寄存器修改时,本地TLB所有项失效,因为新的一组页表被启动。
Linux使用了常规分页。为了适用32位于64位架构,采用4级分页模型:
- 页全局目录
- 页上级目录
- 页中间目录
- 页表
- 对于未启用物理地址扩展的32位系统,2级分页已经足够。Linux将上级和中间目录置为0。但是两个目录在指针序列中的位置被保留,因此它们可以在32和64位系统上使用。内核保留上级和中间目录,将它们项数设为1,并且映射到全局目录的表项中。
- 对于启用物理地址扩展的32位系统,使用3级分页。全局目录与x86的页目录表一致,上层目录被省略,中间目录对应x86页目录,页表对于x86页表。64位系统取决于硬件对线性地址位的划分。
- Linux进程处理很大程度依赖于分页。给每个进程分配不同的物理地址空间,有效防止寻址错误;可以将页框中的页保存到磁盘上,再重新装入其他页框中。每个进程都有自己的页全局目录和页表,发生进程切换时,将cr3寄存器保存在换出进程描述符中,将换入进程对应的值存入cr3。