三种地址:
1. 逻辑地址, 段标识符和偏移量组成
段标识符, 一个16位长的字段, 称为段选择符。
偏移量, 一个32位长的字段。
段选择符字段
index: GDT或LDT中对应段描述符的索引。GDT首地址+8*index即为对应的段描述符。
T1: 0在GDT中, 1在LDT中。
RPL: 当段选择符装入到cs寄存器中时指出cpu当前的特权级。
2. 线性地址, 可以表示4gb的地址, 16进制表示
3. 物理地址, 真实的内存地址.
MMU通过分段单元将逻辑地址转换成线性地址, 然后通过分页单元把线性地址转化成
物理地址。
分段
6个段寄存器cs, ss, ds, es, fs, gs用于保存段选择符。
其中3个有专门用途:
cs: 代码段寄存器, 指向包含程序指令的段。
含两位CPL表示当前cpu的特权等级. 0, 3分别表示内核态和用户态。
ss: 栈段寄存器, 指向包含当前程序栈的段。
ds: 数据段寄存器, 指向包含静态数据或者全局数据段。
其他三个段寄存器可以指向任意的数据段。
段描述符
每个段由一个8位的段描述符表示, 他描述了段的特征, 存放在GDT或LDT中。
通常只定义一个GDT, 而每个进程除了存放在GDT中的端外还有额外定义段的话, 就可以
有自己的LDT。GDT存放在主存中的地址和大小放在gdtr控制寄存器中, 当前正被使用的LDT存放在ldtr控制寄存器中。
段描述符定义:
Base: 分成三部分, 使用时拼接起来就是一个32位的线性地址, 可以表示4gb。
G: 0则段大小以1字节为单位, 1则以4096字节为单位
Limit: 分成两部分, 使用时拼接起来20位, 表示段的偏移量, 计算时乘上g的倍数, 范围分别是1byte-1mb, 4kb-4gb.
S: 系统标志, 如果为0表示系统段, 否则表示普通的数据段或者代码段。
Type: 描述了段的类型特征和他的存取权限。
DPL: 描述符特权级, DPL为0时只有当CPL为0才能访问, DPL为3的段任何CPL值都能访问。
P: 等于0表示段当前不在主存中, 总是置1。linux不会将其放到磁盘上。
D或B: 如果段偏移量的地址是32位长则置1, 16位则置0
AVL: 被linux系统忽略
常见段描述符:
代码段描述符: 存放于GDT或LDT中。非系统段
数据段描述符: 存放于GDT或LDT中。非系统段
任务状态段描述符: 保存处理器寄存器的内容,只出现在GDT中,根据相应进程是否在
CPU上运行, type值为11或9。系统段
LDTD: 只出现在GDT中, type值为2。系统段
每个段寄存器对应一个非编程的寄存器, 在段寄存器中设置了段选择符后, 相应的段描述符就由内存装入到对应的非编程寄存器中。这时, 针对该段的逻辑地址转换就不用走GDT或LDT了, 直接访问该寄存器中的段描述符, 仅当段寄存器中的内容改变时, 才需要访问GDT和LDT。
逻辑地址转换线性地址过程: 分段单元先检查段选择符的T1字段, 以决定描述符保存在GDT(从gdtr获取GDT线性地址)还是当前的LDT(从ldtr获取LDT线性地址),index*8加上该地址得到段描述符, 然后把段描述符的base加上逻辑地址的偏移量就得到了最终的线性地址。
Linux中的分段
linux中的分段使用的十分有限。与分段相比, linux更喜欢分页的方式。
当所有进程使用相同的段寄存器值时, 内存管理变的简单, 可以共享同样的一组线性地址。
运行在用户态中的所有进程都使用相同的段来对指令和数据寻址, 用户代码段和用户数据段。
内核同理, 内核数据段和内核代码段。
所有段都是从0x0开始的,linux下的逻辑地址和线性地址总是相同的。
CPL改变时, 对应的cs和ds寄存器要被更新成对应的段, ss同理。
Linux GDT
每个处理器都有一个GDT,每个GDT包含18个段描述符和14个空的,未使用的或保留的项。
插入空项是为了GDT能够处于同一个32字节的硬件高速缓存中。
18个段:
内核态和用户态的数据段和代码段, 4。
任务状态段, 每个处理器有一个。
1个包含缺省的LDT表的段,被所有进程共享。
3个局部线程段。
与电源管理相关的3个段。
与pnp相关的5个段。
处理双重错误异常的特殊TSS段。
Linux LDT
大多数用户态程序不适用LDT段。使用modify_ldt可以创建自定义的LDT以及在其上分配新的段。
分页
分页单元把线性地址转化成物理地址。
线性地址被分为固定大小, 称为页。页内部连续的线性地址被映射到连续的物理地址上。
分页单元把所有的ram分成固定大小的页框,每一个页框包含一个页。
把线性地址映射到物理地址的数据结构称为页表,存在主存中,启动分页单元之前必须由内核
对页表进行适当的初始化。
常规分页
32位的线性地址被分成3个域:
Directory: 最高10位
Table: 中间10位
Offset: 最低12位
线性地址的转换分两步完成,每一步都基于一种转换表, 第一种转换表称为页目录表,第二种转换表称为页表。
使用这种二级模式的目的在于减少每个进程页表所需内存的数量。因为页表是数组简单的使用一级页表将会有高达2^20的表项,即使有没有用的空间, 二级模式则可以只为用到的部分分配页表内存。
每个活动进程必须有一个分配给他的页目录, 但页表的内存可以在需要的时候再分配给他。
page = data[directory][Table]
字节 = page[offset]
页目录和页表都是10位, 可以寻址到1024*1024*4096, 4gb的内存单元。
页目录项和页表项具有相同结构:
Present标志, 若为1,所指的页或页表就在主存中,若为0,则表示该页或页表不在主存中。如果执行一个地址转换所需的页表项或页目录被清0,那么分页单元就把该地址存放在控制寄存器cr2中,并产生一个缺页异常。
包含页框物理地址最高20位的字段: 由于每个页框大小4kb, 所以他的地址必定是4096的倍数,因此其低12位总为0。如果该字段指向一个页目录,该页框内就包含一个页表,若指向一个页表,则页框内就包含一页数据。
Accessed标志: 每当分页单元对相应页框进行寻址时就设置这个标志。当选中的页被交换出去时, 由操作系统使用。
Dirty标志: 只应用于页表项中。当选中的页被交换出去时,这一标志就可以由操作系统使用。
Read/Write标志: 含有页或页表的存取权限。
User/Supervisor标志: 含有访问页或页表所需的特权级。
PCD和PWT标志: 控制硬件高速缓存处理页或页表的方式。
Page size标志: 只应用于页目录项。如果设置为1,则页目录项指的是2mb或4mb的页框。
Global标志: 只应用于页表项。防止常用也从TLB高速缓存中刷新出去。只有在cr4寄存器的page global enable标志置位时这个标志才会起作用。
扩展分页
允许页框4mb,用于把大段连续的线性地址转化成物理地址。这种情况下内核可以不用中间页表,节省内存并保留tlb项。
通过设置页目录项的page size标志启用扩展分页功能,在这种情况下,分页单元把32位线性地址分成两个字段:
Directory: 高10位
Offset: 其余22位
扩展分页和常规分页的页目录基本相同,除了:
page size必须被设置
20位物理地址只有最高10位有意义,因为以4mb为边界,低22位全是0。
段的权限读写执行,页读写。
硬件高速缓存
行由几十个连续的字节组成。
高速缓存被细分为行的子集。高速缓存单元插在分页单元和主存之间。包含一个硬件高速缓存内存和一个高速缓存控制器。高速缓存内存中存放内存中真正的行,高速缓存控制器中存放一个表项数组,每个表项对应高速缓存内存中的一个行。每个表项有一个标签和几个描述高速缓存行状态的flag。这个标签由一些位组成,让控制器能够识别该行对应哪一个内存单元。
这种内存物理地址分为3部分: 最高几位对应标签,中间几位对应高速缓存控制器的子集索引,最低几位对应行内的偏移量。
当访问一个内存单元时,cpu从物理地址中提取出子集的索引号, 并把自己中所有行的标签与物理地址的高几位比较, 如果发现某个行的标签和这个物理地址的高位相同, 则cpu命中高速缓存。
当执行读操作时,控制器直接从高速缓存行中选择数据并送到cpu寄存器。
当齿形写操作时,可能采用通写或回写两个策略。
通写: 既写ram也写高速缓存。
回写: 只写高速缓存, 当执行一条要求刷新高速缓存表的指令或者高速缓存没有命中时, 高速缓存控制器才会把高速缓存写回ram。
处理器cr0的CD标志用来启用或禁用高速缓存, NN标志用来决定通写回写策略。
Linux中对所有的页框都启用高速缓存, 并使用回写策略。
转换后缓冲区(TLB)
当一个线性地址被第一次使用时,通过慢速访问ram计算出相应的物理地址。同时,物理地址被存放在一个TLB表项中。
每个cpu都有自己的tlb。tlb不用同步,因为存的是线性地址。cr3控制寄存器修改时,tlb中的内容自动无效,因为指向的是旧数据。
Linux中的分页
给每一个进程分配一块不同的物理空间,确保了可以有效的防止寻址错误。
区别页和页框的不同,这就允许页从页框中取出,保存到磁盘上,以后重新装入这同一页时又可以被装到不同的页框中,这就是虚拟内存机制的基本要素。
物理内存布局
在初始化阶段, 内核必须建立一个物理地址映射来指定那些物理地址范围对内核可用。
内核将下列页框记为保留:
在不可用的物理地址范围内的页框。
含有内核代码和已初始化的数据结构的页框。
保留页框中的页绝对不能动态分配或交换到磁盘上。
一般来说linux内核安装在ram中从0x00100000开始的地方,典型的内核可以安装在小于3mb的内存中。
第一个mb作为保留。
进程页表
进程的线性地址空间分为两部分:
从0x00000000-0xbfffffff的线性地址,无论进程运行在用户态还是内核态都可以寻址。
从0xc0000000到0xffffffff的线性地址,只有内核才能寻址。
当进程运行在用户态时,他产生的线性地址小于0xc0000000,运行在内核态时,产生的地址大于等于0xc0000000。但是内核为了检索或存放数据必须访问用户态线性地址空间。
内核页表
内核维持着一组自己使用的页表,驻留在主内核全局目录中。
内核初始化页表的两个阶段:
第一个阶段,内核创建一个有限的地址空间,包含内核的代码段和数据段,初始页表和用于存放动态数据结构的共128kb大小的空间。
第二个阶段,充分利用剩余的空间适当的建立分页表。
临时页全局目录是在编译中静态初始化的,内核在初始化的第一阶段通过从0xc0000000开始的8mb的线性地址对ram的前8mb地址进行寻址。
总结:
该章描述了内存寻址的方式, 逻辑地址通过分段单元解释成线性地址, 线性地址通过分页单元转换成分页地址。
在linux中,每个cpu都有单独gdt, 且常用的段包含内核数据段,内核代码段,用户数据段,用户代码段,以及一个tss段。ldt不常用。
分页则是采取多级分页的机制,32位系统是2级,64位系统是4级,目的是为了动态的分配页框以节省空间。
当访问到一个不在主存中的页时, 会触发一个缺页中断,分页单元会选择一个不常用的页,若该页是脏页则将其写回磁盘并交换目标页,否则直接把目标页覆盖到对应的页框。
硬件高速缓存的通写和回写策略。
tlb快表。