兰陵月 发表于 2018-1-14 10:37:19

X86汇编语言-从实模式到保护模式—笔记(51)-第17章 中断和异常处理与抢占式多任(3)

【17.3.2 创建中断描述符表】
现在转到代码清单17-2,这是内核程序的代码。
当内核开始执行时,执行点位于第872行。此时,指令指针寄存器EIP的内容必然大于0x80040000,因为内核程序已经被映射到虚拟地址空间的高端。
接下来的工作是准备保护模式下的中断系统。保护模式下的中断机制不同于实模式,因此,在进入保护模式之前,我们已经用CLI指令关掉了外部硬件中断,以免出现错误。而且,只有在创建了中断描述符表,并安装了中断处理程序之后,才能使用STI指令开放硬件中断,并享受中断的好处。
如图17-7所示,我们中断描述符表(IDT)定义在物理内存中从地址0x0001F000开始的地方,这里紧挨着内核的页目录表,是一段没有用到的空间。要知道,目前是在分页模式下,低端1MB内存已经被映射到高端,因此,中断描述符表的线性起始地址实际上是0x8001F000。改地址将多次在程序中引用,为了方便修改,在代码清单的第9行,已经将它声明为一个常数idt_linear_address,以后可以直接将它作为数值使用。
常数定义仅仅在程序编译期间有用,在编译之后不占用任何地址空间。
表的线性地址已经确定,现在的工作实在其中安装门描述符。在这里,为每一个中断向量都定义独立的处理程序不太现实,最好是将它们归归类,比如将硬件中断归为一类,再将异常归为另一类,如此一来,只需要定义两个通用的中断处理程序即可。如果有某个中断或异常需要特殊处理,可以根据需要随时安装单独的程序。
异常的通用处理程序是在标号general_exception_handler处定义的,位于第422行,它只做两件事,先显示错误信息,然后停机。在屏幕上显示信息依然要使用过程put_string,在平坦模型下,调用该过程不需要使用远转移指令。但是,该过程还要被包装成调用门,以方便在用户任务内调用。通过调用门的控制转移属于远过程调用,因此,请看第59行,put_string过程是用retf返回的。
这就是说,尽管过程put_string是内核的家人,但还必须用远过程调用的方式使用:
mov ebx,excep_msg
call flat_4gb_code_seg_sel:put_string
我们只为内核定义了两个段:4GB的代码段和4GB的数据段,为了方便引用,在代码清单17-2的第7行和第8行,分别定义了两个常数flat_4gb_code_seg_sel和flat_4gb_data_seg_sel,前者是4GB代码段的选择子,后者是4GB数据段的选择子。
对异常的处理很复杂,要分具体情况。有的异常发生后,只要纠正了错误,还可以再次执行产生异常的指令,比如页故障;有的异常发生后,当前任务不可能再恢复执行;有的异常有错误代码压栈,而有的则没有。前两种情况还好办,如果你愿意,还能用iretd指令返回;但是对于有和没有错误代码的情况,就不好办了。没有还好,可以直接用iretd返回;如果有,则必须先弹出错误代码。在一个通用的异常处理程序中,无法判断有没有错误代码压栈,因此,唯一的异常处理办法就是停机。
第877-880行,调用make_gate_descriptor过程创建中断门描述符。该过程不但可以用于创建调用门描述符,还可以用来创建中断门、陷阱门和任务门的描述符。在此处,描述符的目标代码段选择子是当前内核4GB代码段选择子;段内偏移量是通用异常处理过程的线性地址,肯定大于0x8004000;描述符的属性值为0x8E00,而指示这是一个32位的中断门描述符,门的特权级别为零。
一般来说,内核不会允许3特权级的用户任务使用make_gate_descriptor过程。因此,它可以定义成用ret指令返回的近过程,而不是现在的远过程。在内核中以近过程调用的方式使用它更方便,但我们也不想对它做任何改动,毕竟它一直是用retf指令返回的。因此,在这里对它的调用还是远调用。
第882-889行,在IDT中安装前20个描述符,它们都指向通用异常处理程序。EBX寄存器指向IDT的线性基地址;ESI寄存器是IDT内的索引,或者说是中断向量号。每个描述符占8字节,因此,每个描述符的线性地址是EBX+ESI×8。
第892-903行,在IDT内安装通用中断处理程序,中断向量20-255,对应着Intel保留的中断向量,以及外部硬件中断。通用的中断处理过程general_interrupt_handler是在第410行定义的。第411-419行,先是向8259A芯片发送中断结束命令EOI(End Of Interrupt),然后执行iret指令从中断返回。很明显,通用的中断处理过程什么也不做。但是,如果没有这个什么也不做的过程,当中断发生时,就会出问题。
根据实际需要,中断或异常应当单独处理。第906-913行,在IDT中安装中断的处理过程。这段代码很好理解,首先用make_gate_descriptor过程创建一个指向0x70号中断处理过程的中断门描述符,然后,将它写入相应的IDT表项内。该表项的线性地址等于IDT的线性起始地址,加上0x70乘以8。
【17.3.3 用定时中断实施任务切换】
刚才安装的那个0x70号中断处理程序,主要目的是进行任务切换。我们知道,计算机主板上有实时时钟芯片RTC,可以定时产生更新周期结束中断信号。可以设置RTC芯片,使得它每次更新CMOS中的时间信息后,便发出这个中断信号。在本书的前半部分,刚开始引入中断的概念时,我们用过这个中断。
RTC芯片的中断线和8259A从片的第1个引脚相连,一般情况下,该引脚对应的中断向量为0x70。因此,它的处理过程就叫rtm_0x70_interrupt_handle,位于代码清单17-2的第429行。
由于是硬件中断,因此,第433-435行,先要向8259A芯片发送中断结束命令EOI,否则它不会再向处理器发送另一个中断“通知”。
说实在的,用实时时钟的更新周期结束中断来实施任务切换并不是一个好主意。和别的中断相比,它更啰嗦,因为必须读一下CMOS芯片内的寄存器C,使它复位一下,才能使RTC产生下一个中断信号。否则,它只产生一次中断信号。因此,第437-439行就用来做这个工作。如果对此不熟悉,建议回到本书的前面复习一下。
在多任务系统中,同时有很多任务等待调度。为了记住都有哪些任务,我们使用了任务控制块(TCB),并把它们穿在一起,形成TCB链,链上的每一个TCB称为节点。在上一章里,图16-25给出了TCB的基本结构。在这一章里,我们继续使用这个版本的TCB。
学过数据结构的人都知道,链表用的很广泛,而它也拥有一套完整的算法,用来添加节点、插入节点、删除节点和遍历整个链表。很荣幸地,我们现在终于有机会用汇编语言实现这些算法。
在我们这个链表中,有一个链表头,指向第一个TCB的线性基地址。然后,在每个TCB内偏移量为0x00处,是下一个TCB的线性地址。当此处为0时,说明这是链表上最后一个TCB。第517行,声明了标号tcb_chain,并初始化了一个双字,这就是链表头。如图17-9所示,链表头有自己的线性地址,比如0x8005320。它是一个双字,内容是0x80006000,这就是链上第一个TCB(TCB1)的线性地址。

在线性地址0x80006000处,是TCB1,其内部偏移量为0x04的地方,是当前任务的状态,这是一个字,若其值为0x0000,表示这是一个空闲任务,或者一个挂起的任务;若其值为0xFFFF,则表明这是当前正在运行的任务(当前任务,或者忙任务)。在任何时候,链表中只允许一个为忙的任务。
TCB1内,偏移为0x00处,是下一个TCB,即TCB2的线性地址。于是,我们可以根据0x80009000这个值,定位到TCB2。很显然,这是一个正在运行中的任务,状态为忙,下一个TCB,即TCB3的线性地址是0x80001C00。再来看TCB3,它的状态为挂起或者空闲,而且内部偏移为0x00的地方是0,它就是链上最后一个TCB。
在中断内实施任务切换,可以使用jmp指令,从当前正在运行的任务切换到另一个空闲任务。中断的发生时随机的,但是,可以肯定的是,当中断发生时,必定有一个任务正在运行中。因此,中断总是在某个任务内发生的。
如图17-10所示,当中断发生时,任务可能正在局部空间执行,也可能正在全局空间内执行,即在内核中执行,毕竟内核被映射到每个任务地址空间的高2GB。无论是在任务的局部空间执行,还是在全局空间执行,当中断发生时,因为中断处理过程位于内核中,因此,控制都会转移到任务的全局空间,去执行当前的中断处理过程。

所有任务都共用同一个全局空间,因此,中断处理过程rtm_0x70
_interrupt_handle也只有一个份。尽管如此,当某个任务成为正在执行的当前任务时,它便拥有了该中断处理过程。每个任务在执行该过程时都有自己独立的机器状态和寄存器状态,并使用自己私有的0特权级栈段。所以,这里面不存在任何冲突和混乱的情况。
在图17-10中,我们是假定中断发生在任务的局部空间。也就是说,任务正在自己的局部空间内执行。此时,将转到全局空间内执行内核的中断处理过程。
中断处理过程的主要功能是确定下一个应该被执行的任务,并切换到那个任务。整个过程如下:
1、遍历TCB链,找到当前任务,也就是寻找那个状态值为0xFFFF的节点。如果找不到,或者链表为空,则直接转到步骤6;
2、如果找到了,则将此节点移到链表的末端,使其成为成为最后一个节点。因为该任务刚刚执行完,所以,将它移到链尾,可以使其被调度的优先级别最低;
3、再次遍历TCB链,寻找链上第一个状态为空闲的任务,也就是寻找状态值为0x0000的节点。如果找不到,则直接转到步骤6;
4、如果找到了,将当前任务的状态置为0x0000,将找到的空闲任务状态置为0xFFFF。
5、使用jmp指令从当前任务切换到空闲任务。
6、执行iretd指令,中断返回。
一旦找到了当前为忙的任务,以及那个空闲任务,则按图中所示,使用jmp发起任务切换,切换到空闲任务。因为用的jmp指令,故当前任务的TSS描述符的B位变成“0”,而新任务TSS描述符的B位变成“1”,当前任务和新任务之间是非嵌套的。
另外,非常明显的是,当中断发生,控制转移到其他任务的时候,当前(旧)任务的状态时停留在中断处理过程中的,该任务的TSS可以保存这一状态。当下一次从其他任务切换到这个任务后,将继续执行未完成的中断处理过程,并在过程的最后执行iretd指令,于是返回到当初发生中断的地方继续执行。在图17-10中,是返回到任务的局部空间执行。
注意,其他任务的执行情况也和图17-10中的这个任务相同。
一旦明白了我们要做什么,以及如何做,现在,来看看这个过程具体是怎么实现的。首先是在链表中找到当前任务,也就是那个状态为忙(0xFFFF)的节点(Node),这是代码清单17-2第442-450行的功能。
第442行,先把链表头tcb_chain的线性地址传送到EAX寄存器;第44行,因为链表头的内容是第一个TCB的线性地址,因此,EBX寄存器的内容就是第一个TCB的线性地址,如图17-11(a)所示。

遍历算法和实现过程略。
页: [1]
查看完整版本: X86汇编语言-从实模式到保护模式—笔记(51)-第17章 中断和异常处理与抢占式多任(3)