首页 > 其他分享 >一个操作系统的设计与实现——第7章 中断

一个操作系统的设计与实现——第7章 中断

时间:2023-11-12 09:56:23浏览次数:23  
标签:操作系统 中断 IDT CPU 指令 设计 8259A 处理函数

7.1 什么是中断

中断是一种能够随时打断CPU正常工作的机制。这句话看着挺别扭的,CPU工作的好好的,为什么要"随时打断"它?这是因为,CPU需要为诸多外部设备提供服务,以键盘为例,当键盘上的键被按下时,CPU需要对此做出响应和处理,如果不能及时响应,我们会说:"电脑很卡";如果一直都不能响应,我们会说:"电脑死机了"。由此可见中断的重要性。同时,"键盘被按下"完全是一种人为的操作,是不能用一段代码进行预测的,所以中断必须具备"随时打断CPU正常工作"的能力。正是由于这个能力,中断在CPU内部也有应用,例如:当CPU遇到除0错误,页不存在等问题时,都是通过发起一个中断进行处理的。

7.2 中断的具体细节

7.2.1 中断门与中断描述符表

本章开头的这段介绍把中断说的玄乎其神的,好像非常复杂的样子。事实上,中断就是一些函数,中断发生时,就相当于突然调用了一个函数。

既然中断就是一些函数,那么这些函数该如何找到呢?CPU要求:各个中断处理函数的信息需要安装在中断描述符表(Interrupt Descriptor Table,IDT)中。IDT与GDT类似,GDT中存放的是段描述符,而IDT中存放的是中断门描述符(Interrupt Gate Descriptor),简称中断门,它是系统段的一种,结构如下:

从中断门的结构可以看出,其确实用于描述一个函数,CS和EIP都可以从中找到。

7.2.2 lidt指令

与GDT类似,IDT需要通过lidt [...]指令进行加载,其被存放在中断描述符表寄存器(Interrupt Descriptor Table Register,IDTR)中,这是CPU内部的一个专用寄存器;lidt [...]指令以一段48位的内存作为参数,其低16位是IDT的长度,在数值上等于IDT的大小减1;高32位是IDT的起始地址。

lidt指令也有一个功能与之相反的sidt指令,其用于将IDTR存放到指定的内存中。我们的操作系统中不使用该指令。

7.2.3 int指令

中断处理函数的调用不使用普通的call指令,而是使用int指令。该指令以一个整数作为参数,其被称为中断号。中断号就是IDT的索引值,所以,当中断发生时,CPU会使用中断号从IDT中找到一个中断门,然后调用中断门中的函数。

中断处理函数被调用时的压栈比call指令复杂一些,这里只讨论0特权级下的压栈。当中断发生时,CPU会依次进行以下操作:

  1. 将EFLAGS压栈,然后将EFLAGS的IF位清零(见下文)
  2. 将CS通过高位补0的方式扩充至32位,然后压栈
  3. 将EIP压栈
  4. 使用中断号从IDT中找到一个中断门,将CS和EIP修改为中断门中的CS和EIP

中断号只能是一个8位整数,也就是说,中断号的取值范围是0~255。

7.2.4 iret指令

iret指令是配合int指令使用的中断返回指令,其行为与int指令完全相反。同样的,这里只讨论0特权级下的过程。当iret指令执行时,CPU会依次执行以下操作:

  1. 从栈中弹出EIP
  2. 从栈中弹出CS,并丢弃高16位
  3. 从栈中弹出EFLAGS

至此,CS、EIP和EFLAGS就恢复到了中断发生前的状态。

7.2.5 CPU内部预设的中断

前文提到,CPU自己也会发起中断,这些中断的中断号是预设好的。比较麻烦的是,这些中断的一小部分还会在int指令压栈完毕后,再额外压栈一个4字节的错误码,且iret指令不负责处理错误码。CPU内部预设的中断如下表所示:

中断号 是否有错误码 含义
0x0 除0错误
0x1 调试
0x2 不可屏蔽(NMI)中断,往往意味着硬件级的错误
0x3 断点
0x4 溢出
0x5 边界溢出
0x6 无效的指令
0x7 FPU不存在
0x8 双重错误
0x9 保留
0xa 无效的TSS
0xb 段不存在
0xc 栈段错误
0xd 一般保护性异常
0xe 页不存在
0xf 保留
0x10 浮点错误
0x11 对齐检查
0x12 机器检查
0x13 SIMD浮点异常
0x14~0x1f / 保留

0x20开始的中断号都可供操作系统和用户使用,且不存在错误码这个概念。

7.2.6 外中断

中断不仅能通过int指令发起,如键盘等外部设备也能发起中断。通过int指令发起的中断被称为内中断;外部设备发起的中断被称为外中断;CPU自己发起的中断也是内中断。

EFLAGS的第9位被称为中断允许位(Interrupt Enable Flag,IF)。当这一位为0时,任何外中断都会被屏蔽;当这一位为1时,则不会屏蔽任何外中断。内中断不受此位影响。

IF位可由sticli,以及其他能够影响EFLAGS的指令控制。sti指令将IF位置1,不屏蔽外中断;cli指令将IF位置0,屏蔽外中断。

当中断发生时,CPU会先将EFLAGS压栈,然后将IF位置零,所以,在中断处理函数调用期间,不会再次发生外中断。这是一个非常重要的性质,虽然在本章中暂时用不到。另一方面,当执行iret指令时,由于EFLAGS已经先行压栈,故IF位会恢复到中断发生之前的状态。

7.2.7 可编程中断控制器

计算机中的很多外部设备都可以发起中断,但CPU一次只能处理一个中断,这就带来了一个严重问题:如果多个外部设备几乎同时发起中断,CPU就响应不过来了。于是,在外部设备与CPU之间存在一个中间层,其被称为可编程中断控制器(Programmable Interrupt Controller,PIC)。在我们的操作系统中,使用的是名为8259A的PIC。

一片8259A芯片可以接8个外部设备,并使用一根信号线输出中断信号。在内部,8259A会对外部设备发来的中断信号进行暂存,并依次向CPU发送。此外,各接口对应的中断号也由8259A设定。不过,8个接口实在是有点少,故实际当中使用两片级联的8259A,其中一片被称为主片,其信号线接入CPU;另一片被称为从片,其信号线固定接在主片的第三个接口上。这样一来,两片级联的8259A就能够提供15个接口供外部设备使用(主片已经被占用了一个接口)。这15个接口所接入的外部设备是固定的,如时钟,键盘,硬盘,鼠标等。在我们的操作系统中,只使用这些接口的前两个,其接入的外部设备分别是时钟和键盘。

既然是可编程中断控制器,那就需要对其编程才能使用。这涉及到4个端口:0x200x21端口属于主片,0xa00xa1端口属于从片,这些端口需要连续多次写入不同信息,才能完成设定。具体操作步骤如下:

  1. 0x20端口写入0x11。这是一个固定用法
  2. 0x21端口写入主片的起始中断号。起始中断号必须是8的整数倍,8个接口的中断号从此数字开始顺延。在我们的操作系统中,主片的起始中断号应设为0x20,这是第一个可用的中断号
  3. 0x21端口写入0x4。这个数字是一个8位的位掩码,用于描述从片接的是主片的哪个接口,接入的这个接口置1,其余接口置0。由于从片固定接入主片的第三个接口,所以位掩码是0x4
  4. 0x21端口写入0x1。这是一个固定用法
  5. 0xa0端口写入0x11。这是一个固定用法
  6. 0xa1端口写入从片的起始中断号。同样的,起始中断号必须是8的整数倍,8个接口的中断号从此数字开始顺延。在我们的操作系统中,从片的起始中断号应设为0x28,从主片的中断号顺延
  7. 0xa1端口写入0x2。与主片不同的是,从片写入的这个数字,表示主片被用于接从片的接口的索引值。由于主片的第三个接口,即索引值为2的接口被用于接从片,所以是0x2
  8. 0xa1端口写入0x1。这是一个固定用法
  9. 0x21端口写入0xfe。这个数字是一个8位的位掩码,用于设定8259A对主片的8个接口的中断屏蔽。想要屏蔽一个接口,就需要将其对应的位置1,反之亦然。因此,目前CPU只能接收到时钟中断
  10. 0xa1端口写入0xff。这个数字是一个8位的位掩码,用于设定8259A对从片的8个接口的中断屏蔽。因此,整个从片都被屏蔽

至此,8259A就初始化完毕,可以使用了。

7.2.8 中断响应

当多个外中断发生时,8259A会将这些中断信号暂存,并依次向CPU发送。那么,8259A怎么知道CPU是否准备好接收下一个中断了呢?这就需要在中断发生后,由中断处理函数向8259A发送中断响应信号。具体来说,需要进行以下操作:

  1. 0x20端口写入0x20
  2. 0xa0端口写入0x20

这样一来,8259A就知道CPU已经准备好响应下一个中断了,否则,8259A将不会继续发送中断信号。

7.3 启用时钟中断

时钟看上去是一个没什么用的外部设备,但实际上意义重大。时钟可以以恒定的频率发起中断,利用这一特点,CPU可以完成任务切换,计时等重要功能。在本章中,仅使用时钟中断打印一个字符,以观察效果。

中断部分的代码,往往涉及大量的端口操作,如PIC的初始化;或是对栈十分敏感,如中断处理函数。所以,本章代码采用汇编语言与C语言混合编程。

请看本章代码7/Int.s

第1行,将编译模式设定为32位。

第3~4行,声明外部链接的printInt函数和printf函数。extern关键词的作用相当于C语言的外部链接声明。

第6~7行,将intList符号与__picInit函数声明为外部链接的。与C语言不同,nasm汇编语言中的符号默认是内部链接的,只有使用global关键词声明的符号才是外部链接的。

__picInit函数用于初始化8259A。

第40~51行定义了一个宏。nasm的宏有两种:单行宏与多行宏,这里使用的是后者。多行宏的语法是:

%macro 宏名 参数个数
...
%endmacro

不同于C语言,nasm的多行宏的参数没有名称,只有一个从1开始的数字编号,第一个参数写为%1,第二个参数写为%2,以此类推。当使用宏参数构造符号名时,不需要像C语言那样前置##,第42行的int%1就使用了这一语法。

这个宏定义的是通用的中断处理函数,其依次进行以下操作:

  1. 打印字符串:Int: 中断号\n
  2. 执行hlt指令

hlt指令可将CPU挂起,此时,只有外中断才能唤醒它。然而,在中断处理函数中是接收不到外中断的,这意味着CPU将永远不会被唤醒(直至重启bochs)。因此,这些通用的中断处理函数是不应该被调用的。如果被调用,就说明代码中出现了错误。

第53~100行,将intTmpl宏展开为各中断号对应的中断处理函数。由于时钟中断不需要此函数,所以没有intTmpl 0x20这行代码。

intTimer函数是时钟中断处理函数。

第106~108行,向8259A发送中断响应信号。

第110~112行,以参数6调用printInt函数。

第116行,使用中断处理函数专用的iret指令从中断返回。

第118~119行,定义了第46行的printf函数需要的格式化字符串。

第121~169行,定义了中断处理函数表。这个表中的函数稍后将被用于构造中断门,并填入IDT中。

接下来,请看本章代码7/Int.h

这个头文件中声明了intInit函数。

接下来,请看本章代码7/Int.hpp

第6行,声明了位于7/Int.s中的中断处理函数表。

第7行,定义了IDT,但暂时还没有安装中断门。

第9行,声明了位于7/Int.s中的__picInit函数。

__makeIntGate函数用于将CS、EIP、中断门属性值拼成64位的中断门描述符。

__installIDT函数用于安装IDT。

第20~23行,遍历中断处理函数表,将这些函数构造为中断门描述符,并安装到IDT中。每个函数的段选择子都是1 << 3,中断门属性值都是0x8e00

第25行,构造IDTR。

第27行,使用lidt指令加载IDT。

intInit函数是__picInit函数与__installIDT函数的封装,其用于初始化中断。

接下来,请看本章代码7/Kernel.c

第7行,调用intInit函数,完成中断的初始化。

第9行,打开中断。

接下来,请看本章代码7/Makefile

本章代码使用了汇编语言与C语言混合编程,因此,Int.s也需要被编译成ELF文件,以参与链接。nasm的-f elf参数可将汇编代码编译成ELF文件。

第4行,将Int.s编译成ELF文件Int.o

第5行,将Kernel.oInt.o共同链接。

7.4 测试

本章代码7/Kernel.c用于测试时钟中断。可以发现,时钟中断的发生频率是非常高的。

7.5 调试

首先,IDT在bochs调试器中可使用info idt命令查看,或在GUI中查看。

打开中断后,中断处理函数的触发时机是不容易确定的。这里介绍两种调试方法。

7.5.1 手动发起中断

中断不仅能由外部设备发起,也能由int指令发起。对于本章代码来说,可以将main函数修改成这样:

int main()
{
    printInit();
    intInit();

//    __asm__ __volatile__("sti");

    while (1)
    {
        __asm__ __volatile__("int $0x20");
    }

    return 0;
}

这样一来,就能很方便的进出0x20中断了。

7.5.2 使用trace on命令

在非常罕见的情况下,一段代码在刚开始运行时表现完全正常,但执行了一段时间以后却发生了错误。bochs调试器提供的trace on命令可用于定位此类错误发生的时机。当执行该命令后,bochs调试器会将其执行的每一条指令的详细信息,以及中断发生时的详细信息,都打印在屏幕上。

奇怪的是,bochs调试器的GUI无法显示trace on打开后的输出信息,所以,想要使用这个命令,就需要先注释掉~/.bochsdbgrc的最后一行,使bochs调试器不启用GUI。然后,尽可能的定位到离错误较近的地方,再打开trace on。这是因为,在打开trace on后,bochs调试器的执行会变得非常慢。

定位到错误以后,需要取输出信息中的这个数字:

其表示的是bochs调试器自启动以来执行的总指令数。

现在,重新启动bochs调试器的GUI,然后使用sbsba命令设置断点。这两条命令都以指令数作为参数,区别在于:sb命令设定的是指令数增量,而sba命令设定的是指令数绝对量。例如:如果现在位于第10条指令处,sb 20命令将在第30条指令处设置断点,而sba 20命令将在第20条指令处设置断点。

综上,利用trace on命令与sbsba命令,就能调试那些难以定位的错误了。

标签:操作系统,中断,IDT,CPU,指令,设计,8259A,处理函数
From: https://www.cnblogs.com/yingyulou/p/17825519.html

相关文章

  • 一个操作系统的设计与实现——第6章 显卡驱动
    进入内核以后,应该做些什么呢?本章将实现一个最容易看到效果的模块:显卡驱动。6.1什么是驱动驱动这个词听起来很高大上,但实际上很简单,就是硬件的接口函数。在软件工程中,可以使用接口封装和简化设计,硬件也是一样。例如:想要读硬盘,需要很多指令设定好几个端口,然后等待硬盘就绪,最后才......
  • 一个操作系统的设计与实现——第12章 任务(三):3特权级任务
    特权级是保护模式的核心概念之一,但我们的操作系统一直没有引入这个概念。这是因为,特权级只有在3特权级任务存在时才有意义。本章将要实现的是3特权级任务的加载与任务切换。12.1特权级12.1.1特权级的功能特权级(PrivilegeLevel),是保护模式中用于限制任务权限的机制。特权级有4......
  • 一个操作系统的设计与实现——第11章 任务(二):0特权级任务
    上一章中,我们的操作系统已经支持内核共享,这为任务的加载和运行做好了准备。本章将要实现的是0特权级任务的加载与任务切换。11.1任务切换的原理11.1.1协同式与抢占式任务切换如果CPU上只运行着Kernel.c的main函数,那么情况非常简单,只需要不断执行下一条指令即可。然而,如果现......
  • 一个操作系统的设计与实现——第10章 任务(一):共享内核
    一直以来,我们的操作系统在启动后,运行的都是Kernel.c中的main函数。只运行这一个函数是不够的,操作系统应当有能力加载并运行其他程序。从本章开始,将使用四章的篇幅讨论操作系统如何加载并运行任务。这里的任务(Task)与进程(Process)是同义词,在操作系统领域中,任务这个词更为常用,请读者......
  • 一个操作系统的设计与实现——第13章 任务(四):任务回收
    在前面的两章中,我们的操作系统均不支持任务回收,所以任务不能退出。本章将要实现的是任务回收功能。13.1任务回收的原理如果一个任务位于任务队列中,其就会被运行。所以,如果一个任务的运行已经结束,它就应该从任务队列中删除。仅仅将任务从任务队列中删除是不够的,这是因为任务还......
  • 2023-2024-1 20231405《计算机基础与程序设计》第七周学习总结
    2023-2024-120231405《计算机基础与程序设计》第七周学习总结作业信息作业属于哪个课程https://edu.cnblogs.com/campus/besti/2023-2024-1-CFAP作业要求在哪里https://edu.cnblogs.com/campus/besti/2023-2024-1-CFAP/homework/13009作业的目标自学《计算机......
  • 分布式亿级流量整体架构设计原则
    架构目标高可用性整体系统可用性最低99.9%,目标99.99%。全年故障时间整个系统不超过500分钟,单个系统故障不超过50分钟。高可扩展性系统架构简单清晰,应用系统间耦合低,容易水平扩展,业务功能增改方便快捷。低成本增加服务的重用性,提高开发效率,降低人力成本;最终......
  • 分布式亿级流量整体架构设计原则
    架构目标高可用性整体系统可用性最低99.9%,目标99.99%。全年故障时间整个系统不超过500分钟,单个系统故障不超过50分钟。高可扩展性系统架构简单清晰,应用系统间耦合低,容易水平扩展,业务功能增改方便快捷。低成本增加服务的重用性,提高开发效率,降低人力成本;最终......
  • 数据库设计心得
     在设计一个数据库管理系统,涉及到多个关键表,如用户表、数据库表、日志表、反馈表、索引表和历史查询表.与其他项目不同的是,我们没设计一个表,就要实现相应的功能,所以表的设计和需求分析联系的更加紧密.用户表:用户表是系统的基础,要包含用户的基本信息;使用适当的加密算法来......
  • 设计模式--Command模式
    命令模式(CommandPattern)是一种行为设计模式,它将一个请求封装为一个对象,从而使你可以用不同的请求对客户进行参数化,对请求排队或记录请求日志,以及支持可撤销的操作。命令模式主要包含以下几个角色:Command(抽象命令类):声明执行操作的接口。ConcreteCommand(具体命令类):是一个具体的......