Linux0.11 80X86知识(七)(下)

本文涉及的产品
网络型负载均衡 NLB,每月750个小时 15LCU
应用型负载均衡 ALB,每月750个小时 15LCU
公网NAT网关,每月750个小时 15CU
简介: Linux0.11 80X86知识(七)

Linux0.11 80X86知识(七)(中):https://developer.aliyun.com/article/1597274

六、分页机制

  分页机制是 80X86 内存管理机制的第二部分。它在分段机制的基础上完成虚拟(逻辑)地址到物理地址转换的过程。分段机制把逻辑地址转换成线性地址,而分页则把线性地址转换成物理地址。分页可以用于任何一种分段模型。处理器分页机制会把线性地址空间(段已映射到其中)划分成页面,然后这些线性地址空间页面被映射到物理地址空间的页面上。分页机制几种页面级保护措施,可和分段机制保护机制合用或替代分段机制的保护措施。例如,在基于页面的基础上可以加强读/写保护。另外,在页面单元上,分页机制还提供了用户 - 超级用户两级保护。

  我们通过设置控制寄存器 CR0 的 PG 位可以启用分页机制。如果 PG=1,则启用分页操作,处理器会使用本节描述的机制将线性地址转换成物理地址。如果 PG=0,则禁用分页机制,此时分段机制产生的线性地址被直接用作物理地址。

  前面介绍的分段机制在各种可变长度的内存区域上操作。与分段机制不同,分页机制对固定大小的内存块(称为页面)进行操作。分页机制把线性和物理地址空间都划分成页面。线性地址空间中的任何页面可以被映射到物理地址空间的任何页面上。图 4-16 示出了分页机制是如何把线性和物理地址空间都划分成各个页面,并在这两个空间之间提供了任意映射。图中的箭头把线性地址空间中的页面与物理地址空间中的页面对应了起来。

  80X86 使用 4K(212)字节固定大小的页面。每个页面均是 4KB,并且对齐于 4K 地址边界处。这表示分页机制把 232 字节(4GB)的线性地址空间划分成 220(1M = 1048576)个页面。分页机制通过把线性地址空间中的页面重新定位到物理地址空间中进行操作。由于 4K 大小的页面作为一个单元进行映射,并且对齐于 4K 边界,因此线性地址的低 12 比特位可作为页内偏移量直接作为物理地址的低 12 位。分页机制执行的重定位功能可以看作是把线性地址的高 20 位转换到对应物理地址的高 20 位。

  另外,线性到物理地址的转换功能被扩展成允许一个线性地址被标注为无效的,而非让其产生一个物理地址。在两种情况下一个页面可以被示注为无效的:1)操作系统不支持的线性地址;2)对应在虚拟内存系统中的页面在磁盘上而非在物理内存中。在第一种情况下,产生无效地址的程序必须被终止。在第二种情况下,该无效地址实际上是请求操作系统虚拟内存管理器把对应页面从磁盘上加载到物理内存中,以供程序访问。因为无效页面通常与虚拟存储系统相关,因此它们被称为不存在的页面,并且由页表中称为存在(present)的属性来确定。

  在保护模式中,80X86 允许线性地址空间直接映射到大容量的物理内存(例如 4GB 的 RAM)上,或者(使用分页)间接地映射到较小容量的物理内存和磁盘存储空间中。这后一种映射线性地址空间的方法被称为虚拟存储或者需求页(Demand-paged)虚拟存储。

  当使用分页时,处理器会把线性地址空间划分成固定大小的页面(长度 4KB),这些页面可以映射到物理内存中和/或磁盘存储空间中。当一个程序(或任务)引用内存中的逻辑地址时,处理器会把该逻辑地址转换成一个线性地址,然后使用分页机制把该线性地址转换成对应的物理地址。

  如果包含线性地址的页面当前不在物理内存中,处理器就会产生一个页错误异常。页错误异常的处理程序通常就会让操作系统从磁盘中把相应页面加载到物理内存中(操作过程中可能还会把物理内存中不同的页面写到磁盘上)。当页面加载到物理内存中之后,从异常处理过程的返回操作会使得导致异常的指令被重新执行。处理器用于把线性地址转换成物理地址和用于产生页错误异常(若必要的话)的信息包含在存储于内存中的页目录和页表中。

  分页与分段最大的不同之处在于分页使用了固定长度的页面。段的长度通常与存放在其中的代码或数据结构具有相同的长度。与段不同,页面有固定的长度。如果仅使用分段地址转换,那么储存在物理内存中的一个数据结构将包含其所有的部分。但如果使用了分页,那么一个数据结构就可以一部分存储于物理内存中,而另一部分保存在磁盘中。

  正如前述,为了减少地址转换所要求的总线周期数量,最近访问的页目录和页表会被存放在处理器的缓冲器件中,该缓冲器件被称为转换查找缓中区 TLB(Translation Lookaside Buffer)。TLB 可以满足大多数读页目录和页表的请求而无需使用总线周期。只有当 TLB 中不包含要求的页表项时才会使用额外的总线周期从内存中读取页表项,这通常在一个页表项很长时间没有访问过时才会出现这种情况。

1、页表结构

  分页转换功能由驻留在内存中的表来描述,该表称为页表(page table),存放在物理地址空间中。页表可以看作是简单的 220 物理地址数组。线性到物理地址的映射功能可以简单地看作是进行数组查找。线性地址的高 20 位构成这个数组的索引值,用于选择对应页面的物理(基)地址。线性地址的低 12 位给出了页面中的偏移量,加上页面的基地址最终形成对应的物理地址。由于页面基地址对齐在 4K 边界上,因此页面基地址的低 12 位肯定是 0。这意味着高 20 位的页面基地址和 12 位偏移量连接组合在一起就能得到对应的物理地址。

  页表中每个页表项大小为 32 位。由于只需要其中的 20 位来存放页面的物理基地址,因此剩下的 12 位可用于存放诸如页面是否存在等的属性信息。如果线性地址索引的页表项被标注为存在的,则表示该项即有效,我们可以从中取得页面的物理地址。如果项中表明不存在,那么当访问对应物理页面时就会产生一个异常。

1.1 两级页表结构

  页表含有 220(1M)个表项,而每项占用 4 字节。如果作为一个表来存放的话,它们最多将占用 4MB 的内存。因此为了减少内存占用量,80X86 使用了两级表。由此,高 20 位线性地址到物理地址的转换也被分成两步来进行,每步使用(转换)其中 10 个比特。

  第一级表称为页目录(page directory)。它被存放在 1 页 4K 页面中,具有 210(1K)个 4 字节长度的表项。这些表项指向对应的二级表。线性地址的最高 10 位(位 31-22)用作一级表(页目录)中的索引值来选择 210 个二级表之一。

  第二级表称为页表(page table),它的长度也是 1 个页面,最多含有 1K 个 4 字节的表项。每个 4 字节表项含有相关页面的 20 位物理基地址。二级页表使用线性地址中间 10 位(位 21-12)作为表项索引值,以获取含有页面 20 位物理基地址的表项。该 20 位页面物理基地址和线性地址中的低 12 位(页内偏移)组合在一起就得到了分页转换过程的输出值,即对应的最终物理地址。

  图 4-17 示出了二级表的查找过程。其中 CR3 寄存器指定页目录表的基地址。线性地址的高 10 位用于索引这个页目录表,以获得指向相关第二级页表的指针。线性地址中间 10 位用于索引二级页表,以获得物理地址的高 20 位。线性地址的低 12 位直接作为物理地址低 12 位,从而组成一个完整的 32 位物理地址。

1.2 不存在的页表

  通过使用二级表结构,我们还没有解决需要使用 4MB 内存来存放页表的问题。实际上,我们把问题搞得有些复杂了。因为我们需要另增一个页面来存放目录表。然而,二级表结构允许页表被分散在内存各个页面中,而不需要保存在连续的 4MB 内存块中。另外,并不需要为不存在的或线性地址空间未使用部分分配二级页表。虽然目录表页面必须总是存在于物理内存中,但是二级页表可以在需要时再分配。这使得页表结构的大小对应于实际使用的线性地址空间大小。

  页目录表中每个表项也有一个存在(present)属性,类似于页表中的表项。页目录表项中的存在属性指明对应的二级页表是否存在。如果目录表项指明对应的二级页表存在,那么通过访问二级表,表查找过程第 2 步将同如上描述继续下去。如果存在位表明对应的二级表不存在,那么处理器就会产生一个异常来通知操作系统。页目录表项中的存在属性使得操作系统可以根据实际使用的线性地址范围来分配二级页表页面。

  目录表项中的存在位还可以用于在虚拟内存中存放二级页表。这意味着在任何时候只有部分二级页表需要存放在物理内存中,而其余的可保存在磁盘上。处于物理内存中页表对应的页目录项将被标注为存在,以表明可用它们进行分页转换。处于磁盘上的页表对应的页目录项将被标注为不存在。由于二级页表不存在而引发的异常会通知操作系统把缺少的页表从磁盘上加载进物理内存。把页表存储在虚拟内存中减少了保存分页转换表所需要的物理内存量。

2、页表项格式

   页目录和页表的表项格式见图 4-18 所示。其中位 31–12 含有物理地址的高 20 位,用于定位物理地址空间中一个页面(也称为页帧)的物理基地址。表项的低 12 位含有页属性信息。我们已经讨论过存在属性,这里简要说明其余属性的功能和用途。

P – 位 0 是存在(Present)标志,用于指明表项对地址转换是否有效。P=1 表示有效;P=0 表示无效。在页转换过程中,如果说涉及的页目录或页表的表项无效,则会导致一个异常。如果 P=0,那么除表示表项无效外,其余比特位可供程序自由使用,见图 44-18(b)所示。例如,操作系统可以使用这些位来保存已存储在磁盘上的页面的序号。

R/W – 位 1 是读/写(Read/Write)标志。如果等于 1,表示页面可以被读、写或执行。如果为 0,表示页面只读或可执行。当处理器运行在超级用户特权级(级别 0、1 或 2)时,则 R/W 位不起作用。页目录项中的 R/W 位对其所映射的所有页面起作用。

U/S – 位 2 是用户/超级用户(User/Supervisor)标志。如果为 1,那么运行在任何特权级上的程序都可以访问该页面。如果为 0,那么页面只能被运行在起级用户特权级(0、1 或 2)上的程序访问。页目录项中的 U/S 位对其所映射的所有页面起作用。

A – 位 5 是已访问(Accessed)标志。当处理器访问页表项映射的页面时,页表表项的这个标志就会被置为 1 。当处理器访问页目录表项映射的任何页面时,页目录表项的这个标志就会被置为 1 。处理器只负责设置该标志,操作系统可通过定期地复位该标志来统计页面的使用情况。

D – 位 6 是页面已被修改(Dirty)标志。当处理器对一个页面执行写操作时,就会设置对应页表表项的 D 标志。处理器并不会修改页目录项中的 D 标志。

AVL – 该字段保留专供程序使用。处理器不会修改这几位,以后的升级处理器也不会。

   页目录和页表表项中的存在标志 P 为使用分页技术的虚拟存储提供了必要的支持。若线性地址空间中的页面存在于物理内存中,则对应表项中的标志 P=1,并且该表项中含有相应物理地址。页面不在物理内存中的表项其标志 P = 0。如果程序访问物理内存中不存在的页面,处理器就会产生一个缺页异常。此时操作系统就可以利用这个异常处理过程把缺少的页面从磁盘上调入物理内存中,并把相应物理地址存放在表项中。最后在返回程序重新执行引起异常的指令之前设置标志 P=1。

  已访问标志 A 和已修改标志 D 可以用于有效地实现虚拟存储技术。通过周期性地检查和复位所有 A 标志,操作系统能够确定哪些页面最近没有访问过。这些页面可以成为移出到磁盘上的候选者。假设当一页面从磁盘上读入内存时,其脏标志 D=0,那么当页面再次被移出到磁盘上时,若 D 标志还是为 0,则该页面就无需被写入磁盘中。若此时 D=1,则说明页面内容已被修改过,于是就必须将该页面写到磁盘上。

七、保护

1、代码段之间转移控制时的特权级检查

  对于将程序控制权从一个代码段转移到另一个代码段,目标代码段的段选择符必须加载代码段寄存器(CS)中。作为这个加载过程的一部分,处理器会检测目标代码段的段描述符并执行各种限长、类型和特权级检查。如果这些检查都通过了,则目标代码段选择符就会加载进 CS 寄存器,于是程序的控制权就被转移到 新代码段 中,程序将从 EIP 寄存器指向的指令处开始执行。

  程序的控制转移使用指令 JMP、RET、INT 和 IRET 以及异常和中断机制来实现。异常和中断是一些特殊实现,将在后面描述,本节主要说明 JMP、CALL 和 RET 指令的实现方法。JMP 或 CALL 指令可以利用一下四种方法之一来引用另外一个代码段:


目标操作数含有目标代码段的段选择符;

目标操作数指向一个调用门描述符,而该描述符中含有目标代码段的选择符;

目标操作数指向一个 TSS,而 该 TSS 中含有目标代码段的选择符;

目标操作数指向一个任务门,该任务门指向一个 TSS,而该 TSS 中含有目标代码段的选择符;

下面描述前两种引用类型,后两种将放在有关任务管理一节中进行说明。

1.1 直接调用或跳转到代码段

   JMPCALLRET 指令的近转移形式只是在当前代码段中执行程序控制转移, 因此不会执行特权级检查。JMPCALLRET 指令的远转移形式会把控制转移到另外一个代码段中,因此处理器一定会执行特权级检查。

   当不通过调用门把程序控制权转移到另一个代码段时,处理器会验证 4 种特权级和类型信息,见图 4-21所示:


当前特权级 CPL。(这里,CPL 是执行调用的代码段的特权级,即含有执行调用或跳转程序的代码段的 CPL。)

含有被调用过程的目的代码段段描述符中的描述符特权级 DPL。

目的代码段的段选择符中的请求特权级 RPL。

目的代码段描述符中的一致性标志 C。它确定了一个代码段是非一致代码段还是一致代码段。

  处理器检查 CPL、RPL 和 DPL 的规则依赖于一致标志 C 的设置状态。当访问非一致代码段时(C=0),调用者(程序)的 CPL 必须等于目的代码段的 DPL,否则将会产生一般保护异常。指向非一致代码段的段选择符的 RPL 对检查所起的作用有限。RPL 在数值上必须小于或等于调用者的 CPL 才能使得控制转移成功完成。当非一致代码段的段选择符被加载进 CS 寄存器中时,特权级字段不会改变,即它仍然是调用者的 CPL。即使段选择符的 RPL 与 CPL 不同,这也是正确的。

  当访问一致代码段时(C=1),调用者的 CPL 可以在数值上大于或等于目的代码段的 DPL。仅当 CPL < DPL 时,处理器才会产生一般保护异常。对于访问一致代码段,处理器忽略对 RPL 的检查。对于一致代码段,DPL 表示调用者对代码段进行成功调用可以处于的最低数值特权级。

  当程序控制被转移到一个一致代码段中,CPL 并不改变,即使目的代码段的 DPL 在数值上小于 CPL。这是 CPL 与可能与当前代码段 DPL 不相同的唯一一种情况。同样,由于 CPL 没有改变,因此堆栈也不会切换。

  大多数代码段都是非一致代码段。对于这些段,程序的控制权只能转移到具有相同特权级的代码段中,除非转移是通过一个调用门进行,见下面说明。

1.2 门描述符

   为了对具有不同特权级的代码段提供受控的访问,处理器提供了称为门描述符的特殊描述符集。共有 4 种门描述符:

  • 调用门(Call Gate),类型 TYPE=12;
  • 陷阱门(Trap Gate),类型 TYPE=15;
  • 中断门(Interrupt Gate),类型 TYPE=14;
  • 任务门(Task Gate),类型 TYPE=5。

   任务门用于任务切换,将在后面任务管理一节说明。陷阱门中断门调用门特殊类 ,专门用于 调用异常中断的处理程序 ,这将在下一节进行说明。本节仅说明调用门的使用方法。

   调用门用于在不同特权级之间实现受控的程序控制转移。它们通常仅用于使用特权级保护机制的操作系统中。图 4-22 给出了调用门描述符的格式。调用门描述符可以存放在 GDTLDT 中,但是不能放在中断描述符表 IDT 中。一个调用门主要具有一下几个功能:

  • 指定要访问的代码段;
  • 在指定代码段中定义过程(程序)的一个入口点;
  • 指定访问过程的调用者需具备的特权级;
  • 若会发生堆栈切换,它会指定在堆栈之间需要复制的可选参数个数;
  • 指明调用门描述符是否有效。


   调用门中的段选择符字段指定要访问的代码段。偏移值字段指定段中入口点。这个入口点通常是指定过程的第一条指令。DPL 字段指定调用门的特权级,从而指定通过调用门访问特定过程所要求的特权级。标志 P 指明调用门描述符是否有效。参数个数字段(Param Count)指明在发生堆栈切换时从调用者堆栈复制到新堆栈中的参数个数。Linux 内核中并没有用到调用门。这里对调用门进行说明是为下一节介绍利用中断和异常门进行处理作准备。

1.3 通过调用门访问代码段

   为了访问调用门,我们需要为 CALLJMP 指令的操作数提供一个远指针。该指针中的段选择符用于指定调用门,而指针的偏移值虽然需要但 CPU 并不会用它。该偏移值可以设置为任意值。见图 4-23 所示。

   当处理器访问调用门时,它会使用调用门中的段选择符来定位目的代码段的段描述符。然后 CPU 会把代码段描述符的基地址与调用门中的偏移值进行组合,形成代码段中指定程序入口点的线性地址。


   通过调用门进行程序控制转移时,CPU 会对 4 种不同的特权级进行检查,以确定控制转移的有效性,见图 4-24 所示。

  • 当前特权级 CPL;
  • 调用门选择符中的请求特权级 RPL;
  • 调用门描述符中的描述符特权级 DPL;
  • 目的代码段描述符中的 DPL;

   另外,目的代码段描述符中的一致性标志 C 也将受到检查。


   使用 CALL 指令和 JMP 指令分别具有不同的特权级检测规则,见表 4-5 所示。调用门描述符的 DPL 字段指明了调用程序能够访问调用门的数值最大的特权级最小特权级),即为了访问调用门,调用者程序的特权级 CPL 必须小于或等于调用门的 DPL。调用门段选择符的 RPL 也需同调用这的 CPL 遵守同样的规则,即 RPL 也必须小于或等于调用门的 DPL

  如果调用这与调用门之间的特权级检查成功通过,CPU 就会接着把调用者的 CPL 与代码段描述符的 DPL 进行比较检查。在这方面,CALL 指令和 JMP 指令的检查规则就不同了。只有 CALL 指令可以通过调用门把程序控制转移到特权级更高的非一致性代码段中,即可以转移到 DPL 小于 CPL 的非一致性代码段中去执行。而 JMP 指令只能通过调用门把控制转移到 DPL 等于 CPL 的非一致性代码段中。但 CALL 指令和 JMP 指令都可以把控制转移到更高特权级的一致性代码段中,即转移到 DPL 小于或等于 CPL 的一致性代码段中。

  如果一个调用把控制转移到了更高特权级的非一致性代码段中,那么 CPL 就会被设置为目的代码段的 DPL 值,并且会引起堆栈切换。但是如果一个调用或跳转把控制转移到更高级别的一致性代码段上,那么 CPL 并不会改变,并且也不会引起堆栈切换。

  调用门可以让一个代码段中的过程被不同特权级的程序访问。例如,位于一个代码段中的操作系统代码可能含有操作系统自身和应用软件都允许访问的代码(比如处理字符 I/O 的代码)。因此可以为这些过程设置一个所有特权级代码都能访问的调用门。另外可以专门为仅用于操作系统的代码设置一些更高特权级的调用门。

1.4 堆栈切换

   每当 调用门 用于把程序控制转移到一个 更高级别非一致性 代码段时,CPU 会自动切换到目的代码段特权级堆栈去。 。执行栈切换操作的目的是为了防止高特权级程序由于栈空间不足而引起崩激,同时也为了防止低特权级程序通过共享的堆栈有意或无意地干扰高特权级的程序。

  每个任务必须定义最多 4 个栈。一个用于运行在特权级 3 的应用程序代码,其他分别用于用到的特权级 2、1 和 0 。如果一个系统中只使用了 3 和 0 两个特权级,那么每个任务就只需设置两个栈。每个栈都位于不同的段中,并且使用段选择符和段中偏移值指定。

  当特权级 3 的程序在执行时,特权级 3 的堆栈的段选择符和栈指针会被分别存放在 SS 和 ESP 中,并且在发生堆栈切换时被保存在被调用过程的堆栈上。

  特权级 0、1 和 2 的 堆栈的初始指针值 都存放在当前运行任务的 TSS 段中,TSS 段中这些指针都是只读值。在任务运行时 CPU 并不会修改它们。当调用更高特权级程序时,CPU 才用它们来建立新堆栈。当从调用过程返回时,相应栈就不存在了。下一次再调用该过程时,就又会再次使用 TSS 中的初始指针值建立一个新栈。

  操作系统需要负责为所有用到的特权级 建立堆栈 和 堆栈段描述符 ,并且在任务的 TSS 中设置初始指针值。每个栈必须可读可写,并且具有足够的空间来存放以下一些信息:


调用过程的 SS、ESP、CS 和 EIP 寄存器内容;

被调用过程的参数和临时变量所需使用的空间。

当隐含调用一个异常或中断过程时标志寄存器 EFLAGS 和出错码使用的空间。

  由于一个过程可调用其它过程,因此每个栈必须有足够大的空间来容纳多帧(多套)上述信息。

  当通过调用门执行一个过程调用而造成特权级改变时,CPU就会执行以下步骤切换堆栈并开始在新的特权级上执行被调用过程(见图 4-25 所示):

  1. 使用目的代码段的 DPL(即新的 CPL)从 TSS 中选择新栈的指针。从当前 TSS 中读取新栈的段选择符和栈指针。在读取栈段选择符、栈指针或栈段描述符过程中,任何违反段界限的错误都将导致产生一个无效 TSS 异常;
  2. 检查栈段描述符特权级和类型是否有效,若无效者同样产生一个无效 TSS 异常。
  3. 临时保存 SSESP 寄存器的当前值,把新栈的 段选择符栈指针 加载到 SSESP 中。然后把临时保存的 SSESP 内容压入新栈中。
  4. 把调用门描述符中指定参数个数的参数从调用过程栈复制到新栈中。调用门中参数个数值最大为 31,如果个数为 0,则表示无参数,不需复制。
  5. 把返回指令指针(即当前 CSEIP 内容)压入新栈。把新(目的)代码段选择符加载到 CS 中,同时把调用门中偏移值(新指令指针)加载到 EIP 中。最后开始执行被调用过程。


1.5 从被调用过程返回

  指令 RET 用于执行近返回(near return)、同特权级远返回(far return)和不同特权级的远返回。该指令用于从使用 CALL 指令调用的过程中返回。近返回仅在当前代码段中转移程序控制权,因此 CPU 仅进行界限检查。对于相同特权级的远返回,CPU 同时从堆栈中弹出返回代码段的选择符和返回指令指针。由于通常情况下这两个指针是 CALL 指令压入栈中的,因此它们因该是有效的。但是 CPU 还是会执行特权级检查以应付当前过程可能修改指针值或者堆栈出现问题时的情况。

  会发生特权级改变的远返回仅允许返回到低特权级程序中,即返回到的代码段 DPL 在数值上要大于 CPL。CPU 会使用 CS 寄存器中选择符的 RPL 字段来确定是否要求返回到低特权级。如果 RPL 的数值要比 CPL 大,就会执行特权级之间的返回操作。当执行远返回到一个调用过程时,CPU 会执行以下步骤:

  1. 检查保存的 CS 寄存器中 RPL 字段值,以确定在返回时特权级是否需要改变。
  2. 弹出并使用被调用过程堆栈上的值加载 CSEIP 寄存器。在此过程中会对代码段描述符和代码段选择符的 RPL 进行特权级与类型检查。
  3. 如果 RET 指令包含一个参数个数操作数并且返回操作会改变特权级,那么就在弹出栈中 CSEIP 值之后把参数个数值加到 ESP 寄存器值中,以跳过(丢弃)被调用者栈上的参数。此时 ESP 寄存器指向原来保存的调用者堆栈的指针 SSESP
  4. 把保存的 SSESP 值加载到 SSESP 寄存器中,从而切换回调用者的堆栈。而此时被调用者堆栈的 SSESP 值被抛弃。
  5. 如果 RET 指令包含一个参数个数操作数,则把参数个数值加到 ESP 寄存器值中,以跳过(丢弃)调用者栈上的参数。
  6. 检查段寄存器 DSESFSGS 的内容。如果其中有指向 DPL 小于新 CPL 的段(一致代码段除外),那么 CPU 就会用 NULL 选择符加载加载这个段寄存器。

八、中断和异常处理

  中断(Interrupt)和异常(Exception)是指明系统、处理器或当前执行程序(或任务)的某处出现一个事件,该事件需要处理器进行处理。通常,这种事件会导致执行控制被强迫从当前运行程序转移到被称为中断处理程序(interrupt handler)或异常处理程序(exception handler)的特殊软件函数或任务中。处理器响应中断或异常所采取的行动被称为中断/异常服务(处理)。

  通常,中断发生在程序执行的随机时刻,以响应硬件发出的信号。系统硬件使用中断来处理外部事件,例如要求为外部设备提供服务。当然,软件也能通过执行 INT n 指令产生中断。

  异常发生在处理器执行一条指令时,检测到一个出错条件时发生,例如被 0 除出错条件。处理器可以检测到各种出错条件,包括违反保护机制、页错误以及机器内部错误。

  对应用程序和操作系统来说,80X86 的中断和异常处理机制可以透明地处理发生的中断和异常事件。当收到一个中断或检测到一个异常时,处理器会自动地把当前正在执行的程序或任务挂起,并开始运行中断或异常处理程序。当处理程序执行完毕,处理器就会恢复并继续执行被中断的程序或任务。被中断程序的恢复过程并不会失去程序执行的连贯性,除非从异常中恢复是不可能的或者中断导致当前运行程序被终止。本节描述保护模式中处理器中断和异常的处理机制。


1、异常和中断向量

  为了有助于处理异常和中断,每个需要被处理器进行特殊处理的处理器定义的异常和中断条件都被赋予了一个标识号,称为向量(vector)。处理器把赋予异常或中断的向量用作中断描述符表 IDT(Interrupt Descriptor Table)中的一个索引号,来定位一个异常或中断的处理程序入口点位置。

  允许的向量号范围是 0 到 255。其中 0 到 31 保留用作 80X86 处理器定义的异常和中断,不过目前该范围内的向量号并非每个都已定义了功能,未定义功能的向量号将留作今后使用。

  范围在 32 到 255 的向量号用于用户定义的中断。这些中断通常用于外部 I/O 设备,使得这些设备可以通过外部硬件中断机制向处理器发送中断。对于每个异常,该表给出了异常类型以及是否会产生一个错误码并保存在堆栈上。同时还给出了每个预先定义好的异常和 NMI 中断源。

2、中断源和异常源

2.1 中断源

处理器从两种地方接收中断:

  • 外部(硬件产生)的中断;
  • 软件产生的中断。


  外部中断通过处理器芯片上两个引脚(INTR 和 NMI)接收。当引脚 INTR 接收到外部发生的中断信号时,处理器就会从系统总线上读取外部中段控制器(例如 8259A)提供的中断向量号。当引脚 NMI 接收到信号时,就产生一个非屏蔽中断。它使用固定的中断向量号 2。在何通过处理器 INTR 引脚接收的外部中断都被称为可屏蔽硬件中断,包括中断向量号 0 到 255。标志寄存器 EFLAGS 中的 IF 标志可用来屏蔽所有这些硬件中断。

  通过在指令操作数中提供中断向量号,INT n 指令可用于从软件中产生中断。例如,指令 INT 0x80 会执行 Linux 的系统中断调用中断 0x80。向量 0 到 255 中的任何一个都可以用作 INT 指令的中断号。然而,如果使用了处理器预先定义的 NMI 向量,那么处理器对它的响应将与普通方式产生的该 NMI 中断不同。如果 NMI 的向量号 2 用于该 INT 指令,就会调用 NMI 的中断处理器程序,但是此时并不会激活处理器的 NMI 处理硬件。

  注意,EFLAGS 中的 IF 标志不能屏蔽使用 INT 指令从软件中产生的中断。

2.2 异常源

处理器接收的异常也有两个来源:

  • 处理器检测到的程序错误异常;
  • 软件产生的异常。

  在应用程序或操作系统执行期间,如果处理器检测到程序错误,就会产生一个或多个异常。80X86 处理器为其检测到的每个异常定义了一个向量。异常可以被细分为故障(faults)、陷阱(traps)和中止(aborts),见后面说明。

  指令 INTO、INT 3 和 BOUND 指令可以用来从软件中产生异常。这些指令可对指令流中指定点执行的特殊异常条件进行检查。例如,INT 3 指令会产生一个断点异常。

  INT n 指令可用于在软件中模拟指定的异常,但有一个限制。如果 INT 指令中的操作数 n 是 80X86 异常的向量号之一,那么处理器将为该向量号产生一个中断,该中断就会去执行与该向量有关的异常处理程序。但是,因为这实际上是一个中断,因此处理器并不会把一个错误号压入堆栈,即使硬件产生的该向量相关的中断通常会产生一个错误码。对于那些会产生错误码的异常,异常的处理程序会试图从堆找上弹出错误码。因此,如果使用 INT 指令来模拟产生一个异常,处理程序则会把 EIP(正好处于缺少的错误码位置处)弹出堆栈,从而会造成返回位置错误。

3、异常分类

   根据异常被报告的方式以及导致异常的指令是否能够被重新执行,异常可被细分成故障(Fault)、陷阱(Trap)和中止(Abort)。

Fault 是一种通常可以被纠正的异常,并且一旦被纠正程序就可以继续运行。当出现一个 Fault,处理器会把机器状态恢复到产生 Fault 的指令之前的状态。此时异常处理程序的返回地址会指向。产生 Fault 的指令,而不是其后面一条指令。因此在返回后产生 Fault 的指令将被重新执行。

  • Trap 是一个引起陷阱的指令被执行后立刻会报告的异常。Trap 也能够让程序或任务连贯地执行。Trap 处理程序的返回地址指向引起陷饼指令的随后一条指令,因此在返回后会执行下一条指令。
  • Abort 是一种不会总是报告导致异常的指令的精确位置的异常,并且不允许导致异常的程序重新继续执行。Abort 用于报告严重错误,例如硬件错误以及系统表中存在不一致性或非法值。

4、程序或任务的重新执行

  为了让程序或任务在一个异常或中断处理完之后能重新恢复执行,除了中止(Abort)之外的所有异常都能报告精确的指令位置,并且所有中断保证是在指令边界上发生。

  对于故障类异常,处理器产生异常时保存的返回指针指向出错指令。因为,当程序或任务在故障处理程序返回后重新开始执行时,原出错指令会被重新执行。重新执行引发出错的指令通常用于处理访问指令操作数受阻的情况。Fault 最常见的一个例子是页面故障(Page-fault)异常。当程序引用不在内存中页面上的一个操作数时就会出现这种异常。当页故障异常发生时,异常处理程序可以把该页面加载到内存中并

通过重新执行出错指令来恢复程序执行。为了确保重新执行对于当前执行程序具有透明性,处理器会保存必要的寄存器和堆栈指针信息,以使得自己能够返回到执行出错指令之前的状态。

  对于陷阱 Trap 类异常,处理器产生异常时保存的返回指针指向引起陷阱操作的后一条指令。如果在一条执行控制转移的指令执行期间检测到一个 Trap,则返回指令指针会反映出控制的转移情况。例如,如果在执行 JMP 指令时检测到一个 Trap 异常,那么返回指令指针会指向 JMP 指令的目标位置,而非指向 JMP 指令随后的一条指令。

  中止 Abort 类异常不支持可靠地重新执行程序或任务。中止异常的处理程序通常用来收集异常发生时有关处理器状态的诊断信息,并且尽可能恰当地关闭程序和系统。

  中断会严格地支持被中断程序的重新执行而不会丢失任何连贯性。中断所保存的返回指令指针指向处理器获取中断时将要执行的下一条指令边界处。如果刚执行的指令有一个重复前缀,则中断会在当前重复结束并且寄存器已为下一次重复操作设置好时发生。

5、中断描述符表

  中断描述符表 IDT(Interrupt Descriptor Table)将每个异常或中断向量分别与它们的处理过程联系起来。与 GDT 和 LDT 表类似,IDT 也是由 8 字节长描述符组成的一个数组。与 GDT 不同的是,表中第 1 项可以包含描述符。为了构成 IDT 表中的一个索引值,处理器把异常或中断的向量号*8。因为最多只有 256 个中断或异常向量,所以 IDT 无需包含多于 256 个描述符。IDT 中可以含有少于 256 个描述符,因为只有可能发生的异常或中断才需要描述符。不过 IDT 中所有空描述符项应该设置其存在位(标志)为 0。

  IDT 表可以驻留在线性地址空间的任何地方,处理器使用 IDTR 寄存器来定位 IDT 表的位置。这个寄存器中含有 IDT 表 32 位的基地址和 16 位的长度(限长)值,见图 4-26 所示。IDT 表基地址应该对齐在 8 字节边界上以提高处理器的访问效率。限长值是以字节为单位的 IDT 表的长度。

   指令 LIDTSIDT 指令分别用于加载和保存 IDTR 寄存器的内容。LIDT 指令把在内存中的限长值和基地址操作数加载到 IDTR 寄存器中。该指令仅能由当前特权级 CPL0 的代码执行,通常被用于创建 IDT 时的操作系统初始化代码中。SIDT 指令用于把 IDTR 中的基地址和限长内容复制到内存中。该指令可在任何特权级上执行。

   如果中断或异常向量引用的描述符超过了 IDT 的界限,处理器会产生一个一般保护性异常。

6、IDT 描述符

IDT 表中可以存放三种类型的门描述符:

  • 中断门(Interrupt gate)描述符
  • 陷阱门(Trap gate)描述符
  • 任务门(Task gate)描述符

  图 4-27 给出了这三种门描述符的格式。中断门和陷阱门含有一个长指针(即段选择符和偏移值),处理器使用这个长指针把程序执行权转移到代码段中异常或中断的处理过程中。这两个段的主要区别在于处理器操作 EFLAGS 寄存器 IF 标志上。IDT 中任务门描述符的格式与 GDT 和 LDT 中任务门的格式相同。任务门描述符中含有一个任务 TSS 段的选择符,该任务用于处理异常和/或中断。

7、异常与中断处理

   处理器对异常和中断处理过程的调用操作方法与使用 CALL 指令调用程序过程和任务的方法类似。当响应一个异常或中断时,处理器使用异常或中断的向量作为 IDT 表中的索引。如果索引值指向中断门陷阱门则处理器使用与 CALL 指令操作调用门类似的方法调用异常或中断处理过程。如果索引值指向任务门,则处理器使用与 CALL 指令操作任务门类似的方法进行任务切换,执行异常或中断的处理任务。

   异常或中断门引用运行在当前任务上下文中的异常或中断处理过程,见图 4-28 所示。门中的段选择符指向 GDT 或当前 LDT 中的可执行代码段描述符。门描述符中的偏移字段指向异常或中断处理过程的开始处。


当处理器执行异常或中断处理过程调用时会进行以下操作:

处理器从当前执行任务的 TSS 段中得到中断或异常处理过程使用的堆栈的段选择符和栈指针(例如 tss.ss0、tss.esp0)。然后处理器会把被中断程序(或任务)的 栈选择符 和 栈指针 压入新栈中,见图 4-29 所示。

接着处理器会把 EFLAGS、CS 和 EIP 寄存器的当前值也压入新栈中。

如果异常会产生一个错误号,那么该错误号也会被最后压入新栈中。

如果处理过程将在被中断任务同一个特权级上运行,那么:

处理器把 EFLAGS、CS 和 EIP 寄存器的当前值保存在当前堆栈上。

如果异常会产生一个错误号,那么该 错误号 也会被最后压入新栈中。

   为了从中断处理过程中返回,处理过程必须使用 IRET 指令。IRET 指令与 RET 指令类似,但 IRET 还会把保存的寄存器内容恢复到 EFLAGS 中。不过只有当 CPL 是 0 时才会恢复 EFLAGS 中的 IOPL 字段,并且只有当 CPL<=IOPL 时,

   为了从中断处理过程中返回,处理过程必须使用 IRET 指令。IRET 指令与 RET 指令类似,但 IRET 还会把保存的寄存器内容恢复到 EFLAGS 中。不过只有当 CPL 是 0 时才会恢复 EFLAGS 中的 IOPL 字段,并且只有当 CPL<=IOPL 时,IF 标志才会被改变。如果当调用中断处理过程时发生了堆找切换,那么在返回时 IRET 指令会切换回到原来的堆栈。

  1. 异常和中断处理过程的保护

制转移到比 CPL 更低特权级代码段的中断处理过程中,否则将产生一个一般保护性异常。另外,中断和异常的保护机制在以下方面与一般调用门过程不同:

因为中断和异常向量没有 RPL,因此在隐式调用异常和中断处理过程时不会检查 RPL。

只有当一个异常或中断是利用使用 INT n、INT 3 或 INTO 指令产生时,处理器才会检查中断或陷阱门中的 DPL。此时 CPL 必须小于等于门的 DPL。这个限制可以防止运行在特权级 3 的应用程序使用软件中断访问重要的异常处理过程,例如页错误处理过程,假设这些处理过程已被存放在更高特权级的代码段中。对于硬件产生的中断和处理器检测到的异常,处理器会忽略中断门和陷阱门中的 DPL。

  因为异常和中断通常不会定期发生,因此这些有关特权级的规则有效地增强了异常和中断处理过程能够运行的特权级限制。我们可以利用以下技术之一来避免违反特权级保护:


异常或中断处理程序可以存放在一个一致性代码段中。这个技术可以用于只需访问堆栈上数据的处理过程(例如,除出错异常)。如果处理程序需要数据段中的数据,那么特权级 3 必须能够访问这个数据段。但这样一来就没有保护可言了。

处理过程可以放在具有特权级 0 的非一致代码段中。这种处理过程总是可以执行的,而不管被中断程序或任务的当前特权级 CPL 。

2, 异常或中断处理过程的标志使用方式

  当通过中断门或陷阱门访问一个异常或中断处理过程时,处理器会在把 EFLAGS 寄存器内容保存到堆栈上之后清除 EFLAGS 中的 TF 标志。清除 TF 标志可以防止指令跟踪影响中断响应。而随后的 IRET 指令会用堆栈上的内容恢复 EFLAGS 的原 TF 标志。

  中断门与陷阱门唯一的区别在于处理器操作 EFLAGS 寄存器 IF 标志的方法。当通过中断门访问一个异常或中断处理过程时,处理器会复位 IF 标志以防止其他中断干扰当前中断处理过程。随后的 IRET 指令则会用保存在堆栈上的内容恢复 EFLAGS 寄存器的 IF 标志。而通过陷阱门访问处理过程并不会影响 IF 标志。

3. 执中断处理过程的任务

  当通过 IDT 表中任务门访问异常或中断处理过程时,就会导致任务切换。从而可以在一个专用任务中执行中断或异常处理过程。IDT 表中的任务门引用 GDT 中的 TSS 描述符。切换到处理过程任务的方法与普通任务切换一样。由于本书讨论的 Linux 操作系统没有使用这种中断处理方式,因此这里不再赞述。

8、中断处理任务

   当通过 IDT 中任务门来访问异常或中断处理过程时就会导致任务切换。使用单独的任务来处理异常或中断有如下好处:

  • 被中断程序或任务的完整上下文会被自动保存;
  • 在处理异常或中断时,新的 TSS 可以允许处理过程使用新特权级 0 的堆栈。在当前特权级 0 的堆栈已毁坏时如果发生了一个异常或中断,那么在为中断过程提供一个新特权级 0 的堆栈条件下,通过任务门访问中断处理过程能够防止系统崩溃;
  • 通过使用单独的 LDT 给中断或异常处理任务独立的地址空间,可以把它与其他任务隔离开来。

   使用独立任务处理异常或中断的不足之处是:在任务切换时必须对大量机器状态进行保存,使得它比使用中断门的响应速度要慢,导致中断延时增加。

   IDT 中的任务门会引用 GDT 中的 TSS 描述符,图 4-30 所示。切换到句柄任务的过程与普通任务切换过程相同。到被中断任务的反向链接会被保存在句柄任务 TSS 的前一任务链接字段中。如果一个异常会产生一个出错码,则该出错码会被复制到新任务堆栈上。

   当异常或中断句柄任务用于操作系统中时,实际上有两种分派调度任务的机制:操作系统软件调度和处理器中断机制的硬件调度。使用软件调度方法时需要考虑到中断开启时采用中断处理任务。


九、任务管理

  任务(Task)是处理器可以分配调度、执行和挂起的一个工作单元。它可用于执行程序、任务或进程、操作系统服务、中断或异常处理过程和内核代码。

  80X86 提供了一种机制,这种机制可用来保存任务的状态、分派任务执行以及从一个任务切换到另一个任务。当工作在保护模式下,处理器所有运行都在任务中。即使是简单系统也必须起码定义一个任务。更为复杂的系统可以使用处理器的任务管理功能来支持多任务应用。

  80X86 提供了多任务的硬件支持。任务是一个正在运行的程序,或者是一个等待准备运行的程序。通过中断、异常、跳转或调用,我们可以执行一个任务。当这些控制转移形式之一和某个描述符表中指定项的内容一起使用时,那么这个描述符是一类导致新任务开始执行的描述符。描述符表中与任务相关的描述符有两类:任务状态段描述符 和 任务门。当执行权传给这任何一类描述符时,都会造成任务切换。

  任务切换很像过程调用,但任务切换会保存更多的处理器状态信息。任务切换会把控制权完全转移到一个新的执行环境,即新任务的执行环境。这种转移操作要求保存处理器中几乎所有寄存器的当前内容,包括标志寄存器 EFLAGS 和所有段寄存器。与过程不同,任务不可重入。任务切换不会把任何信息压入堆栈中,处理器的状态信息都被保存在内存中称为 任务状态段(Task state segment)的数据结构中。

1、任务的结构和状态

  一个任务由两部分构成:任务执行空间和任务状态段 TSS(Task-state segment)。任务执行空间包括代码段、堆栈段和一个或多个数据段,见图 4-33 所示。如果操作系统使用了处理器的特权级保护机制,那么任务执行空间就需要为每个特权级提供一个独立的堆栈空间。TSS 指定了构成任务执行空间的各个段,并且为任务状态信息提供存储空间。在多任务环境中,TSS 也为任务之间的链接提供了处理方法。

   一个任务使用指向其 TSS 的段选择符来指定。 当一个任务被加载进处理器中执行时,那么该任务的段选择符、基地址、 段限长以及 TSS 段描述符属性就会被加载进任务寄存器 TR(Task Register)中。如果使用了分页机制,那么任务使用的 页目录表基地址 就会被加载进控制寄存器 CR3 中。当前执行任务的状态由处理器中的以下所有内容组成:


所有通用寄存器和段寄存器信息;

标志寄存器 EFLAGS、程序指针 EIP、控制寄存器 CR3、任务寄存器和 LDTR 寄存器;

段寄存器指定的任务当前执行空间;

I/O 映射位图基地址和 I/O 位图信息(在 TSS 中);

特权级 0、1 和 2 的堆栈指针(在 TSS 中);

链接至前一个任务的链指针(在 TSS 中)。

2、任务的执行

   软件或处理器可以使用以下方法之一来调度执行一个任务:

  • 使用 CALL 指令明确地调用一个任务;
  • 使用 JMP 指令明确地跳转到一个任务(Linux 内核使用的方式);
  • (由处理器)隐含地调用一个中断句柄处理任务
  • 隐含地调用一个异常句柄处理任务

  所有这些调度任务执行的方法都会使用一个指向 任务门 或任务 TSS 段的选择符来确定一个任务。当使用 CALL 或 JMP 指令调度一个任务时,指令中的选择符既可以直接选择任务的 TSS,也可以选择存放有 TSS 选择符的任务门。当调度一个任务来处理一个中断或异常时,那么 IDT 中该中断或异常表项必须是一个任务门,并且其中含有中断或异常处理任务的 TSS 选择符。

  当调度一个任务执行时,当前正在运行任务和调度任务之间会自动地发生任务切换操作。在任务切换期间,当前运行任务的执行环境(称为任务的状态或上下文)会被保存到它的 TSS 中并且暂停该任务的执行。此后新调度任务的上下文会被加载进处理器中,并且从加载的 EIP 指向的指令处开始执行新任务。

  如果当前执行任务(调用者)调用了被调度的新任务(被调用者),那么调用者的 TSS 段选择符会被保存在被调用者 TSS 中,从而提供了一个返回调用者的链接。对于所有 80X86 处理器,任务是不可递归调用的,即任务不能调用或跳转到自己。

  中断或异常可以通过切换到一个任务来进行处理。在这种情况下,处理器不仅能够执行任务切换来处理中断或异常,而 且也会在中断或异常处理任务返回时自动地切换回被中断的任务中去。这种操作方式可以处理在中断任务执行时发生的中断。

  作为任务切换操作的一部份,处理器也会切换到另一个 LDT,从而允许每个任务对基于 LDT 的段具有不同逻辑到物理地址的映射。同时,页目录寄存器 CR3 也会在切换时被重新加载,因此每个任务可以有自己的一套页表。这些保护措施能够用来隔绝各个任务并且防止它们相互干扰。

  使用处理器的任务管理功能来处理多任务应用是任选的。我们也可以使用软件来实现多任务,使得每个软件定义的任务在一个 80X86 体系结构的任务上下文中执行。

3、任务管理数据结构

   处理器定义了一下一些支持多任务的寄存器和数据结构:

  • 任务状态段 TSS
  • TSS 描述符;
  • 任务寄存器 TR
  • 任务门描述符;
  • 标志寄存器 EFLAGS 中的 NT 标志。

   使用这些数据结构,处理器可以从一个任务切换到另一个任务,同时保存原任务的上下文,以允许任务重新执行。

3.1 任务状态段

   用于恢复一个任务执行的处理器状态信息被保存在称为任务状态段 TSS(Task state segment)的段中。图 4-34 给出了 32 位 CPU 使用的 TSS 的格式。TSS 段中各字段可分成两大类:动态字段静态字段

  1. 动态字段 。当任务切换而被挂起时,处理器会更新动态字段的内容。这些字段包括:
  • 通用寄存器字段。用于保存 EAXEBXECXEDXESPEBPESIEDI 寄存器的内容。
  • 段选择符字段。用于保存 ESCSSSDSFSGS 段寄存器的内容。
  • 标志寄存器 EFLAGS 字段。在切换之前保存 EFLAGS。
  • 指令指针 EIP 字段。在切换之前保存 EIP 寄存器内容。
  • 先前任务连接字段。含有前一个任务 TSS 段选择符(在调用、中断或异常激发的任务切换时更新)。该字段(通常也称为后连接字段(Back link field))允许任务使用 IRET 指令切换到前一个任务。
  1. 静态字段。处理器会读取静态字段的内容,但通常不会改变它们。这些字段内容是在任务被创建时设置的。这些字段有:

LDT 段选择符字段。含有任务的 LDT 段的选择符。

CR3 控制寄存器字段。含有任务使用的页目录物理基地址。控制寄存器 CR3 通常也被称为页目录基地址寄存器 PDBR(Page directory base register)。

特权级 0、1 和 2 的堆栈指针字段。这些堆栈指针由堆栈段选择符(SS0、SS1 和 SS2)和栈中偏移量指针(ESP0、ESP1 和 ESP2)组成。注意,对于指定的一个任务,这些字段的值是不变的。因此,如果任务中发生堆找切换,寄存器 SS 和 ESP 的内容将会改变。

调试陷阱(Debug Trap)T 标志字段。该字段位于字节 0x64 比特 0 处。当设置了该位时,处理器切换到该任务的操作将产生一个调试异常。

I/O 位图基地址字段。该字段含有从 TSS 段开始处到 I/O 许可位图处的 16 位偏移值。

  如果使用了分页机制,那么在任务切换期间应该避免处理器操作的 TSS 段中(前 104 字节中,备注:104 正好是上面所列的 TSS 的大小 0x68)含有内存页边界。如果 TSS 这部分包含内存页边界,那么该边界处两边的页面都必须同时并且连续存在于内存中。另外,如果使用了分页机制,那么与原任务 TSS 和新任务 TSS 相关的页面,以及对应的描述符表表项应该是可读写的。

3.2 TSS 描述符

   与其他段一样,任务状态段 TSS 也是使用段描述符来定义。图 4-35 给出了 TSS 描述符的格式。TSS 描述符只能存放在 GDT 中。

  类型字段 TYPE 中的忙标志 B 用于指明任务是否处于忙状态。忙状态的任务是当前正在执行的任务或等待执行(被挂起)的任务。值为 0b1001 的类型字段表明任务处于非活动状态;而值为 0b1011 的类型字段表示任务正忙。任务是不可以递归执行的,因此处理器使羽忙标志 B 来检测任何企图对被中断执行任务的调用。

  其中基地址、段限长、描述符特权级 DPL、颗粒度 G 和存在位具有与数据段描述符中相应字段同样的功能。当 G=0 时,限长字段必须具有等于或大于 103(0x67)的值,即 TSS 段的最小长度不得小于 104 字节。如果 TSS 段中还包含 I/O 许可位图,那么 TSS 段长度需要大一些。另外,如果操作系统还想在 TSS 段中存放其他一些信息,那么 TSS 段就需要更大的长度。

  使用调用或跳转指令,任何可以访问 TSS 描述符的程序都能够造成任务切换。可以访问 TSS 描述符的程序其 CPL 数值必须小于或等于 TSS 描述符的 DPL。在大多数系统中,TSS 描述符的 DPL 字段值应该设置成小于 3。这样,只有具有特权级的软件可以执行任务切换操作。然而在多任务应用中,某些 TSS 的 DPL 可以设置成 3,以使得在用户特权级上也能进行任务切换操作。

  可访问一个 TSS 段描述符并没有给程序读写该描述符的能力。若想读或修改一个 TSS 段描述符,可以使用映射到内存相同位置的数据段描述符(即别名描述符)来操作。把 TSS 描述符加裁进任何段寄存器将导致一个异常。企图使用 TI 标志置位的选择符(即当前 LDT 中的选择符)来访问 TSS 段也将导致异常。

3.3 任务寄存器

  任务寄存器 TR(Task Register》中存放着 16 位的段选择符以及当前任务 TSS 段的整个描述符(不可见部分)。这些信息是从 GDT 中当前任务的 TSS 描述符中复制过来的。处理器使用任务寄存器 TR 的不可见部分来缓冲 TSS 段描述符内容。

  指令 LTR 和 STR 分别用于加载和保存任务寄存器的可见部分,即 TSS 段的选择符。LTR 指令只能被特权级 0 的程序执行。LTR 指令通常用于系统初始化期间给 TR 寄存器加裁初值(例如,任务 0 的 TSS 段选择符),随后在系统运行期间,TR 的内容会在任务切换时自动地被改变。

3.4 任务门描述符

  任务门描述符(Task gate descriptor)提供对一个任务间接、受保护地的引用,其格式见图所示。任务门描述符可以被存放在 GDT、LDT 或 IDT 表中。

  任务门描述符中的 TSS 选择符字段指向 GDT 中的一个 TSS 段描述符。这个 TSS 选择符字段中的 RPL 域不用。任务门描述符中的 DPL 用于在任务切换时控制对 TSS 段的访问。当程序通过任务门调用或跳转到一个任务时,程序的 CPL 以及指向任务门的门选择符的 RFL 值必须小于或等于任务门描述符中的 DPL 请注意,当使用任务门时,目标 TSS 段描述符的 DPL 忽略不用。

  程序可以通过任务门描述符或者 TSS 段描述符来访问一个任务。图 4-36 示出了 LDT、GDT 和 IDT 表中的任务门如何都指向同一个任务。

4、任务切换

   处理器可使用一下 4 种方式之一执行任务切换操作:

  1. 当前任务对 GDT 中的 TSS 描述符执行 JMPCALL 指令;
  2. 当前任务对 GDTLDT 中的任务门描述符执行 JMPCALL 指令;
  3. 中断或异常向量指向 IDT 表中的任务门描述符;
  4. EFLAGS 中的 NT 标志置位时当前任务执行 IRET 指令。

  JMP、CALL 和 IRET 指令以及中断和异常都是处理器的普通机制,可用于不发生任务切换的环境中。对于 TSS 描述符或任务门的引用(当调用或跳转到一个任务),或者 NT 标志的状态(当执行 IRET 指令时)确定了是否会发生任务切换。

  为了进行任务切换,JMP 或 CALL 指令能够把控制转移到 TSS 描述符或任务门上。使用这两种方式的作用相同,都会导致处理器把控制转移到指定的任务中,见图 4-37 所示。

  当中断或异常的向量索引的是 IDT 中的一个任务门时,一个中断或异常就会造成任务切换。如果向量索引的是 IDT 中的一个中断或陷阱门,则不会造成任务切换。

  中断服务过程总是把执行权返回到被中断的过程中,被中断的过程可能在另一个任务中。如果 NT 标志处于复位状态,则执行一般返回处理。如果 NT 标志是置位状态,则返回操作会产生任务切换。切换到的新任务由中断服务过程 TSS 中的 TSS 选择符(前一任务链接字段)指定。

当切换到一个新任务时,处理器会执行一下操作:

  1. 从作为 JMPCALL 指令操作数中,或者从任务门中,或者从当前 TSS 的前一任务链接字段(对于由 IRET 引起的任务切换)中取得新任务的 TSS 段选择符。
  2. 检查当前任务是否允许切换到新任务。把数据访问特权级规则应用到 JMP 和 CALL 指令上。当前任务的 CPL 和新任务段选择符的 RPL 必须小于或等于 TSS 段描述符的 DPL,或者引用的是一个任务门。无论目标任务门或 TSS 段描述符的 DPL 是何值,异常、中断(除了使用 INT n 指令产生的中断)和 IRET 指令都允许执行任务切换。对于 INT n 指令产生的中断将检查 DPL。
  3. 检查新任务的 TSS 描述符是标注为存在的(P=1),并且 TSS 段长度有效(大于 0x67)。当试图执行会产生错误的指令时,都会恢复对处理器状态的任何改变。这使得异常处理过程的返回地址指向出错指令,而非出错指令随后的一条指令。因此非常处理过程可以处理出错条件并且重新执行任务。异常处理过程的介入处理对应用程序来说是完全透明的。
  4. 如果任务切换产生自 JMP 或 IRET 指令,处理器就会把当前任务(老任务)TSS 描述符中的忙标志 B 复位;如果任务切换是由 CALL 指令、异常或中断产生,则忙标志 B 不动。
  5. 如果任务切换由 IRET 产生,则处理器会把临时保存的 EFLAGS 映像中的 NT 标志复位;如果任务切换由 CALL、JMP 指令或者异常或中断产生,则不用改动上述 NT 标志。
  6. 把当前任务的状态保存到当前任务的 TSS 中。处理器会从任务寄存器中取得当前任务 TSS 的基地址,并且把一下寄存器内容复制到当前 TSS 中:所有通用寄存器、段寄存器中的段选择符、标志寄存器 EFLAGS 以及指令指针 EIP。
  7. 如果任务切换是由 CALL 指令、异常或中新产生,则处理器就会把从新任务中加载的 EFLAGS 中的 NT 标志置位。如果任务切换产生自 JMP 或 IRET 指令,就不改动新加载 EFLAGS 中的标志。
  8. 如果任务切换由 CALL、JMP 指令或者异常或中断产生,处理器就会设置新任务 TSS 描述符中的。忙标志 B。如果任务切换由 IRET 产生,则不去改动 B 标志。
  9. 使用新任务 TSS 的段选择符和描述符加载任务寄存器 TR(包括隐藏部分)。设置 CR0 寄存器的 TS 标志。
  10. 把新任务的 TSS 状态加载进处理器。这包括 LDTR 寄存器、PDBR(CR3)寄存器、EFLAGS 寄存器、EIP 寄存器以及通用寄存器和段选择符。在此期间检测到的任何错误都将出现在新任务的上下文中。
  11. 开始执行新任务(对于异常处理过程,新任务的第一条指令显现出还没有执行)。

  当成功地进行了任务切换操作,当前执行任务的状态总是会被保存起来。当任务恢复执行时,任务将从保存的 EIP 指向的指令处开始执行,并且所有寄存器都恢复到任务挂起时的值。

  当执行任务切换时,新任务的特权级与原任务的特权级没有任何关系。新任务在 CS 寄存器的 CPL 字段指定的特权级上开始运行。因为各个任务通过它们独立的地址空间和 TSS 段相互隔绝,并且特权级规则已经控制对 TSS 的访问,所以在任务切换时软件不需要再进行特权级检查。

  每次任务切换都会设置控制寄存器 CR0 中的任务切换标志 TS。该标志对系统软件非常有用。系统软件可用 TS 标志来协调处理器和浮点协处理器之间的操作。TS 标志表明协处理器中的上下文内容可能与当前正在执行任务的不一致。

 

5、任务链

   TSS 的前一任务连接(Backlink)字段以及 EFLAGS 中的 NT 标志用于返回到前一个任务操作中。NT 标志指出了当前执行的任务是否是嵌套在另一个任务中执行,并且当前任务的前一任务连接字段中存放着嵌套层中更高层任务的 TSS 选择符,若有的话(见图 4-38 所示)。

  当 CALL 指令、中断或异常造成任务切换,处理器把当前 TSS 段的选择符复制到新任务 TSS 段的前一任务链接字段中,然后在 EFLAGS 中设置 NT 标志。NT 标志指明 TSS 的前一任务链接字段中存放有保存的 TSS 段选择符。如果软件使用 IRET 指令挂起新任务,处理器就会使用前一任务链接字段中值和 NT 标志返回到前一个任务。也即如果 NT 标志是置位的话,处理器会切换到前一任务链接字段指定的任务去执行。

  注意,当任务切换是由 JMP 指令造成,那么新任务就不会是嵌套的。也即,NT 标志会被设置为 0,并且不使用前一任务链接字段。JMP 指令用于不希望出现嵌套的任务切换中。

  表 4-10 总结了任务切换期间,忙标志 B(在 TSS 段描述符中)、NT 标志、前一任务链接字段和 TS 标志(在 CR0 中)的用法。注意,运行于任何特权级上的程序都可以修改 NT 标志,因此任何程序都可以设置 NT 标志并执行 IRET 指令。这种做法会让处理器去执行当前任务 TSS 的前一任务链接字段指定的任务。为了避免这种伪造的任务切换执行成功,操作系统应该把每个 TSS 的该字段初始化为 0。

6、任务地址空间

  任务的地址空间由任务能够访问的段构成。这些段包括代码段、数据段、堆栈段、TSS 中引用的系统段以及任务代码能够访问的任何其他段。这些段都被映射到处理器的线性地址空间中,并且随后被直接地或者通过分页机制映射到处理器的物理地址空间中。

  TSS 中的 LDT 字段可以用于给出每个任务自己的 LDT。对于一个给定的任务,通过把与任务相关的所有段描述符放入 LDT 中,任务的地址空间就可以与其他任务的隔绝开来。

  当然,几个任务也可以使用同一个 LDT。这是一种简单而有效的允许某些任务互相通信或控制的方法,而无须抛弃整个系统的保护屏障。

  因为所有任务都可以访问 GDT,所以也同样可以创建通过此表访问的共享段。

  如果开启了分页机制,则 TSS 中的 CR3 寄存器字段可可以让每个任务有它自己的页表。或者,几个任务能够共享相同页表集。

6.1 把任务映射到线性和物理地址空间

有两种方法可以把任务映射到线性地址空间和物理地址空间:

所有任务共享一个线性到物理地址空间的映射。当没有开启分页机制时,就只能使用这个办法。不开启分页时,所有线性地址映射到相同的物理地址上。当开启了分页机制,那么通过让所有任务使用一个页目录,我们就可以使用这种从线性到物理地址空间的映射形式。如果支持需求页虚拟存储技术,则线性地址空间可以超过现有物理地址空间的大小。

每个任务有自己的线性地址空间,并映射到物理地址空间。通过让每个任务使用不同的页目录,我们就可以使用这种映射形式。因为每次任务切换都会加载 PDBR(控制寄存器 CR3),所以每个任务可以有不同的页目录。

  不同任务的线性地址空间可以映射到完全不同的物理地址上。如果不同页目录的条目(表项)指向不同的页表,而且页表也指向物理地址中不同的页面上,那么各个任务就不会共享任何物理地址。

  对于映射任务线性地址空间的这两种方法,所有任务的 TSS 都必须存放在共享的物理地址空间区域中,并且所有任务都能访问这个区域。为了让处理器执行任务切换而读取或更新 TSS 时,TSS 地址的映射不会改变,就需要使用这种映射方式。GDT 所映射的线性地址空间也应该映射到共享的物理地址空间中。否则就丧失了 GDT 的作用。

6.2 任务逻辑地址空间

   为了在任务之间共享数据,可使用下列方法之一来为数据段建立共享的逻辑到物理地址空间的映射:

  通过使用 GDT 中的段描述符。所有任务必须能够访问 GDT 中的段描述符。如果 GDT 中的某些段描述符指向线性地址空间中的一些段,并且这些段被映射到所有任务共享的物理地址空间中,那么所有任务都可以共享这些段中的代码和数据。

  通过共享的 LDT。两个或多个任务可以使用相同的 LDT,如果它们 TSS 中 LDT 字段指向同一个 LDT。如果一个共享的 LDT 中某些段描述符指向映射到物理地址空间公共区域的段,那么共享 LDT 的所有任务可以共享这些段中的所有代码和数据。这种共享方式要比通过 GDT 来共享好,因为这样做可以把共享局限于指定的一些任务中。系统中有与此不同 LDT 的其他任务没有访问这些共享段的权利。

  通过映射到线性地址空间公共地址区域的不同 LDT 中的段描述符。如果线性地址空间中的这个公共区域对每个任务都映射到物理地址空间的相同区域,那么这些段描述符就允许任务共享这些段。这样的段描述符通常称为别名段。这个共享方式要比上面给出的方式来得更好,因为 LDT 中的其他段描述符可以指向独立的未共享线性地址区域。

相关实践学习
每个IT人都想学的“Web应用上云经典架构”实战
本实验从Web应用上云这个最基本的、最普遍的需求出发,帮助IT从业者们通过“阿里云Web应用上云解决方案”,了解一个企业级Web应用上云的常见架构,了解如何构建一个高可用、可扩展的企业级应用架构。
高可用应用架构
欢迎来到“高可用应用架构”课程,本课程是“弹性计算Clouder系列认证“中的阶段四课程。本课程重点向您阐述了云服务器ECS的高可用部署方案,包含了弹性公网IP和负载均衡的概念及操作,通过本课程的学习您将了解在平时工作中,如何利用负载均衡和多台云服务器组建高可用应用架构,并通过弹性公网IP的方式对外提供稳定的互联网接入,使得您的网站更加稳定的同时可以接受更多人访问,掌握在阿里云上构建企业级大流量网站场景的方法。 学习完本课程后,您将能够: 理解高可用架构的含义并掌握基本实现方法 理解弹性公网IP的概念、功能以及应用场景 理解负载均衡的概念、功能以及应用场景 掌握网站高并发时如何处理的基本思路 完成多台Web服务器的负载均衡,从而实现高可用、高并发流量架构
目录
相关文章
|
Ubuntu 关系型数据库 MySQL
M1 macos docker获取x86 x64 amd 等指定架构版本linux ubuntu mysql 容器并启动容器
M1 macos docker获取x86 x64 amd 等指定架构版本linux ubuntu mysql 容器并启动容器
|
4月前
|
Ubuntu Linux
查看Linux系统架构的命令,查看linux系统是哪种架构:AMD、ARM、x86、x86_64、pcc 或 查看Ubuntu的版本号
查看Linux系统架构的命令,查看linux系统是哪种架构:AMD、ARM、x86、x86_64、pcc 或 查看Ubuntu的版本号
1000 3
|
4月前
|
存储 Linux 编译器
Linux0.11 80X86知识(七)(中)
Linux0.11 80X86知识(七)
42 0
|
4月前
|
存储 关系型数据库 Linux
Linux0.11 80X86知识(七)(上)
Linux0.11 80X86知识(七)
59 0
|
7月前
|
消息中间件 测试技术 Linux
linux实时操作系统xenomai x86平台基准测试(benchmark)
本文是关于Xenomai实时操作系统的基准测试,旨在评估其在低端x86平台上的性能。测试模仿了VxWorks的方法,关注CPU结构、指令集等因素对系统服务耗时的影响。测试项目包括信号量、互斥量、消息队列、任务切换等,通过比较操作前后的时戳来测量耗时,并排除中断和上下文切换的干扰。测试结果显示了各项操作的最小、平均和最大耗时,为程序优化提供参考。注意,所有数据基于特定硬件环境,测试用例使用Alchemy API编写。
1004 0
linux实时操作系统xenomai x86平台基准测试(benchmark)
|
7月前
|
存储 负载均衡 网络协议
X86 linux异常处理与Ipipe接管中断/异常
本文讲述了X86平台上Xenomai的ipipe如何接管中断处理。首先回顾了X86中断处理机制,包括IDT(中断描述符表)的工作原理和中断处理流程。接着详细介绍了Linux中中断门的初始化,包括门描述符的结构、中断门的定义和填充,以及IDT的加载。在异常处理部分,文章讲解了早期异常处理和start_kernel阶段的异常向量初始化。最后,讨论了APIC和SMP中断在IDT中的填充,以及剩余中断的统一处理。文章指出,ipipe通过在中断入口处插入`__ipipe_handle_irq()`函数,实现了对中断的拦截和优先处理,确保了实时性。
155 0
X86 linux异常处理与Ipipe接管中断/异常
|
网络协议 Linux 网络安全
Linux 利用 qemu-system-aarch64 实现 x86 机器安装 arm64 的操作系统 2
Linux 利用 qemu-system-aarch64 实现 x86 机器安装 arm64 的操作系统
532 0
|
存储 编解码 Linux
Linux 利用 qemu-system-aarch64 实现 x86 机器安装 arm64 的操作系统 1
Linux 利用 qemu-system-aarch64 实现 x86 机器安装 arm64 的操作系统
971 0
|
NoSQL 网络协议 安全
Linux系统:第十二章:AWS服务器X86架构安装配置Mysql与MongoDB
Linux系统:第十二章:AWS服务器X86架构安装配置Mysql与MongoDB
249 0