货车倒车影像安装:内核代码学习==>深入介绍Linux内核(七)

来源:百度文库 编辑:偶看新闻 时间:2024/05/03 05:56:10

深入介绍Linux内核(七)



.......续上第六篇


4.7 任务管理

任务(Task)是处理器可以分配调度、执行和掛起的一个工作单元。它循
於执行程式、任务或进程、作业系统服务、中断或異常处理行程和內核代码?。

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

80X86提供了多工的硬体支援。任务是一个正在执行的程式,或者是一个等待準备执行的程式。透过中断、異常、跳转或呼叫,我们可以执行一个任务。当这些控制转移形式之一和某个描述符号表中指定项的內容一起使用时,那麼这个描述符号就是一类导致新任务开始执行的描述符号。描述符号表中与任务相关的描述符号有两类:任务状态段描述符号和任务门。当执行权传给这任何一类描述符号时,都会造成任务切換。


任务切換很像程序呼叫,但任务切換会保存更多的处理器状态资讯。任务切換会把控制权完全转移到一个新的执行环境,即新任务的执行环境。这种转移操作要求保存处理器中几乎所有寄存器的当前內容,包括标志寄存器EFLAGS和所有段积存器。与行程不过,任务不可重入。任务切換不会把任何资讯压入堆栈中,处理器的状态资讯都被保存在记忆体中称为任务状态段(Task state segment)的资料结构中。


4.7.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中)。


4.7.2 任务的执行

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

▓ 使用CALL指令明确地呼叫一个任务;

▓ 使用JMP指令明确地跳转到一个任务 (Linux內核使用的方式) ;

▓ (由处理器) 隐含地呼叫一个中断控制码处理任务;

▓ 隐含地呼叫一个異常控制码处理任务;

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

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

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

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

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

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

处理器定义了一下一些支援多工的寄存器和资料结构:

▓ 任务状态段TSS;

▓ TSS描述符号;

▓ 任务暂存器TR;

▓ 任务门描述符号;

▓ 标志寄存器EFLAGS中的NT标志。

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


任务状态段

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







⑴ 动态栏位。当任务切换而被挂起时,处理器会更新动态栏位的内容。这些栏位包括:

● 通用寄存器栏位。用于保存EAX、ECX、EDX、EBX、ESP、EBP、ESI和EDI寄存器的内容。

● 段选择符号栏位。用于保存ES、CS、SS、DS、FS和GS段寄存器的内容。

● 堆栈寄存器EFLAGS栏位。在切换之间保存EFLAGS。

● 指令指标EIP栏位。在切换之前EIP寄存器内容。

● 先前任务连接栏位。含有前一个任务TSS段选择符号(在呼叫、中断或異常激发的任务切換时更新)。该栏位(通常也称为后连接栏位(Back link field))允许任务使用IRET指令切換到前一个任务。








⑵ 靜态栏位。处理器会读取靜态栏位的內容,但通常不会改变它们。这些栏位內容是在任务被建立时设置的。这些栏位有:

● LDT段选择符号栏位。含有仟务的LDT段的选择符号。

● CR3控制暂存器栏位。含行任务使用的页目录物理基底位址。控制寄存器CR3通常也被称为页目录基底位址:寄存器PDBR(Page directory base register)。

● 特权级0、l和2的堆栈指标栏位。这些堆栈指标由堆栈段选择符号(SS0、SS1和SS2)和堆栈中偏移量指标(ESP0、ESPl和ESP2)组成。注意,对於指定的一个任务,这些栏位的值是不变的。冈此,如果任务中发生堆栈切換,寄存器SS和ESP的内容将会改变。

● 除错陷阱(Debug Trap)T标志栏位。该栏位位于位元组0x64 Bit0处。当设置了该位时,处理器切換到该任务的操作将產生一个错異常。

● I/O点阵固基底位址栏位。该栏位含有从TSS段开始处到I/O许可点阵图处的16位偏栘值。

如果使用了分页机制,那麼在任务切換期间应该避免处理器操作的TSS段中(前104位元组中)含有记忆体页边界。如果TSS这部分包含记忆体页边界,那麼该边界处两边的页面都必须同时并且连续存于记忆体中。另外,如果使用了分页机制,那麼与原任务TSS和新任务TSS相关的页面,以及对应的描述符号表表项应该是可读写的。


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描述符号载入进任何段寄存器将导致一个异常。企图使用Tl标志置位元的选择符号(即当前LDT中的选择符号)来存取TSS段也将导致異常。


任务寄存器

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

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


任务门描述符号

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

任务描述符号中的TSS选择符号栏位指向GDT中的一个TSS段描述符号。这个TSS选择符号栏位中的RPL域不用。任务描述符号中的DPL用於在任务与換时控制对TSS段的存取。当程式透过任务门呼叫或跳转到一个任务时,程式的CPL以及指向任务门的门选择符号的RPL值必须小於或等於任务门描述符号中的DPL。请注意,当使用任务门时,目标TSS段描述符号的DPL忽略不用。

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








4.7.4任务切換

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

⑴ 当前任务对GDT中的TSS描述符号执行JMP或CALL指令;

⑵ 当前任务对GDT或LDT中的任务门描述符号执行JMP或CALL指令;

⑶ 中断或異常向量指向IDT表中的任务门描述符号;

⑷ 当EFLAGS中的NT标志设置位元时当前任务执行IRET指令。


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

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






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

⑴ 从作为JMP或CALL指令运算元中,或者从任务门中,或者从当前TSS的前一任务链结栏位(对於由IRET引起的任务切換)中取得新任务的TSS段选择符号。

⑵ 检查当前任务是否允许切換到新任务。把资料存取特权级规则应用到JMP和CALL指令上。当前任务的CPL和新任务段选择符号的RPL必须小於或等於TSS段描述符号的DPL,或者引用的是一个任务门。无论目标任务门或TSS段描述符号的DPL是何值,異常、中断(除了使用INT n指令產生的中断)和IRET一指令都允许执行任务切換。对於INT n指令產生的中断将检查DPL。

⑶ 检查新任务的TSS描述符号是标注为存在的(P=1),並且TSS段长度有效(大於0x67)。当试图执行会產生错误的指令时,都会恢复对处理器状态的任何改变。这使得異常处理行程的返回位址指向出错指令,而非出错指令随后的一条指令。因此異常处理行程可以处理出错条件並且重新执行任务。異常处理行程的介入处理对应用程式来說是完全透明的。



⑷ 如果任务切換產生自JMP或1RET指令,处理器就会把当前任务(老任务)TSS描述符号中的忙标志B重定;如果任务切換是由CALL指令、異常或中断產生,则忙标志B不动。

⑸ 如果任务切換由IRET產生,则处理器会把临时保存的EFLAGS映射中的NT标志重定;如果任务切換由CALL、JMP指令或者異常或中断產生,则不用改动上述NT标志。


⑹ 把当前任务的状态保存到当前任务的TSS中。处理器会从任务寄存器中取得当前任务TSS的基底位址,並且把一下寄存器內容复制到当前TSS中:所有通用寄存器、段寄存器中的段选择符号、标志寄存器EFLAGS以及指令指标EIP。

⑺ 如果任务切換是由CALL指令、異常或中断產生,则处理器就会把从新任务中载入的EFLAGS中的NT标志置位元。如果任务切換產生自JMP或IRET指令,就不改动新载入EFLAGS中的标志。

⑻ 如果任务切換由CALL、JMP指令或者異常或中断產生,处理器就会设置新任务TSS描述符号中的忙标志B。如果任务切換由IRET產生,则不去改动B标志。

⑼ 使用新任务TSS的段选择符号和描述符号载入任务寄存器TR(包括隐藏部分)。设置CR0寄存器的TS标志。

⑽ 把新任务的TSS状态载入进处理器。这包括LDTR寄存器、PDBR(CR3)寄存器、
EFLAGS寄存器、EIP寄存器以及通用寄存器和段选择符号。在此期间检测到的任何错误都将出现在新任务的上下文中。

⑾ 开始执行新任务(对於異常处理行程,新任务的第一条指令显现出还沒有执行)。


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

当执行任务切換时,新任务的特权级与原任务的特权级沒有任何关系。新着任务在CS寄存器的CPL栏位指定的特权级上开始执行。因为各个任务透过它们独立的位址空间和TSS段相互隔絕,並且特权级规则已经控制对TSS的存取,所以在任务切換时软件不需要再进行特权级检查。

每次任务切換都会设置控制寄存器CR0中的任务切換标志TS。该标志对系统软件非常有用。系统软体可用TS标志来协调处理器和浮点辅助运算器之间的操作。TS标志指明辅助运算器中的上下文可能与当前任务的不同。


4.7.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-9总结了任务切換期间,忙标志B(在TSS段描述符号中)、NT标志、前一任务链结栏位和TS标志(在CR0中)的用法。注意,执行於任何特权级上的程式都可以修改NT标志,因此任何程式都可以设置NT标志並执行IRET指令。这种做法会让处理器去执行当前任务TSS的前一任务链结栏位指定的任务。为了避免这种偽造的任务切換执行成功,作业系统应该把每个TSS的该栏位初始化为0。






4.7.6 任务位址空间的

任务的位址空间由任务能夠存取的段构成。这些段包括代码段、资料段、堆栈段、TSS中引用的系统段以及任务代码能夠存取的任何其他段。这些段都被映射到处理器的線性位址空间中,並且随后被直接地或者透过分页机制映射到处理器的实体位址空间中。

TSS中的LDT栏位可以用於给出每个任务自己的LDT 。对於一个给定的任务,透过把与任务相关的所有段描述符号放入LDT中,任务的位址空间就可以与其他任务的隔絕开来。

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


因为所有任务都可以存取GDT,所以也同樣可以建立透过此表存取的共用段。

如果开啟了分页机制,则TSS中的CR3暂存器栏位可以让每个任务有它自己的页表。或者,几个任务能夠共用相同页表集。


把任务映射到線性和实体位址空间

有两种方法可以把任务映射到線性位址空间和实体位址空间:

▓ 所有任务共用一个線性到实体位址空间的映射。当沒有开啟分页机制时,就只能使用这个办法。不开啟分页时,所有線性位址映射到相同的实体位址上。当开啟了分页机制,那麼透过让所有任务使用一个页目錄,我们就可以使用这种从線性到实体位址空间的映射形式。如果支援需求页虛拟储存技术,则線性位址空间可以超过现有实体位址空间的大小。

▓ 每个任务有自己的線性位址空间,並映射到实体位址空间。透过让每个任务使用不同的页目錄,我们就可以使用这种映射形式。因为每次任务切換都会载入PDBR (控制暂存器CR3) ,所以每个任务可以有不同的页目錄。

不同任务的線性位址空间可以映射到完全不同的实体位址上。如果不同页目錄的条目(表项)指向不同的页表,而且页表也指向实体位址中不同的页面上,那麼各个任务就不会任何实体位址。

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


任务逻辑位址空间

为了在任务之间共用资料,可使用下列方法之一来为资料段建立共用的逻辑到实体位址空间的映射:

透过使用GDT中的段描述符号。所有任务必须能夠存取GDT中的段描述符号。如果GDT中的某些段描述符号指向線性位址空间中的一些段,並且这些段被映射到所有任务共用的实体位址空间中,那麼所有任务部可以共用这些段中的代码和资料。

透过共用的LDT。两个或多个任务可以使用相同的LDT,如果它们TSS中LDT栏位指向同一个LDT。如果一个共用的LDT中某些段描述符号指向映射到实体位址空间公共区域的段,那麼共用LDT的所有任务可以共用这些段中的所有代码和资料。这种共用方式要比透过GDT来共用好,因为这樣做可以把共用局限於指定的一些任务中。系统中有不同LDT的其他任务沒有存取这些共用段的权利。

透过映射到線性位址空间公共位址区域的不同LDT中的段描述符号。如果線性位址空间中的这个公共区域对每个任务都映射到实体位址空间的相同区域,那麼这些段描述符号就允许任务共用这些段。这樣的段描述符号通常称为別名段。这个共用方式要此上面给出的方式来得更好,因为LDT中的其他段描述符号可以指向独立的未共用線性位址区域。


4.8 保护模式程式初始化

我们知道,80X86可以工作在几种模式下。当机器上电或硬体重定时,处理器工作在8086处理器相容的真实位址模式下,並且从实体位址0xFFFFFFF0处开始执行软体初始化代码(通常在EPROM中)。软体初始化代码首先必须设置基本系统功能操作必要的资料结构资讯,例如处理中断和異常的实模式IDT表(即中断向量表)。如果处理器将仍然工作在实模式下,软体必须载入作业系统模组和相应资料以允许应用程式能在实模式下可靠地执行。如果处理器将要工作在保护模式下,那麼作业系统软体就必须载入保护模式操作必要的资料结构资讯
,然后切換到保护模式。


4.8.1 进入保护模式时的初始化操作

保护模式所需要的一些资料结构由处理器记忆体管理功能确定。处理器支援分段模型,可以使用从单个、统一的位址空间平坦模型到每个任务都具有几个受保护位址空间的高度结构化的多段模型。分页机制能夠用来部分在记忆体、部分在磁片上的大型资料结构资讯。这两种位址转換形式都需要作业系统在记忆体中为记忆体管理硬体设置所要求的资料结构。因此在处理器能夠被切換到保护模式下执行之前,作业系统载入和初始化软(bootsect.s、setup.s和head.s)必须在记忆体中先设置好保护模式下使用的资料结构的基本资讯。这些资料结构包括
以下几种:

▓ 保护模式中断描述符号表IDT;

▓ 全域描述符号表GDT;

▓ 任务状态段TSS;

▓ 区域描述符号表LDT;

▓ 若使用分页机制,则起码需要设置一个页目錄和一个页表;

▓ 处理器切換到保护模式下执行的代码段;

▓ 含有中断和異常处理程式的代码模组。

在能夠切換到保护模式之前,软体初始化代码还必须设置以下系统寄存器:

▓ 全域描述符号表基底位址寄存器GDTR;

▓ 中断描述符号表基底位址寄存器IDTR;

▓ 控制寄存器CRl- -CR3;

在初始化了这些资料结构、代码模组和系统暂存器之后,透过设置CR0寄存器的保护模式标志PE(位元0) ,处理器就可以切換到保护模式下执行。


保护模式系统结构表

软体初始化期间在记忆体中设置的保护模式系统表主要依赖於作业系统将要支援的记忆体管理类型:平坦的、平坦並支援分页的、分段的或者分段並支援分页的。

为了实现无分页的平坦记忆体模型,软体初始化代码必须起码设置具有一个代码段和一个资料段的GDT表。当然GDT表第l项还需要放置一个空描述符号。堆栈可以放置在普通可读写资料段中,因此並不需要专门的堆栈描述符号。支援分页机制的平坦记忆体模型还需要一个页目錄和至少一个页表。在可以使用GDT表之前,必须使用LGDT指令把GDT表的基底位址和长度值载入到GDTR寄存器中。

而多段模型则还需要用於作业系统的其他段,以及用於每个应用程式的段和LDT表段。LDT表的段描述符号要求存放在GDT表中。某些作业系统会为应用程式另行分配新段和新的LDT段。这种做法为动态程式设计环境提供了最大灵活性,例如Linux作业系统就使用了这种方式。像程序控制器那樣的嵌入式系统可以预先为固定数量的应用程式分配固定数量的段和LDT,这是实现即时系统软件环境结构的一种简单而有效的方法。


保护模式異常和中断初始化

软体初始化代码必须设置一个保护模式IDT,其中最少需要含有处理器可能產生的每个異常向量对应的门描述符号。如果使用了中断或陷阱门,那麼门描述符号可以都指向包含中断和異常处理行程的同一个代码段。若使用了任务门,那麼每个使用任务门的異常处理行程都需要一个TSS以及相关的代码、资料和堆栈段。如果允许硬件產生中断,那麼必须在IDT中为一个或多个中断处理行程设置门描述符号。

在可以使用IDT之前,必须使用LIDT指令把IDT表基底位址和长度载入到IDTR寄存器中。


分页机制初始化

分页机制由控制寄存器CR0中的PG标志设置。当这个标志被清除0时(即硬体重定时的状态),分页机制被关闭:当设置了PG标志,就开啟分页机制。在设置PG标志之前,必须先初始化以下资料结构和寄存器:

▓ 软体必须在实体记忆体中建立至少一个页目錄和一个页表。如果页目錄表中含有指向自身的目錄项时,可以不使用页表。此时页目錄表和页表被存放在同一页面中。

▓ 把页目錄表的物理基底位址载入到CR3寄存器中(也称为PDBR寄存器)。

▓ 处理器处於保护模式下。如果满足所有其他限制,则PG和PE标志可以同时设置。
为保持相容性,设置PG标志(以及PE标志)时必须遵守以下规则:

▓ 设置PG标志的指令应该立刻跟随一条JMP指令。MOV CR0指令后面的JMP指令会改变执行流,所以它会清空80X86处理器已经取得或已解码的指令。然而,Pentium及以上处理器使用了分支目标缓冲器BTB(Branch Target Buffer)为分支代码定向,因此減去了为分支指令更新佇列的需要。
▓ 设置PG标志到跳转指令JMP之间的代码必须来自对等映射(即跳转之前的線性位址与开启分页后的实体位址相同)的一个页面上。


多工初始化

如果将要使用多工机制,並且/或者允许改变特权级,那麼软件初始化代码必须至少设置一个TSS及相应的TSS段描述符号(因为特权级0、l和2的各堆栈段指标需要从TSS中取得) 。在建立TSS描述符号时不要将其标注为忙(不要设置忙标志) ,该标志仅由处理器在执行任务切換时设置。与LDT段描述符号相同,TSS的描述符号也存放在GDT中。

在处理器切換到保护模式之后,可以用LTR指令把TSS段描述符号的选择符号载入到任务暂存器TR中。这个指令会把TSS标记成忙状态(B=1) ,但是並不执行任务切換操作。然后处理器可以使用这个TSS来定位特权级0、1和2的堆栈。在保护模式中,软件进行第一次任务切換之前必须首先载入TSS段的选择符号,因为任务切換会把当前任务状态复制到该TSS中。

在LTR指令执行之后,随后对任务暂存器的操作由任务切換进行。与其他的段和LDT类似,TSS段和TSS段描述符号可以预先设置好,也可以在需要时才进行设置。


4.8.2模式切換

为了让处理器工作在保护模式下,必须从真实位址模式下进行模式切換操作。一旦进入保护模式,软件通常不会再需要回到真实位址模式。为了还能执行为真实位址模式编制的程式,通常在虛拟-8086模式中执行比再切換回实模式下执行更为方便。


切換到保护模式

在切換到保护模式之前,必须首先载入一些起码的系统资料结构和代码模组。一旦建立了这些系统表,软件初始化代码就可以切換到保护模式中。透过执行在CR0寄存器中设置PE标志的MOV CR0指令,我们就可以进行保护模式。(在同一个指令中,CR0的PG标志可用于开啟分页机制。)刚进入保护模式中执行时,特权级是0。为了保证程式的相容性,切換操作应该按照以下步骤进行:

⑴ 禁止中断。使用CLI指令可以禁止可遮罩硬体中断。NMI会由硬体电路来禁止。同时软体应该确保在模式切換操作期间不產生異常和中断。

⑵ 执行LGDT指令把GDT表的基底位址载入进GDTR暂存器。

⑶ 执行在控制暂存器CR0中设置PE标志(可选同时设置PG标志)的MOV CR0指令。

⑷ 在MOV CR0指令之后立刻执行一个远跳转JMP或远呼叫CALL指令。这个操作通常是远跳转到或远呼叫指令流中的下一条指令。

⑸ 若要使用区域描述符号表,则执行LLDT指令把LDT段的选择符号载入到LDTR寄存器中。
⑹ 执行LTR指令,用初始保护模式任务的段选择符号或者可写记忆体区域的段描述符号载入任务寄存器TR。这个可写记忆体区域用於在任务切換时存放任务的TSS资讯。

⑺ 在进入保护模式后,段寄存器仍然含有在真实位址模式时的內容。第4步中的JMP或CALL指令会重置CS寄存器。执行以下操作之一可以更新其余段寄存器的內容:其余段寄存器的內容可透过重新载入或切換到一个新任务来更新。

⑻ 执行LIDT指令把保护模式IDT表的基底位址和长度载入到IDTR寄存器中。

⑼ 执行STI指令开啟可遮罩硬体中断,並且执行必要的硬体操作开啟NMI中断。


另外,MOV CR0指令之后紧接著的JMP或CALL指令会改变执行流。如果开啟了分页机制,那麼MOV CR0指令到JMP或CALL指令之间的代码必须来自对等映射(即跳转之前的線性位址与开敔分页后的实体位址相同)的一个页面上。而JMP或CALL指令跳转到的目标指令並不需要处於对等映射页面上。


切換回真实位址模式

若想切換回真实位址模式,则可以使用MOV CR0指令把控制暂存器CR0中的PE标志清0。重新进入真实位址模式的行程应该按照以下步骤进行:

⑴ 禁止中断。使用CLI指令可以禁止可遮罩硬件中断。NMI会由硬件电路来禁止。同时
软体应该确保在模式切換操作期间不產生異常和中断。

⑵ 如果已开啟分页机制,那麼需要执行:

● 把程式的控制转移到对等映射的線性位址处(即線性位址等於实体位址)’

● 确保GDT和IDT在对等映射的页面上。

● 清除CR0中的PG标志。

● CR3寄存器中设置为0x00,用於更新TLB缓冲。

⑶ 把程式的控制转移到长度为64KB(0xFFFF)的可读段中。这步操作使用实模式要求的段长度载入CS寄存器。

⑷ 使用指向含有以下设置值的描述符号的选择符号来载入SS、DS、ES、FS和GS段寄存器。

● 段限长Limit:64KB

● 位元组颗粒度(G=0)。

● 向上扩展(E=0)。

● 可写(W=1)。

● 存在(P=I)。

⑸ 执行LIDT指令来指向在1 MB实模式位址范围內的真实位址模式中断表。

⑹ 清除CR0中的PE标志来切換到真实位址模式。

⑺ 执行一个远跳转指令跳转到一个实模式程式中。这步操作会更新指令佇列並且为CS寄存器载入合适的基底位址和存取许可权值。

⑻ 载入真实位址模式程式码会使用的SS、DS、ES、FS和GS寄存器。

⑼ 执行STI指令开啟可遮罩硬件中断,並且执行必要的硬件操作开启NMI中断。


4.9 一个简单的多工內核实例

作为对本章和前几章內容的总结,本节完整描述了一个简单多工內核的设计和实现方法。这个內核示例中包含两个特权级3的用戶任务和一个系统呼叫中断行程。我们首先說明这个简单內核的基本结构和载入执行的基本原理,然后描述它是如何被载入进机器RAM记忆体中以及两个任务是如何进行切換执行的。最后我们给出实现这个简单內核的来源程式:开机啟动引导程式boot.s和保护模式多工內核程式head.s。


4.9.1 多工程式结构和工作原理

本节给出的內核实例由2个档构成。一个是使用as86语言编制的开机啟动程式boot.s,用於在电脑系统加电时从啟动碟上把內核代码载入到记忆体中;另一个是使用GNU as组合语言编制的內核程式head.s,其中实现了两个执行在特权级3上的任务在时钟中断控制下相互切換执行,並且还实现了在萤幕上显示字元的一个系统呼叫。我们把这两个任务分別称为任务A和任务B(或任务0和任务1),它们会呼叫这个显示系统呼叫在萤幕上分別显示出字元‘A’和‘B’直到每个10毫秒切換到另一个任务。任务A连续回圈地呼叫系统呼叫在萤幕上显示字元‘A’;任务B则一直显示字元‘B’。若要终止这个內核实例程式,则需要重新啟动机器,或者关闭执行的模拟PC执行环境软件。

Boot.s程式编译出的代码共512位元组,将被存放在软碟映射档的第一个磁区中,见图4-39所示。 PC机在加电啟动时,ROM BIOS中的程式会把啟动盘上第一个磁区载入到实体记忆体0x7c00(31KB)位置开始处,並把执行权转移到0x7c00处开始执行boot程式码。






boot程式的主要功能是把软碟或映射档中的head內核代码载入到记忆体某个指定位置处,並在设置好临时GDT表等资讯后,把处理器设置成执行在保护模式下,然后跳转到head代码处去执行內核代码。实际上,boot.s程式会首先利用ROM BIOS中断int 0x13把软碟中的head代码读入到记忆体0x10000(64KB)位置开始处,然后再把这段head代码移动到记忆体0开始处。最后设置控制寄存器CR0中的开啟保护执行模式标志,並跳转到记忆体0处开始执行head代码。Boot程式码在记忆体中移动head代码的示意图见图4-40所示。





把head內核代码移动到实体记忆体0开始处的主要原因是为了设置GDT表时可以简单一些,因而也能让head.s程式尽量短一些。但是我们不能让boot程式把head代码从软碟或映射档中直接载入到记忆体0处。因为载入操作需要使用ROM BIOS提供的中断行程,而BIOS使用的中断向量表正处於记忆体0开始的地方,並且在记忆体l Kb开始处是BIOS程式使用的资料区,所以若直接把head代码载入到记忆体0处将使得BIOS中断行程不能正常执行。当然我们也可以把head代码载入到记忆体0xl0000处后就直接跳转到该处执行head代码,使用这种方式的来源程式可从oldlinux.org网站下载,见下面說明。

Head.s程式执行在32位元保护模式下,其中主要包括初始设置的代码、时钟中断int 0x08的行程代码、系统呼叫中断int 0x80的行程代码以及任务A和任务B等的代码和资料。其中初始设置工作主要包括:①重新设置GDT表:②设置系统计时器晶片;③重新设置IDT表並且设置时钟和系统呼叫中断门:④移动到任务A中执行。

在虛拟位址空间中head.s程式的內核代码和任务代码分配图如图4-4l所示。实际上,本內核示例中所有代码和资料段部对应到实体记忆体同一个区域上,即从实体记忆体0开始的区域。GDT中全域代码段和资料段描述符号的內容都设置为:基底位址为0x0000;段限长值为0x07ff 。因为颗粒度为1,所以实际段长度为8MB。而全域显示资料段被设置成:基底位址为0xb8000:段限长值为0x0002,所以实际段长度为8KB,对应到显示记忆体区域上。







两个任务的在LDT中代码段和资料段描述符号的內容也都设置为:基底位址为0x0000;段限长值为0x03ff,实际段长度为4MB 。因此在線性位址空间中这个“內核”的代码和资料段与任务的代码和资料段都从線性位址。开始並且由於沒有採用分页机制,所以它们都直接对应实体位址0开始处。在head程式编译出的目标档中以及最终得到的软碟映射档中,代码和资料的组织形式见图4-42所示。






由於处於特权级0的代码不能直接把控制权转移到特权级3的代码中执行,但中断返回操作是可以的,因此当初始化GDT、IDT和定时晶片结束后,我们就利用中断返回指令IRET来啟动执行第l个任务。具体实现方法是在初始堆栈init_stack中人工设置一个返回环境。即把任务。的TSS段选择符号载入到任务寄存器LTR中、LDT段选择符号载入到LDTR中以后,把任务。的用戶堆栈指标(0x17:init_stack)和代码指标(0x0f:task0)以及标志寄存器值压入堆栈。然后执行中断返回指令IRET。该指令会弹出堆栈上的堆栈指标作为任务。用戶堆栈指标,恢复假设的任务0的标志寄存器內容,並且弹出堆栈中代码指标放入CS:EIP寄存器中,从而开始执行任务0的代码,完成了从特权级0到特权级3代码的控制转移。

为了每隔10毫秒切換执行的任务,head.s程式中把计时器晶片8253的通道0设置成每经过l0毫秒就向中断控制晶片8259A发送一个时钟中断请求信号。PC机的ROM BIOS开机时已经在8259A中把时钟中断请求信号设置成中断向量8,因此我们需要在中断8的处理行程中执行任务切換操作。任务切換的实现方法是查看current变数中当前执行任务号。如果current当前是0,就利用任务l的TSS选择符号作为运算元执行远跳转指令,从而切換到任务l中执行,否则反之。

每个任务在执行时,会首先把一个字元的ASCII码放入寄存器AL中,然后呼叫系统中断呼叫int 0x80,而该系统呼叫处理行程则会呼叫一个简单的字元写到萤幕副程式,把积存器AL中的字元显示在萤幕上,同时把字元显示的萤幕的下一个位置记錄下来,作为下一次显示字元的萤幕位置。在显示过一个字元后,任务代码会使用回圈语句延迟一段时间,然后又跳转到任务代码开始处继续回圈执行,直到执行了l0毫秒而发生了定时中断,从而代码会切換到另一个任务去执行。对於任务A,寄存器AL中将始终存放字元‘A’,而任务B执行时AL中始终存放字元‘B’。因此在程式执行时我们将看到一连串的字元‘A’和一连串的字元‘A’间隔地连续不断地显示在萤幕上,见图4-43所示。






图4-43是我们在Bochs模拟软件中执行这个內核示例的萤幕显示情況。细心的读者会发现,在图中底端一行上显示出一个字元‘C’。这是由於PC机偶然產生了一个不是时钟和系统呼叫的其他中断而引起的。因为我们已经在程式给所有其他中断安装了一个预设中断处理程式,在这个程式中会显示一个字元‘C’,然后退出中断。

下面列出boot.s和head.s程式的详细注释。有关这个简单內核示例的编译和执行方法请参考我们最后一章中“编译执行简单內核示例程式”一节內容。


4.9.2 开机啟动程式boot.s

为了尽量让程式简单,这个开机啟动磁区程式仅能夠载入长度不超过16个磁区的head代码,並且直接使用了ROM BIOS预设设置的中断向量号,即定时中断请求处理的中断号仍然是8。这与Linux系统中使用的不同。Linux系统会在内核初始化时重新设置8259A中断控制晶片,并把时钟中断请求信号对应到中断0x20以上,详细说明请参考“内核开机启动程式”一章的内容。






4.9.3多工内核程式head.s

在进入保护模式后,head.s程式重新建立和设置IDT、GDT表的主要原因是为了让程式在结构上比较清晰,也为了与后面Linux 0.12内核原始码中这两个表的设置方式保持一致。当然,就本程式来说我们完全可以直接使用boot.s中设置的IDT和GDT表位置,填入适当的描述符号项即可。







192 .1ong 0,0,0,0,0 /* espl,ssl,esp2,ss2,cr3 */
193 .1ong 0,0,0,0,0 /* eip,eflags,eax,ecx,edx */
194 .1ong 0,0,0,0,0 /* ebx esp,ebp,esi,edi */
195 .1ong 0,0,0,0,0 /* es,cs,ss,ds,fs,gs */
196 .1ong LDT0_SEL,0x8000000 /* ldt,trace bitmap */
197
198 .fill 128,4,0 #这是任务0的內核堆栈空间。
199 krn_stk0 :
200
201 #下面是任务l的LDT表段內容和TSS段內容。
202 .align 3
203 ldt1: .quad 0x0000000000000000 #第l个描述符号,不用。
204 .quad 0x00c0fa00000003ff #选择符号是0x0f,基底位址 = 0x00000。
205 .quad 0x00c0f200000003ff #选择符号是0x17,基底位址 = 0x00000。
206
207 tss1: .long 0 /* back link */
208 .long krn_stk1,0x10 /* esp0,ss0 */
209 .1ong 0,0,0,0,0 /* espl,ssl,esp2,ss2,cr3 */
210 .1ong task1,0x200 /* eip,eflags */
211 .1ong 0,0,0,0,0 /* eax,ecx,edx,ebx */
212 .1ong usr_stk1,0,0,0 /* esp,ebp,esi,edi */
213 .1ong 0x17,0x0f,0x17,0x17,0x17,0x17 /* es,cs,ss,ds,fs,gs */
214 .1ong LDT1_SEL,0x8000000 /* ldt,trace bitmap */
215
216 .fill 128,4,0 #这是任务1的內核堆栈空间其用戶堆栈直接使用初始堆栈空间。
217 krn_stk1 :
218
219 #下面是任务0和任务1的程式,它们分别回圈显示字元‘A’和‘B’。
220 task0 :
221 movl $0x17,%eax #首先让DS指向任务的区域资料段。
222 movw %ax, %ds #因为任务沒有使用区域资料,所以这两句可省略。
223 movl $65, %al #把需要显示的字元‘A’放入AL寄存器中。
224 int $0x80 #执行系统呼叫,显示字元。
225 movl $0xfff,%ecx #执行回圈,起延时作用。
226 l : loop lb
227 jmp task0 #跳转到任务代码开始处继续显示字元。
228 taskl :
229 movl $66, %al #把需要显示的字元‘B’放入AL寄存器中。
230 int $0x80 #执行系统呼叫,显示字元。
231 movl $0xfff,%ecx #延时一段时间,並跳转到开始处继续回圈显示。
232 l : loop lb
233 jmp taskl
234
235 .fill 128,4,0 #这是任务l的用戶堆栈空间
236 usr_stk1 :







To be continued......