简介
在上篇中,我们详细讲述了保护模式下对于中断的基本原理已及对可编程中断控制器8259A的编程方法。如果说上一篇更偏重有原理及特定的硬件编程方法,那么本篇就会偏软一点,将详细描述怎样编写操作系统中的中断处理程序,并将通过pyos进行验证。在此篇中,你将会详细了解到操作系统是怎样处理中断的,中断处理程序是怎样编写的,操作系统又是怎样调用中断处理程序的。希望本篇可以使你对上述问题有个比较清晰的认识。
本篇是独立的,当然,如果你阅读了上篇,那么对于理解本篇中所描述的内容无疑是有巨大帮助的。
pyos是一个实验性的架构系统,阅读本篇之后,你可以尝试着改动pyos中的中断处理部份,这样你将更可以详细而深入的理解多重中断,现场保护等内容,本篇在最后也将对于怎样进行这样的自我实验做些许描述。如果你在学习“操作系统”或“组成原理”的过程中,对于书中描述的内容感到不太直观,你可以试试用pyos去验证你所学习的知识。
再次声明:此文只是我在进行操作系统实验过程中的一点心得体会,记下来,避免自己忘记。对于其中可能出现的错误,欢迎你来信指证。
一、操作系统中断服务概述
现代计算机如果从纯硬件角度,我个人更倾向于将它理解为是利用的一种所谓的“中断驱动”机制,就相当于我们常常津津乐道的Windows的“消息驱动”机制一样。CPU在正常情况下按顺序执行程序,一旦有外部中断到来,CPU将会中断现行程序的运行,转到中断服务程序进行中断处理,当中断处理完成之后,CPU再回到原来执行程序被中断的地方继续执行,并等待下一个中断的来临。
CPU需要响应的中断有很多种,比如键盘中断、磁盘中断、CPU时钟中断等等,每种中断的功能都是不同的,而所需要的中断服务程序也是不同的,CPU又是怎么识别这种种不同的中断的呢?
二、中断描述符表及中断描述符
在上一篇中,我们知道了CPU是通过给这些不同的中断分配不同的中断号来识别的。一种中断就对应一个中断号,而一个中断号就对应一个中断服务程序,这样,当有中断到来的时候,CPU就会识别出这个中断的中断号,并将这个中断号作为一个索引,在一张表中查找此索引号对应的一个入口地址,而这个入口地址就是中断服务程序的入口地址,CPU取得入口地址后,就跳转到这个地址所指示的程序处运行中断服务程序。
这张存放不同中断服务程序的表在系统中常常称为“中断向量表”。在保护模式下也常常称为“中断描述符表”(IDT),这个表中的每一项就是一个中断描述符,每一个中断描述符都包含中一个中断服务程序的地址,CPU通过将中断号做为索引值取得的就是这样一个“中断描述符”,通过“中断描述符”,CPU就可以得到中断服务程序的地址了,下面,我们就来看看中断描述符的结构:
(图一)
上图就是一个“中断描述符”的结构,其中P位是存在为,置1的时候就是这个描符述可以被使用;DPL是特权级,可以指定为0~3中的一级;保留位是留给Inter将来用的,在现阶段,我们只需要简单的将其置零就可以;偏移量总共是32位,它表示一个中断服务程序在内存中的位置。由于保护模式下,内存的寻址是由段选择符与偏移量指定的,所以在中断描述符中也分别设定了段选择符与偏移量位,他们共同决定了一个中断服务程序在内存中的位置。(有关保护模式下内存的寻址方面的描述,可以参考《操作系统引导探究》一文。)
在前面我们描述了,一个中断描述符是放在一张中断描述符表中的,而中断号就是中断描述符在中断描述符表中的索引或说下标。那么系统又怎么知道中断描述符表是放在什么地方的呢?这在系统中是通过一个称之为“中断描述符表寄存器”(IDTR)实现的,这个寄存器中就存放了“中断描述符表”在内存中的地址。下面,我们也来看看这个寄存器的结构:
(图二)
下面,我们可以比较完整的来欣赏一下CPU处理中断的流程:
(图三)
三、pyos中的中断系统实验
下面,我们将以pyos做为例子,用它来实验操作系统中中断系统的实现。当然,在实际的操作系统中这也许是非常复杂的,但我们现在通过pyos也完全可以进行这样的实验。在实验正式开始之前,你许要下载本实验所用到的源代码,这可以在我们的网站“纯C论坛”(http://purec.binghua.com)“操作系统实验专区”中下载,也可以来信向作者索要。对于实验环境的搭建你也许需要看看“操作系统实验专区”中“When Do We Write Our Chinese Os (1)”对此的描述。
在实验正式开始之前,我们将详细描述一下pyos的文件组织形式。Go~~~
3.1 pyos 的文件组织形式
本实验用到的pyos目前的文件组织如下:“boot.asm”、“setup.asm”这两个文件是操作系统引导文件,他们负责把pyos的内核读入内存,然后转到内核执行。(这方面的内容可以参见《操作系统引导探究》一文。)“kernel.cpp”就是pyos的内核,pyos是用c++开发的,因此它的内核看起来非常简单,也比较有结构,下面就是“kernel.cpp”中的内容:
#include "system.h"
#include "video.h"
extern "C" void Pyos_Main()
{
/* 系统初始化 */
class_pyos_System::Init() ;
class_pyos_Video::ClearScreen() ;
class_pyos_Video::PrintMessage( "Welcome to Pyos :)" ) ;
for(;;);
}
我想,这样的程序也许不用我做什么注释了。“system.h”中定义了一个名为“class_pyos_System”的系统类,用来做系统初始化的工作,“vide.h”中定义了一个名为“class_pyos_Video”的显卡类,它封装了对VGA显卡的操作,从上面的代码中我们可以看见,系统先通过class_pyos_System类的Init()完成系统初始化,然后调用class_pyos_Video类中的ClearScreen()进行清屏,最后用PrintMessage()输出了一条欢迎信息。下面,我就一步一步的来剥开上面看上去很神秘的Init()——系统初始化函数。对于显卡类,你可以参看源代码,本篇中将不进行描述,也许以后我会用专门的一篇来详细描述它。当然,你如果看过“When Do We Write Our Chinese OS (3)”的话,对于显卡类的理解就易容反掌了。
3.2 pyos 的系统初始化
下面,我们来看看 pyos 的系统初始化函数:
#include "interrupt.h"
/* 系统初始化 */
void class_pyos_System::Init()
{
/* 初始化Gdt表 */
InitGdt() ;
/* 初始化段寄存器 */
InitSegRegister() ;
/* 初始化中断 */
class_pyos_Interrupt::Init() ;
}
是的,他就是这么简单,由于InitGdt与InitSegRegister的内容在《操作系统引导探究》中已经描述过了,这里我们就专注于我们本篇的核心内容——对于中断系统的初始化。
从上面的代码中我们可以看出,系统首先在“interrupt.h”中定义了一个名为“class_pyos_Interrup”的中断类,专门来处理系统的中断部份。然后,系统调用中断类的Init()函数,来进行初始化。呵呵,我们马上就去看看这个中断类的初始化函数到底做了些什么:
/* 初始化中断服务 */
void class_pyos_Interrupt::Init()
{
/* 初始化中断可编程控件器 8259A */
Init8259A() ;
/* 初始化中断向量表 */
InitInterruptTable() ;
/* 许可键盘中断 */
class_pyos_System::ToPort( 0x21 , 0xfd ) ;
/* 汇编指令,开中断 */
__asm__( "sti" ) ;
}
我想,对于这样自说明式的代码,解释是多余的,我们还是抓紧时间来看看它首先是怎样初始化 8259A 的吧:
/* 初始化中断控制器 8259A */
void class_pyos_Interrupt::Init8259A()
{
// 给中断寄存器编程
// 发送 ICW1 : 使用 ICW4,级联工作
class_pyos_System::ToPort( 0x20 , 0x11 ) ;
class_pyos_System::ToPort( 0xa0 , 0x11 ) ;
// 发送 ICW2,中断起始号从 0x20 开始(第一片)及 0x28开始(第二片)
class_pyos_System::ToPort( 0x21 , 0x20 ) ;
class_pyos_System::ToPort( 0xa1 , 0x28 ) ;
// 发送 ICW3
class_pyos_System::ToPort( 0x21 , 0x4 ) ;
class_pyos_System::ToPort( 0xa1 , 0x2 ) ;
// 发送 ICW4
class_pyos_System::ToPort( 0x21 , 0x1 ) ;
class_pyos_System::ToPort( 0xa1 , 0x1 ) ;
// 设置中断屏蔽位 OCW1 ,屏蔽所有中断请求
class_pyos_System::ToPort( 0x21 , 0xff ) ;
class_pyos_System::ToPort( 0xa1 , 0xff ) ;
}
从上面的代码可以看出程序是通过向 8259A 发送4个ICW对8259A进行初始化的。其中ToPort是class_pyos_System类中定义的一个成员函数,它的声明如下:
/* 写端口 */
void class_pyos_System::ToPort( unsigned short port , unsigned char data )
OK,好像又不需要我多解释了,当然,你也许会问,为什么会放送这几个值的数据,而不是其它值的数据。对于这个问题,因为在“上篇”中已经详细描述了,这里就不再浪费大家的时间了。:)
3.3 初始化 pyos 的中断向量表
从中断初始化的代码中我们可以清楚的看见,pyos在进行完8259A的初始化后,调用InitInterruptTable()对中断向量表进行了初始化,这可是本篇的核心内容,我们这就来看看这个核心函数:
/* 中断描述符结构 */
struct struct_pyos_InterruptItem{
unsigned short Offset_0_15 ; // 偏移量的0~15位
unsigned short SegSelector ; // 段选择符
unsigned char UnUsed ; // 未使用,须设为全零
unsigned char Saved_1_1_0 : 3 ; // 保留,需设为 110
unsigned char D : 1 ; // D 位
unsigned char Saved_0 : 1 ; // 保留,需设为0
unsigned char DPL : 2 ; // 特权位
unsigned char P : 1 ; // P 位
unsigned short Offset_16_31 ; // 偏移量的16~31位
} ;
/* IDTR所用结构 */
struct struct_pyos_Idtr{
unsigned short IdtLengthLimit ;
struct_pyos_InterruptItem* IdtAddr ;
} ;
static struct_pyos_InterruptItem m_Idt[ 256 ] ; // 中断描述符表项
static struct_pyos_Idtr m_Idtr ; // 中断描述符寄存器所用对象
extern "C" void pyos_asm_interrupt_handle_for_default() ; // 默认中断处理函数
/* 初始化中断向量表 */
void class_pyos_Interrupt::InitInterruptTable()
{
/* 设置中断描述符,指向一个哑中断,在需要的时候再填写 */
struct_pyos_InterruptItem tmp ;
tmp.Offset_0_15 = ( unsigned int )pyos_asm_interrupt_handle_for_default ;
tmp.Offset_16_31 = ( unsigned int )pyos_asm_interrupt_handle_for_default >> 16 ;
tmp.SegSelector = 0x8 ; // 代码段
tmp.UnUsed = 0 ;
tmp.P = 1 ;
tmp.DPL = 0 ;
tmp.Saved_1_1_0 = 6 ;
tmp.Saved_0 = 0 ;
tmp.D = 1 ;
for( int i = 0 ; i < 256 ; ++i ){
m_Idt[ i ] = tmp ;
}
m_Idtr.IdtAddr = m_Idt ;
m_Idtr.IdtLengthLimit = 256 * 8 - 1 ; // 共 256项,每项占8个字节
// 内嵌汇编,载入 ldt
__asm__( "lidt %0" : "=m"( m_Idtr ) ) ; //载入GDT表
}
程序首先说明了两个结构,一个用来描述中断描述符,一个用来描述中断描述符寄存器。大家可以对照前面的描述看看这两个结构中的成员分别对应硬件系统中的哪一位。
之后,程序建立了一个中断描述符数组m_Idt,它共有256项,这是因为CPU可以处理256个中断。还建立了一个中断描述符寄存器所用的对象。随后,程序开始为这些变量赋值。
从程序中我们可以看出,pyos现在是将每个中断描述符都设成一样的,均指向一个相同的中断处理程序:pyos_asm_interrupt_handle_for_default(),在一个实际的操作系统中,在最初初始化的时候,也常常是这样做的,这个被称之为“默认中断处理程序”的程序通常是一个什么也不干的“哑中断处理程序”或者是一个只是简单报错的处理程序。而要等到实际需要时,才实用相应的处理程序替换它。
程序在建立“中断描述符表”后,用lidt指令将中断描符述表寄存器所用的内容载入了中断描述符寄存器(IDTR)中,对于“中断描述符表”的初始化就完成了,下面我们可以来看看,pyos_asm_interrupt_handle_for_default()这个程序到底做了些什么事:
3.4 中断处理程序的编写
pyos_asm_interrupt_handle_for_default:
;保护现场
pushad
;调用相应的C++处理函数
call pyos_interrupt_handle_for_default
;恢复现场
popad
;告诉硬件中断处理完毕,即发送 EOI 消息
mov al , 0x20
out 0x20 , al
out 0xa0 , al
;返回
iret
这个程序是在一个名为“interrupt.asm”的汇编文件中,显然,它是一个汇编语言写的源程序。为什么这里又要用汇编语言编写而不直接用C++内嵌汇编编写呢,比如写成下面这样:
void pyos_asm_interrupt_handle_for_default()
{
__asm__( "pushad" ) ;
/* do something */
__asm__( "popad" ) ;
__asm__( "iret" ) ;
}
这里我们需要了解这样一个问题。中断服务程序是由CPU直接调用的,随后,它使用iret指令返回,而不想一般的c/c++函数由ret返回。c/c++的编译器在处理c/c++语言的函数的时候,会在这个函数的开头与结尾加上很多栈操作,以支持程序调用,比如上边的代码就有可能被c/c++编译器处理成如下形式:(其中绿色为编译器自行加上的代码)
pusha
pushad
/* do something */
popad
iret
popa
ret
请注意这样一个实事,当程序运行到iret时就返回了,而随后的popa就不会被执行,因此这样就破坏了堆栈,于是,我们就只能通过汇编语言编写中断处理程序。
现在再来看看pyos中用汇编语言写的中断处理程序,首先它用pushad指令把寄存器中的内容压入堆栈,这常常称之为保护现场,因为之后程序需要返回被中断的程序中继续运行,因此这些寄存器中的内容也必须在中断处理程序结束时恢复。保护现场完了之后,它调用了一个c++语言程序pyos_interrupt_handle_for_default(),然后,它弹出原先保存在堆栈中的寄存器的内容,这常常称为恢复现场,随后,它发送了EOI消息通知8259A中断处理完成(关于EOI消息,在“上篇”中有详细描述),最后通过iret返回被中断的程序处继续执行。
晕!原来pyos_asm_interrupt_handle_for_default()只是一个汇编的壳,而真正的处理函数是pyos_interrupt_handle_for_default()!怪不得它会在名字中多个“asm”呢?:),下面,我们不得不来看看真正的中断处理程序做了什么:
extern "C" void pyos_interrupt_handle_for_default()
{
/* 处理中断 */
/* 读 0x60 端口,获得键盘扫描码 */
char ch = class_pyos_System::FromPort( 0x60 ) ;
/* 显示键盘扫描码 */
class_pyos_Video::PrintMessage( ch ) ;
}
yeah!这才是真正的中断处理程序,不过它的内容很简单,就是读键盘的0x60端口,获得键盘的扫描码,然后显示这个扫描码。是不是很简单?:)
3.5 class_pyos_Interrupt::Init()的最后工作
现在让我们重新回到中断类的初始化程序Init()中吧。Init()在完成了8259A及中断描述符表的初始化工作之后,它的工作就也近尾声了。随后,它调用了下面一个程序:
/* 许可键盘中断 */
class_pyos_System::ToPort( 0x21 , 0xfd ) ;
这是向8259A发送中断屏蔽字,十六进制fd所对应的二进制为1111 1101,在“上篇”中我们知道了1代表屏蔽,而0代表不屏蔽,因此fd就表示屏蔽了IRQ0、IRQ2~IRQ7,而唯IRQ1没有屏蔽。通过“上篇”我们知道IRQ1是代表的键盘中断,因此这一语句的意思就是屏蔽掉除键盘中断之外的所有中断,也即:只响应键盘中断。最后,程序用sti汇编指令打开了CPU的中断请求功能。
3.6 实验结果
想想我们前面所描述的代码,你现在应当知道了本实验最后所应达到的一个实验结果:pyos起动后,会响应键盘中断,而中断服务程序的功能是输出键盘的扫描码,也就是说如果你敲击键盘的话,你可以看见计算机的屏蔽上输出键盘的扫描码。下面我们就能看看我们的实验是否达到了程序所预期的效果。下面,我们在Virtual PC中启动我们自己的操作系统——pyos:)
我们可以看见,现在pyos已经启动了,现在让我们随便敲击一下键盘:
呵呵,屏幕上出现了方才敲键的扫描码。看来我们程序的目的的确是达到了,不过怎么有两个字符呢?再敲一次试试:)
呵呵,又出来两个字符,看来的确是按一个键发生了两次键盘中断,为什么会这样呢?这个问题留到下一篇再解决吧。:)
3.7 实验的改进
pyos是一个实验系统,开发他的目的就是为了做实验,检验所学,因此,你完全可以用它来进行实验。比如,你可以少屏蔽两个中断,为每个中断指定不同的中断服务程序,以观察多重中断是怎样工作的,中断屏蔽又是怎样起作用的。你还可以用“上篇”所介绍的方法,改变不同中断的优先级,看看中断优先级又是怎样工作的。本实验中的“中断”二字还体现的不明显,但你马上就可以改改,比如在主程序中,不要让它空循环,把原来:
for( ;; ) ;
改成:
for( ;; ){
class_pyos_Video( ‘0’ ) ;
}
然后,你再敲击键盘,你就会体会到“中断”二字的含义。可以完成的实验很多,只要你愿意去做,也非常欢迎你能来信与笔者交流。:)
原文:http://purec.binghua.com/Article/ShowArticle.asp?ArticleID=92