深入理解Linux内核笔记—第02章:内存寻址.md

时间:2022-10-17 15:48:21

2.1 内存地址

  • 80x86微处理器的三种不同地址:
    • 逻辑地址:包含在机器语言指令中用来指定一个操作数或一条指令的地址
      • 每一个逻辑地址都有一个段和偏移量组成,偏移量指明了段开始的地方到实际地址之间的距离
    • 线性地址:也成为虚拟地址,是一个32位无符号整数,可以用来表示高达4G的地址
      • 线性地址通常用十六进制表示,值的范围为0x00000000到oxffffffff
    • 物理地址:用于内存芯片级内存单元寻址
      • 它们从微处理器的地址引脚发送到内存总线上的电信号相对应
      • 物理地址由32位或36位无符号整数表示
  • 内存控制单元(MMU):逻辑地址=>物理地址

    • 首先通过一个称为分段单元的硬件电路把一个逻辑地址转换为线性地址
    • 然后再通过分页单元的硬件电路把线性地址转换为一个物理地址
  • 内存仲裁器:

    • 引入原因
      • 多处理器系统中,所有CPU都共享同一内存,意味着RAM芯片可由独立的CPU并发访问
      • 在RAM芯片上的读或写操作必须串行的执行
      • 即使单处理器上也使用内存仲裁器,因为单处理器系统中,DMA和CPU也存在并发操作
    • 位置:插在总线和每个RAM芯片之间
    • 作用:若某个RAM芯片空闲就准予一个CPU访问,若忙则延迟CPU访问

2.2 硬件中的分段

  • Intel处理器执行地址转换的两种方式:
    • 实模式:为了维持处理器与早期的模型兼容,并让操作系统自举
    • 保护模式

2.3 段选择符和段寄存器

  • 逻辑地址的组成:一个段标识符和一个指定段内相对地址的偏移量
    • 段标识符是一个16位长的字段,称为段选择符
    • 偏移量是一个32位长的字段
  • 6个段寄存器:
    • CS:代码段寄存器,指向包含程序指令的段
      • 另一个重要功能:含有一个两位的字段,用以指明CPU的当前特权级(CPL):0代表最高优先级,3代表最低优先级
    • SS:栈段寄存器,指向包含当前程序栈的段
    • DS:数据段寄存器,指向包含静态数据或者全局数据段
    • 其噶3个是通用段寄存器,可以指向任意的数据段

2.4 段描述符

  • 每个段有一个8字节的段描述符表示,描述了段的特征
    • 段描述符存放于全局描述符表(GDT)或局部描述符表(LDT)中
  • 通常只定义一个GDT,而每个进程如需创建附加的段,就可以有自己的LDT
    • gdtr:存放GDT在主存中的地址和大小
    • ldtr:存放当前正在被使用的LDT地址和大小
  • 段描述符字段表(表2-1,P43)
字段名 描述
Base 包含段的首字节的线性地址
G 粒度标志:0,段大小以字节为单位;否则以4KB字节的倍数计
Limit 存放段中最后一个内存单元的偏移量,从而决定段的长度
S 系统标志:被清0 - 这是一个系统段;否则是一个普通的代码段或数据段
Type 描述了段的类型特征它的存取权限
DPL 描述符特权级字段:设为0的段只能当CPL为0时,才可访问;DPL为3的段对任何CPL值都是可访问的
P Segment-Present标志:0 - 段当前不在主存中;linux总是把这个标志(47位)设为1,因为它从来不把整个段交换到磁盘上去
D或B 称为B或D标志,取决于代码段还是数据段;如果偏移量的地址是32位,基本上设为1,若为16位,则被清为0
AVL标志 可以有操作系统使用,但被Linux忽略

- 有几种不同类型(Type)的段以及和他们对应的段描述符,Linux中被广泛采用的类型:
- 代码段描述符:表示这段描述符代表一个代码段,它可以放在GDT或LDT中,该描述符置S标志为1(非系统段)
- 数据段描述符:表示这个段描述符代表一个数据段,它可以放在GDT或LDT中,该描述符置S标识为1,栈段是通过一般的数据段实现
- 任务状态段描述符(TSSD):该描述符代表一个任务状态段,也就是说这个段用于保存处理器寄存器的内容,它只能出现在GDT中
- 根据相应的进程是否在CPU上运行,其Type字段的值分别为11或9,该描述符的S标志置为0
- 局部描述符表描述符:表示这个段描述符代表一个包含LDT的段,它只出现在GDT中,Type值为2,S标志置为0

2.5 快速访问段描述符

  • 80x86处理器提供了一种附加的非编程的寄存器(不可被设置),可供6个可编程的段寄存器使用

    • 每一个非编程的寄存器含有8个字节的段描述符,由相应的段寄存器中的段选择符中的选择符来指定
    • 每当一个段选择符被装入段寄存器时,相应的段描述符就由内存装入到了对应的非编程CPU寄存器
      • 之后,针对那个段的逻辑地址就可以不访问主存中的GDT或LDT,处理器只需直接引用存放段描述符的CPU寄存器即可
      • 仅当段寄存器的内容改变时,才有必要访问GDT或LDT
  • 段选择符字段

字段名 描述
index 指定了放在GDT或LDT中的相应段描述符
TI 0:段描述符是在GDT中;1:在LDT中
RPL 请求者特权级:当段选择符装入CS寄存器中时指示CPU当前的特权级;此外在访问数据段时有选择的削弱处理器的特权级

- 由于段选择符是8位字节长,因此它在GDT或者LDT内的相对地址是有段选择符的最高13位的值乘以8得到的

例如:GDT在0x200020000,且有段选择符所指定的索引号为2,那么相应的段描述符地址是0x00020000 + (2 x 8),或者0x00020010
(GDT的基地址保存在gdtr寄存器中)

  • GDT相关:
    • 第一项总是设为0,这就确保空段选择符的逻辑地址会被认为是无效的,因此引起一个处理器异常
    • 能够保存在GDT中的段描述符的最大数目是8191,即2的13次方减去1

2.6 分段单元

  • 分段单元执行的下面过程:
    • 先检查段选择符的TI字段,以决定段描述符保存在哪一个描述符表中
    • 从段选择符的index字段计算段描述符的地址,index字段的值乘以8,该结果与gdtr或ldtr寄存器中的内容相加
    • 把逻辑地址的偏移量与段描述符的Base字段的值相加就得到了线性地址
  • 逻辑地址转换为物理地址的过程

  • 有了段寄存器相关的不可编程寄存器,只有当段寄存器的内容被串改时才需要执行前两个操作

2.7 Linux中的分段

  • 80x86微处理器中的分段鼓励程序员把他们的程序划分成逻辑上相关的实体
    • 例如:子程序或者全局与局部数据区
  • 实际上,分段和分页在某种程度上有点多余,因为它们都可以划分进程的物理地址空间
    • 分段可以给每一个进程分配不同的线性地址空间
    • 分页可以把同一线性地址空间映射到不同的物理空间
  • Linux更偏向于使用分页,因为:

    • 所有进程使用相同的段寄存器值时,内存管理变得更简单,即他们能共享同样的线性地址
    • Linux设计目标之一是可以把它移植到绝大多数流行的处理器上,然后,RISC体系结构对分段的支持很有限
      -2.6版的Linux只有80x86结构下才需要使用分段
    • 运行在用户态的所有Linux进程都是用一对相同的段来对指令和数据寻址,即用户数据段和用户代码段
    • 运行在内核态的所有Linux进程都是用一对相同的段对指令和数据寻址,即内核数据段和内核代码段
    • 四个主要的Linux段的段描述符字段的值
  • 段选择符由宏__USER_CS,__USER_DS,__KERNEL_CS和__KERNEL_DS分别定义

    为了对内核代码段寻址,内核只需要把__KERNEL_CS宏产生的值装进CS段寄存器

  • 用用户下和内核态下所有进程可以使用相同的逻辑地址

    因为与段相关的线性地址从0开始,达到2的32次方减去1的寻址限长

  • 在Linux下逻辑地址与线性地址是一致的,即逻辑地址的偏移量字段的值与相应的线性地址的值总是一致的

    因为所有字段都从0x00000000开始

  • 只要当前的特权级被改变,一些段寄存器必须相应的更新,例如ds段寄存器(ss段寄存器也一样):

    • 当CPL=3时(用户态),ds寄存器必须含有用户数据段的段选择符
    • 当CPL=0时,ds段寄存器必须含有内核数据段的段选择符
  • 当对指向指令或者数据结构的指针进行保存时,内核根本不需要为其设置逻辑地址的段选择符,因为cs寄存器就含有当前的段选择符
    • 例如:
      • 当内核调执行一条call汇编指令调用一个函数时,该指令仅指定其逻辑地址的偏移量部分,因为段选择符已经隐藏在cs寄存器中了
      • 因为“内核态执行”的段只有代码段,由宏__KERNEL_CS定义,因而只要当CPU切换到内核态时将__KERNEL_CS装进CS就够了
    • 例子中的情况也同样适合指向内核数据结构的指针以及指向用户数据结构的指针

2.8 Linux GDT

  • 单处理器系统只有一个GDT,对处理器的系统中每个CPU对应一个GDT
    • 所有的GDT都存放在cpu_gdt_table数组中
    • 所有GDT的地址和它们的大小被存放在cpu_gdt_descr数组中
  • GDT表:

  • GDT表中段选择符的说明:

    • 用户态和内核态下的代码段和数据段共四个kernel_code、kernel_data、kernel_code和user_data
    • 任务状态段(TSS),每个处理器只有一个
      • 每个TSS相应的线性地址空间都是内核数据段相应线性地址空间的一个小子集
      • 所有的TSS都顺序的存放在init_tss数组中
        • 特别说明:第n个CPU的TSS描述符的base字段指向init_tss数组的第n个元素
      • G标志清0,而Limit字段设置为0xeb,因为TSS段是236字节长
      • Type字段设置为9或11(可用的32位TSS),且DPL设置为0,因为不允许用户态下的进程访问TSS段
    • 1个包含缺省局部描述符的段,这个段通常是被所有进程共享的段
    • 3个局部线程存储段(TLS):这种机制允许多线程程序使用最多3种局部于线程的数据段
      • 系统调用set_thread_area()和get_thread_area()分别为正在执行的进程创建和撤销一个TLS段
    • 与高级电源管理(AMP)相关的3个段
      • 由于BIOS代码使用段,所以当Linux APM驱动程序调用BIOS函数来获取或者设置APM设备的状态时,就可以使用自定义的代码段和数据段
    • 与支持即插即用(PnP)功能的BIOS服务程序相关的5个段
      • 在前一种情况下,就像前述与AMP相关的3个段的情况一样,可以自定义代码段和数据段
    • 被内核用来处理“双重错误”异常的特殊TSS段
  • 系统中每个处理器都有一个GDT副本,除少数情况下,所有GDT的副本都存放相同的表项
    • 首先,每个处理器都有它自己的TSS段,因此其对应的GDT项不同
    • 其次,GDT中只有少数像可能依赖于CPU正在执行的进程(LDT和TLS段描述符)
    • 最后,在某些情况下,处理器可能临时修改GDT副本里的某个项
      • 例如,当调用APM的BIOS历程时就会发生这种情况

Linux LDT

  • 大多数用户态的Linux程序不使用局部描述符表,这样内核就定义了一个缺省的LDT供大多数进程共享
    • 局部缺省的LDT存放于default_ldt数组中,其包含5项,内核仅仅有效的使用了其中的两项:
      • 用于IBCS执行文件的调用门
      • 用于Solari/x86可执行文件的调用门
        • 调用门时80x86微处理器提供的一种机制,用于在调用预定义函数是改变CPU的特权级,详见intel文档
  • 某些情况,进程还是需要创建自己的局部描述符表
    • 对有些程序有用,如wine程序,它们执行面向段的微软Windows应用程序
    • modify_ldt()系统调用允许进程创建自己的LDT
      • 任何被modify_ldt()创建的自定义局部描述符表仍然需要他自己的段
      • 当处理器开始执行拥有自定义的LDT进程时,该CPU的GDT副本中的LDT表项相应的就被修改了
    • 用户态下的程序也可以利用modify_ldt()来分配新的段,但内核却从不使用这些段,也不去了解相应的段描述符,因为这些段描述符被包含进了进程自定的LDT中了

分页机制

硬件中的分页

  • 分页单元(paging unit)把线性地址转换为物理地址
    • 其中一个关键任务是把所请求的访问类型与线性地址的访问权限相比较,若这次访问无效,则产生缺页异常
  • 几个重要的概念:
    • 页:线性地址被分成以固定长度为单位的组,称为页;通常,页既指一组线性地址,也包含在这组地址中的数据
      • 页内部连续的线性地址被映射到连续的物理地址中
      • 内核就可以指定一个页的物理地址和其存取权限,而不用指定页所包含的全部线性地址的存取权限
    • 页框:分页单元把所有的RAM分成固定长度的页框(物理页),一个页框的长度与一个页的长度一致
      • 页框是主存的部分,因此也是一个存储区域,而页只是一个数据块,可以存放在任何页框或者磁盘中
    • 页表:线性地址映射到物理地址的数据结构成为页表,页表存放在主存中,并在启用分页单元之前必须由内核对其进行初始化
  • 所有的80x86处理器都支持分页,通过设置cr0寄存器的PG标志启动
    • PG为0,线性地址就被解释成物理地址
    • PG为1,启动分页机制

常规分页

  • intel处理器的分页单元处理4k的页
  • 32位的线性地址被分成3个域:
    • Directory(目录):最高10位,它决定页目录中的目录项,而目录项指向适当的页表
    • Table(页表):中间10位,它决定页表中的表项,表项中含有页所在页框的物理地址
    • Offset(偏移量):最低12位,它决定也框内的相对位置,由于有12位,故每一页含有4096字节的数据
  • 线性地址转换分为两步完成,每一步都基于一种转换表:
    • 第一种转换表称为页目录转换表(page directory)
    • 第二种转换表称为页表(page table)
  • 页目录:

    • 正在使用的页目录的物理地址存放在控制寄存器cr3中
    • 每个活动进程必须有一个分配给他的页目录,但是只有在进程实际需要一个页表时才给该页表分配RAM会更有效
  • 页目录项和页表项具有相同的结构,每一项都包含下面字段:

    • present标志:
      • 置1:所指定页在主存中
      • 清0:不在主存中;如果执行一个地址转换时该标志被清零,则分页单元就把该线性地址存放在cr2中,并产生缺页异常
    • Field包含页框物理地址最高20位的字段
      • 由于每个页框大小为4KB,物理地址必须是4096倍数,因而物理地址最低12位总是为0
      • 如果这个字段指向一个页目录,相应的页框就含有一个页表;若指向一个页表,相应的页框就含有一页数据
    • Accessed标志:
      • 每当分页单元相应页框进行寻址就设置这个标志
      • 当选中的页被交换出去时,这一标志就可以由操作系统使用
      • 分页单元从来不重置这个标志,而是必须有操作系统去做
    • Dirty标志:只应用于页表项中
      • 每当对一个页框进行写操作时就设置这个标志
      • 当选中的页被交换出去时,这一标志就可以由操作系统使用
      • 分页单元从来不重置这个标志,而是必须有操作系统去做
    • Read/Write标志:页或页表的存取权限
    • User/Supervisor标志:含有访问页或页表的特权级
    • PCD和PWT标志:控制硬件高速缓存处理页或页表的方式
    • Page Size标志:只应用于页目录项;若置为1,则页目录指向的是2MB或4MB的页框
    • Global标志:只应用于页表项
      • 该标志引用于Pentium pro,用来防止常用页从TLB高速缓存中刷新出去
      • 只有在cr4寄存器的页全局启用(PGE)标志置为时这个标志才启用

扩展分页

  • 页目录项的Page Size标志启用扩展分页功能
  • 扩展分页允许页框大小为4MB而不是4KB
  • 扩展分页用于把大段的连续的线性地址转换为物理地址

    • 这种情况,内核可以不用中间页表进行地址转换,从而节省内核并保留TLB
  • 分页单元把32位线性地址分为两个字段:

    • Directory:最高10位
    • Offset:其余22位
  • 扩展分页和普通分页的也目录基本相同,除了:
    • Page Size标志必须被设置
    • 20位物理地址只有高10位有意义,因为每个物理地址都是在以4MB为边界的地方开始的,故这个地址的低22位为0
  • 通过设置cr4寄存器的PSE标志能使扩展分页普通分页共存

硬件保护方案

  • 页与页表相关的特权级只有两个,因为特权由“常规分页”一节中提到的User/Supervisor标志所控制
    • 标志为0:只有当CPL小于3(这意味着对于Linux而言,处理器处于内核态)时才能对页寻址
    • 标志为1:则总能对页寻址
  • 页的存取权限只有两种:读与写
    • 页目录或页表项的Read/Write标志为0:相应的页表或页是只读的
    • 否则是可读写的

常规分页举例

  • 假设内存给一个正在运行的进程的线性地址空间为0x20000000到0x2003ffff;该空间正好由64页组成
    • 对线性地址空间的分析:
      • 线性地址高10位(Directory字段)都是以2开头后跟着0,因此高10位有相同的值,都是0x80,指向页目录的第129项
        • 页目录中必须包含该进程的页表的物理地址
        • 如果没有给该进程分配其他线性地址,择业目录的其余1023项都填为0
      • 中间10为的值(Table字段)范围为0-0x03f,因而只有页表的前64位有意义,其余的960都填为0
    • 假设线程读取0x20021406中的字节
      • Directory字段的0x80用于选择页目录的第0x80目录项
      • Table字段0x21用于选择页表的第0x21表项,该页表指向包含所需页的页框
        • 如果页表的Present标志为0,则此页不在主存中,此时将会产生缺页异常
        • 无论何时,当访问该线性空间0x20000000到0x2003ffff的地址之外的线性地址时,都将产生缺页异常
          • 因为这些页表都填为了0,尤其是它们的Present标志都被清0了
      • Offset字段0x406用于在目标页框中读偏移量为0x406中的字节