中断是什么,为什么要有中断
这里讲解一个粗闲的认知:由于CPU获知了计算机发生的某些事,CPU暂停正在执行的程序,转而去执行处理该事件的程序,当这段程序执行完毕后,CPU继续执行刚才的程序。
中断大幅度提升了整体的运行效率且对于用户而言更加方便的使用计算机,像是听歌的同时打开浏览器。
操作系统是中断驱动的
操作系统是一个死循环,没有中断的话,操作系统几乎什么都不能做。
中断分类
外部中断
外部中断的中断源必须是某个硬件,所以外部中断又被称为硬件中断。由于我们不可能为某一个外设准备一个引脚接受中断,所以我们准备了两个公共线路,所有来自外设的中断信号都共享公共线路连接到CPU。
INTR和NMI两条线路的区分来源于区分任务的轻重。
INTR引脚收到的中断都是不影响系统运行的,即可屏蔽中断,可以通过dflags寄存器的IF位将所有这些外部设备的中断屏蔽,也可以通过该中断代理单独屏蔽某个设备的中断。对于这类可屏蔽中断,可以分为上半部和下半部分开处理。上半部是需要立即执行的部分,这部分需要限时执行,是不可以被中断的。下半部则不需要立即执行,是可以被中断的。
例如:对于网卡而言,将网卡缓冲区的数据拷贝到内核缓冲区是上半部,因为缓冲区溢出会导致后续数据丢失,而处理这部分数据则是下半部。
不可屏蔽中断是通过NMI引脚进入CPU的,它表示系统中发生了致命的错误。
CPU收到中断后,通过中断向量表(实模式)或中断描述符(保护模式)来实现的。可屏蔽中断并不会导致致命错误且数量有限,所以每一种中断源都可以获得一个中断向量号,而不可屏蔽中断的原因有很多,且多数属于硬件上的错误,只能找硬件工程师解决,所以只分配一个中断向量号就行了,不可屏蔽中断的中断向量号为2。中断向量号就是一个整数,它就是中断向量表或中断描述符表中的索引下标。中断发起时,相应的中断向量号通过NMI或INTR引脚被传入CPU,然后通过索引对应的中断处理程序并去执行。
内部中断
具体看书p301~304
中断描述符表
中断描述符表的简介和结构
中断描述符表是保护模式下存储中断处理程序入口的表。中断描述符表中出了中断描述符,还有任务门描述符和陷阱门描述符,因为其作是通向某段程序的“大门”,又被称之为门。
门表示一段程序的入口,门描述符中描述的是一段代码。所有的描述符大小都是8字节,描述符中添加了各种属性,用于判断。描述符高4字节的第8~12位是表述描述符的类型,第12位是S位,用来表示系统段(S为0)或非系统端(S为1)。
这里我们把精力放在中断门上, 中断门包含了中断处理程序所在段的段选择子和段内偏移地址,通过此方式进入中断后,标志寄存器eflags中的IF位自动置0,即自动把中断关闭,避免中断嵌套。中断门只允许存在于IDT(中断描述符表)中。type值为1110。
IDT的位置不确定,所以就像是GDT一样,CPU内部有一个中断描述符表寄存器(IDTR)来记录。16位的表界限,即64kb,可容纳的描述符个数是64kb/8=8k=8192个。与GDT不同,IDT的第0个门描述符是可以使用的,加载IDTR使用专门的指令——lidt,其用法是:
lidt48位内存数据。
中断处理过程及保护
了解中断描述符表之后,咋们说说中断发生到中断处理的过程,过程中涉及到特权级检查,也就是本节所说的的保护。完整的中断过程分为CPU外和CPU内。
- CPU外:外部设备的中断有中断代理芯片接收,接受后将该中断的中断向量号发生给CPU。
- CPU内:CPU执行该中断向量号对应的中断处理程序。
在后续的中断代理芯片Inter 8259A会详细将CPU外,这部分内容属于硬件范畴,这里我们先讨论处理器内的过程,也就是CPU内。
1)处理器根据中断向量号定位中断描述符
中断向量号是中断描述符表的索引,当处理器接收到中断向量号后,它用此向量号去表中查询对应的中断描述符,然后再去执行该中断描述符中的中断处理程序。由于中断描述符是8个字节。所以处理器用中断向量号乘以8之后在与IDTR中的中断描述符表地址相加。
2)处理器进行特权级检查
由于中断是由中断向量号通知到处理器,中断向量号只是整数,其中并没有RPL,所以检查并不涉及RPL,而是对当前特权级CPL和门描述符DPL和门中目标代码段DPL之间检查。
a)若是由软中断int n、int3和into引发的中断,这些由用户进程主动发起的中断,由用户代码控制,处理器要检查当前特权级CPL和门描述符DPL,若CPL特权大于等于DPL,特权级“门槛”检查通过,即数值上CPL<=门描述符DPL,进入下一步的“门槛”检查,否则抛出异常。
b)这一步检查特权级的上限:处理器检查当前特权级CPL和门描述符中所记录的选择子对应的代码段DPL,若CPL权限小于目标代码段描述符DPL,即数值上CPL>目标代码段描述符DPL,检查通过,否则引起异常。即出了用返回指令从高特权级返回,特权转移只能发生在由低到高。
3)执行中断处理程序
特权级检查结束后,将门描述符目标代码段描述符选择子加载到代码段寄存器CS中,把门门描述符中中断处理程序的偏移地址加载到EIP,开始执行中断处理程序。
中断发生后,eflags中的NT位和TF位会被置0.如果中断对应的门描述符是中断门,IF位也会置0。处理器提供了 专门控制IF位的指令,cli使IF位为0,sti使IF位为1。注意,IF位只能限制外部设备的中断,即外部设备中的可屏蔽中断,对于不可屏蔽中断和内部中断无效。
TF位是Trap Flag,也就是陷阱标志位,TF为0时表示禁止单步执行。NT位是Nest Task Flag,即任务嵌套标志位。NT为0表示中断返回,NT为1表示新任务返回旧任务,这与iret中断返回指令相配合。当CPU执行iret时,若NT为1,说明当前任务是被嵌套执行的,因此会从自己TSS中“上一个任务TSS的指针”字段获取旧任务,然后去执行该任务;若NT为0,表示当前是在中断处理环境下,于是就执行正常的中断退出流程。
中断发生时的压栈
门描述符中只有代码段的选择子及在段内偏移地址,所以要将其加载到代码段寄存器CS及偏移量到指令指针寄存器EIP。要存储原先的CS和EIP,旧栈SS和ESP的值,EFLAGS寄存器和错误码。
iret会从当前栈顶处一次弹出32位数据分别到寄存器EIP、CS、EFLAGS。单iret不清楚栈中数据的正确性,他只负责对号入座弹出相关寄存器。对于16位段寄存器CS和SS,会将高16位丢弃(全0),再将低16位载入到CS和SS。
中断错误码
错误码本质上就是一个描述符选择子。若中断源是不可屏蔽中断NMI或外部设备,EXT为1,否则为0。IDT为1表示刺选择子指向中断描述符表,只有IDT为0,TI位才有意义。若错误码全0,表示中断的发生与特定的段无关,或引用了一个空描述符,即加载选择子时发现选择子指向的描述符是空。中断返回时,iret指令并不会把错误码从栈中弹出,所以在中断处理程序中需要手动用栈指针跨过错误码或将其弹出。通常能压入错误码的中段属于中断向量号在0~32之内的异常,而外部中断和int 软中断并不会产生错误吗。通常我们并不用处理错误码。
可编程中断控制器 8259A
本节将介绍可屏蔽中断的代理——可编程中断控制器 8259A。8259A的作用是负责所有来自外设的中断,其中就包括来自时钟的中断。
8259A介绍
中断代理8259A 控制可屏蔽中断INTR信号线的使用,用于管理和控制可屏蔽中断,它表现在屏蔽外设中断,对他们实行优先级判决,向CPU提供中断向量号等功能。
一个8259A 只可以管理8个中断,但可以通过组合来提高管理的中断个数,即级联,最多可以级联9个,即最多支持64个中断,计算公式为n片8259A芯片可支持7n+1个中断源。有主片从片区分,只有主片才会向CPU发送INT中断信号。而我们的个人电脑中告知有两篇 8259A。现代电脑的中断管理模块集成到南桥芯片组中,主板中已无8259A芯片。但这并不意味着8259A不使用,只是集合在了南桥芯片组中。
联机一个芯片,要占用主片一个IRQ接口,而从片上的IRQ接口不被占用,从片上有专门的接口用于级联,相当于从片向CPU发送INT信号的接口插在了主片上的某个IRQ。
当某个外设发出一个中断信号时,由于主板上已经将信号通路指向了8259A芯片的某个IRQ接口,所以该中断信号最终被送入了 8259A,8259A 首先检查IMR寄存器中是否已经屏蔽了来自改IRQ接口的中断信号,若IMR寄存器中的位为1,表示中断屏蔽,否则为0,表示中断放行送入IRR寄存器中等待。将该IRQ接口所在IRR寄存器中对应的BIT置1,后续会根据对应的值生成中断向量号,接着PR会从IRR寄存器中挑选一个优先级最大的中断,此处的优先级判定:IRQ接口号越低,优先级越大,所以IRQ0优先级最大。之后,8259A 会在控制电路通过INT接口向CPU发送INTR信号,CPU在执行完当前的指令后,会向 8259A的INTA接口回复一个中断响应信号,表示CPU准备好了。8259A收到信号后没会将优先级最大的中断在ISR寄存器中对应的BIT置1,同时将IRR中对应的BIT置0。
之后,CPU将再次发送INTA信号,这一次想获取中断对应的中断向量号,由于大部分情况下8259A的其实中断向量号并不是0,所以用起始中断向量号+IRQ接口号就是该设备的中断向量号,发送给CPU,CPU收到后用此作为索引中断向量表或中断描述符表中寻找对应的中断处理程序并执行。
若8259A的“EOI通知”被设置为非自动模式(手工模式),中断处理程序结束处必须向8259A发送EOI的代码,8259A在收到EOI后,将当前正处理的中断在ISR寄存器中对应的BIT置0。否则为自动模式,即8259A在收到第二个INTA信号后,也就是CPU索要中断向量号后,8259A自动将此中断在ISR中对应的BIT置0。
并不是进入ISR寄存器的中断就高枕无忧。若是8259A发送中断向量号之前,这时候来了一个新中断,此中断的优先级高,则恢复IRR,替换ISR。
所以我们要做的很简单
1)构造好IDT
2)提供中断向量号
外部设备不知道中断向量号,所以我们做的就是“自圆其说”,自己为外部设备设置好中断向量号,然后自己在中断描述符表中的对应项添加好合适的中断处理程序。
8259A 的编程
8259A 内部有两个寄存器,一组是初始化命令寄存器组,用来保存初始化命令字,ICW,ICW共4个,ICW1~ICW4。另一组是操作命令寄存器,用来保存操作命令字(OCW),共三个OCW1~OCW3。
ICW用做初始化,用来确定是否需要级联,设置起始中断向量号,设置中断结束模式。起编程就是往8259A 的端口发送一系列ICW,必须依次写入。
OCW来操作控制8259A ,像是中断屏蔽和中断结束,就是通过网 8259A 端口发送OCW实现的,顺序无要求。
接下来就是要详细降级ICW和OCW,这部分内容并不难,但是记得多,所以需要打起精神。
这里注重要明确,ICW和OCW是8259A内部的两组寄存器,即不管是主片和从片,内部都有,我们做的只是往对应芯片的寄存器写入数据,即编程。
ICW
ICW1
ICW1用来初始化8295A 的连接方式和中断信号的触发方式。连接方式是指使用单片还是级联,触发方式是电平触发还是边沿触发。
ICW1需要主片0x20端口和从片的0xA0端口写入,ICW1结构如图所示。
IC4表示是否要写入ICW4,IC4为1是表示需要在后面写入ICW4,注意x86系统IC4必须为1。
SNGL 表示single,若SNGL为1,表示单片,若为0表示级联(这表示ICW3需要写入)
ICW2
ICW2用来设置起始中断向量号。
注意,ICW2主片的0x21端口和从片的0xA1端口写入,ICW2结构如下:
前面我们所说,我们只要自己设置起始中断向量号,在ICW2中,我们需要设置T3~T7来作为我们的起始中断向量号,而低3位则作为IRQ0~IRQ7的对应。这样高5位(起始地址)+第三位(IRQ序号)便表示了任意一个IRQ的中断向量号。
ICW3
ICW3仅在级联的方式下才需要,用来设置主片和从片用哪个IRQ接口互连。
注意ICW3需要主片 0x21端口及从片的0xA1端口写入,ICW3结构如下:
对于主片,ICW3中置1的哪一位对应的IRQ接口用于连接从片,若为0则表示接外部设备。例如若主片IRQ2 和IRQ5接有从片,则主片的ICW3为0010010。
对于从片,由于从片专门用于级联主片的接口并不是IRQ,所以从片的结构与主片的不同。
从片ICW3的低三位用于标志从属于哪个IRQ接口,当中断响应时,主片会发送与从片做级联的IRQ接口号,所有从片用自己的ICW3的低3位和他对比,例如IRQ2对应的ICW3内容应该是00000010。正因如此,只需要低3位就行了,高5位全部置为0。
ICW4
ICW4需要主片0x21或从片0xA1写入,ICW4结构如下:
ICW4有些低位选项基于高位,所以这里先从高位讲起。
第7~5位未定义,直接置为0即可。
SFNM表示特殊全嵌套模式,若SFNM为0,则表示全嵌套模式;为1则表示特殊嵌套模式。
全嵌套模式:IRQ0优先级最高,IRQ7优先级最低,在CPU中断服务期间,若有新的中断请求到来,只允许优先级更高的中断请求进入,“同级”和“低级”的中断请求禁止响应。
特殊嵌套模式:IRQ的优先级规定一致,但在CPU中断请求时允许同级的中断请求进入,从而实现对同级中断请求的特殊嵌套。
一般而言,多片级联下,主片用特殊嵌套模式,从片用全嵌套模式。
BUF表示本8259A芯片是否工作在缓冲模式。BUF为0,则工作非缓冲模式;BUF为1,工作在缓冲模式。缓冲模式,8259A与经总线驱动器与系统数据总线相连。非缓冲模式,8259A数据线直接与系统数据总线相连。
当多个8259A级联时,如果工作在缓冲模式下,M/S为,表示主片;M/S为0表示从片。若工作在非缓冲模式下,M/S无效。
OCW
OCW1
OCW1用来屏蔽连接在8259A上的外部设备的中断信号,实际上就是把OCW1写入了IMR寄存器。但最终是否被屏蔽还是得看IF位是否为0.
OCW1要写入(主片)0x21端口或(从片)0xA1端口,OCW1结构如下:
M0~M7对应IRQ0~IRQ7,某位为1,对应的IRQ的中断信号就被屏蔽;为0的话则放行。
OCW2
OCW2用来设置中断结束方式和优先级模式,要写入到主片的0x20及从片的0xa0端口
如果R为0,表示采用固定优先级;若R为1表示采取循环优先级方式。
SL为指定最低优先级开关,若SL为1,可以使用L0~L2指定最低优先级。
EOI为中断结束命令位。EOI为1,则会令ISR寄存器中的相应位清0,也就是将当前处理的中断清掉,表示处理结束。向8259A主动发送EOI是手工结束中断的做法,所以使用前有个前提,ICW4中的AEOI位为0,即非自动中断时才用。
值得注意的是,在手动中断下,若中断来自主片,只需要向主片发送EOI,若来自从片,则对于主片和从片都要发送EOI。
第4~3位的00是OCW2的标识。
L2~L0用来确定优先级的编码,这里分两种,一种用于EOI时,表示被中断的优先级别,另一种用于优先级循环式,指定起始最低的优先级别。
OCW3
OCW3用来设定特殊屏蔽方式及查询方式,要写入主片的0x20或从片的0xa0
ICW和OCW识别
以上四个ICW要保证一定的次序写入,所以8259A就知道写入端口的数据是什么。
但OCW的写入与顺序无关,这就要根据端口的奇偶来判断,在写入ICW后,往奇地址端口写入的OCW只有OCW1 ,而对于OCW2和OCW3,在其结构中有设计判断。
这样,我们就完成了ICW和OCW的识别。注意,在使用ICW初始化之后,才可以用OCW对它进行操作。
编写中断处理程序
a
本节我们将通过操作8259A 打开中断,实现第一个中断处理程序。
init_all 函数用来初始化所有的设备及数据结构,init_all 首先调用idt_init,它用来初始化中断相关的内容。pic_init 用来初始化可编程中断控制器 8259A。ide_desc_init 用来初始化中断描述符表IDT,最后加载IDT。
我们先使用汇编语言来定义中断处理函数。由于这里要定义33个中断处理函数,我们先介绍一下如何使用宏来定义多行函数,其定义方法是:
%macro 宏名字参数个数
...
宏代码体
...
%endmacro
宏定义头中包含了“宏名字”,这是调用宏时用的,后面的“参数个数”只告诉预处理器,此宏可以支持的参数个数。在“宏代码体”部分,参数用“%数字”的方式引用,例如,第一个参数用%1。
这里定义了一个宏,宏名字为mul_add,参数个数为3。
OS/kernel/kerne.S
1、代码功能
创建33个中断处理函数。
2、实现原理
中断信号进入中断控制器后,会被分配中断向量号,把中断向量号作为索引寻找对应的中断处理程序。其中,0-19中断号为处理器内部固定的异常类型,20-31是Intel保留的。同时为了演示中断机理,只写一个简单时钟中断处理程序,所以共33个。
3、代码逻辑
定义33个中断处理程序,每个程序都有其处理和程序地址,且把中断处理程序的地址组成为一个顺序数组。每个中断处理程序都一样,调用打印函数打印"interrupt occur!"。
4、如何写代码?
A.为了统一栈中处理,为没有错误码的中断处理定义错误码
B.定义一个字符串,其中字符会在中断程序中被打印,然后定义一个标号。因为编辑器的特性,同一类型的SECTION会被组合成一个大SECTION,所以后续调用宏中所形成的中断处理程序地址会被汇集在这个标号下。
C.定义一个宏,包括声明中断处理程序的地址入口、错误码的压入、打印字符和本程序的入口地址。
D.注意表7-1,知道哪个中断需要手动压入错误码。
[bits 32]
%define ERROR_CODE nop ; 若在相关的异常中cpu已经自动压入了错误码,为保持栈中格式统一,这里不做操作.
%define ZERO push 0 ; 若在相关的异常中cpu没有压入错误码,为了统一栈中格式,就手工压入一个0
extern put_str ;声明外部函数
section .data
intr_str db "interrupt occur!", 0xa, 0
global intr_entry_table
intr_entry_table:
%macro VECTOR 2 ; 定义宏,两个参数,参数1用来命名,参数二表示是否要压入错误码
section .text
intr%1entry: ; 每个中断处理程序都要压入中断向量号,所以一个中断类型一个中断处理程序,自己知道自己的中断向量号是多少
%2
push intr_str ; 把打印的字符串压入栈中
call put_str ; 打印
add esp,4 ; 跳过参数
; 如果是从片上进入的中断,除了往从片上发送EOI外,还要往主片上发送EOI
mov al,0x20 ; 中断结束命令EOI
out 0xa0,al ; 向从片发送
out 0x20,al ; 向主片发送
add esp,4 ; 跨过错误码
iret ; 从中断返回,32位下等同指令iretd
section .data
dd intr%1entry ; 存储各个中断入口程序的地址,形成intr_entry_table数组
%endmacro
VECTOR 0x00,ZERO
VECTOR 0x01,ZERO
VECTOR 0x02,ZERO
VECTOR 0x03,ZERO
VECTOR 0x04,ZERO
VECTOR 0x05,ZERO
VECTOR 0x06,ZERO
VECTOR 0x07,ZERO
VECTOR 0x08,ERROR_CODE
VECTOR 0x09,ZERO
VECTOR 0x0a,ERROR_CODE
VECTOR 0x0b,ERROR_CODE
VECTOR 0x0c,ZERO
VECTOR 0x0d,ERROR_CODE
VECTOR 0x0e,ERROR_CODE
VECTOR 0x0f,ZERO
VECTOR 0x10,ZERO
VECTOR 0x11,ERROR_CODE
VECTOR 0x12,ZERO
VECTOR 0x13,ZERO
VECTOR 0x14,ZERO
VECTOR 0x15,ZERO
VECTOR 0x16,ZERO
VECTOR 0x17,ZERO
VECTOR 0x18,ERROR_CODE
VECTOR 0x19,ZERO
VECTOR 0x1a,ERROR_CODE
VECTOR 0x1b,ERROR_CODE
VECTOR 0x1c,ZERO
VECTOR 0x1d,ERROR_CODE
VECTOR 0x1e,ERROR_CODE
VECTOR 0x1f,ZERO
VECTOR 0x20,ZERO
上一节中我们完成了中断处理程序的编写,在本节中我们要把它们安装到中断描述符表中。即本节将要把中断处理程序地址装载到中断描述符中。
OS/kernel/global.h
global定义了两个数据,一个是低32位中16~32的选择子,一个是高32位中8~15的属性
#ifndef __KERNEL_GLOBAL_H
#define __KERNEL_GLOBAL_H
#include "stdint.h"
#define RPL0 0
#define RPL1 1
#define RPL2 2
#define RPL3 3
#define TI_GDT 0
#define TI_LDT 1
#define SELECTOR_K_CODE ((1 << 3) + (TI_GDT << 2) + RPL0)
#define SELECTOR_K_DATA ((2 << 3) + (TI_GDT << 2) + RPL0)
#define SELECTOR_K_STACK SELECTOR_K_DATA
#define SELECTOR_K_GS ((3 << 3) + (TI_GDT << 2) + RPL0)
//-------------- IDT描述符属性 ------------
#define IDT_DESC_P 1
#define IDT_DESC_DPL0 0
#define IDT_DESC_DPL3 3
#define IDT_DESC_32_TYPE 0xE // 32位的门
#define IDT_DESC_16_TYPE 0x6 // 16位的门,不用,定义它只为和32位门区分
#define IDT_DESC_ATTR_DPL0 ((IDT_DESC_P << 7) + (IDT_DESC_DPL0 << 5) + IDT_DESC_32_TYPE)
#define IDT_DESC_ATTR_DPL3 ((IDT_DESC_P << 7) + (IDT_DESC_DPL3 << 5) + IDT_DESC_32_TYPE)
#endif
OS/kernel/interrupt.h
interrupt.h声明了中断函数,给void*起了一个别名 intr_handler,意为中断处理程序头部
#ifndef __KERNEL_INTERRUPT_H
#define __KERNEL_INTERRUPT_H
#include "stdint.h"
typedef void* intr_handler;
void idt_init(void);
#endif
OS/kernel/interrupt.c
interrupt.c做了两件事,一是初始化并加载中断描述符表,二是对8259A进行初始化。
#include "interrupt.h"
#include "stdint.h"
#include "global.h"
#include "io.h"
#include "print.h"
#define PIC_M_CTRL 0x20 // 这里用的可编程中断控制器是8259A,主片的控制端口是0x20
#define PIC_M_DATA 0x21 // 主片的数据端口是0x21
#define PIC_S_CTRL 0xa0 // 从片的控制端口是0xa0
#define PIC_S_DATA 0xa1 // 从片的数据端口是0xa1
#define IDT_DESC_CNT 0x21 // 目前总共支持的中断数
/*中断门描述符结构体*/
struct gate_desc {//定义越低,地址越高
uint16_t func_offset_low_word; //中断处理程序在目标程序的低16位
uint16_t selector; //中断处理程序目标代码段描述符选择子
uint8_t dcount; //此项为双字计数字段,是门描述符中的第4字节。此项固定值,不用考虑
uint8_t attribute; //属性值,具体数值在global.h中定义
uint16_t func_offset_high_word; //中断处理程序在目标程序的高16位
};
// 静态函数声明,非必须
static void make_idt_desc(struct gate_desc* p_gdesc, uint8_t attr, intr_handler function);
static struct gate_desc idt[IDT_DESC_CNT]; //定义 idt是中断描述符表,本质上就是个中断门描述符数组
extern intr_handler intr_entry_table[IDT_DESC_CNT]; // 声明引用定义在kernel.S中的中断处理函数入口数组
/* 初始化可编程中断控制器8259A */
static void pic_init(void) {
/* 初始化主片 */
outb (PIC_M_CTRL, 0x11); // ICW1: 边沿触发,级联8259, 需要ICW4.
outb (PIC_M_DATA, 0x20); // ICW2: 起始中断向量号为0x20,也就是IR[0-7] 为 0x20 ~ 0x27.
outb (PIC_M_DATA, 0x04); // ICW3: IR2接从片.
outb (PIC_M_DATA, 0x01); // ICW4: 8086模式, 正常EOI
/* 初始化从片 */
outb (PIC_S_CTRL, 0x11); // ICW1: 边沿触发,级联8259, 需要ICW4.
outb (PIC_S_DATA, 0x28); // ICW2: 起始中断向量号为0x28,也就是IR[8-15] 为 0x28 ~ 0x2F.
outb (PIC_S_DATA, 0x02); // ICW3: 设置从片连接到主片的IR2引脚
outb (PIC_S_DATA, 0x01); // ICW4: 8086模式, 正常EOI
/* 打开主片上IR0,也就是目前只接受时钟产生的中断 */
outb (PIC_M_DATA, 0xfe);
outb (PIC_S_DATA, 0xff);
put_str(" pic_init done\n");
}
/* 创建中断门描述符 */
// 三个参数,中断描述符的指针、中断描述符内的属性及中断描述符内对应的中断处理程序
static void make_idt_desc(struct gate_desc* p_gdesc, uint8_t attr, intr_handler function) {
//使用后面两个参数来构建中断描述符的指针指向的元素
p_gdesc->func_offset_low_word = (uint32_t)function & 0x0000FFFF;
p_gdesc->selector = SELECTOR_K_CODE;
p_gdesc->dcount = 0;
p_gdesc->attribute = attr;
p_gdesc->func_offset_high_word = ((uint32_t)function & 0xFFFF0000) >> 16;
}
/*初始化中断描述符表*/
//填充中断描述符表
static void idt_desc_init(void) {
int i;
for (i = 0; i < IDT_DESC_CNT; i++) {
make_idt_desc(&idt[i], IDT_DESC_ATTR_DPL0, intr_entry_table[i]);
}
put_str(" idt_desc_init done\n");
}
/*完成有关中断的所有初始化工作*/
void idt_init() {
put_str("idt_init start\n");
idt_desc_init(); // 初始化中断描述符表
pic_init(); // 初始化8259A
/* 加载idt */
uint64_t idt_operand = ((sizeof(idt) - 1) | ((uint64_t)(uint32_t)idt << 16));
asm volatile("lidt %0" : : "m" (idt_operand));
put_str("idt_init done\n");
}
OS/kernel/io.h
io.h定义了四个与端口相关的函数,因为端口操作频繁,所以采取空间换速度的策略,在.h文件中定义函数。
/************** 机器模式 ***************
b -- 输出寄存器QImode名称,即寄存器中的最低8位:[a-d]l。
w -- 输出寄存器HImode名称,即寄存器中2个字节的部分,如[a-d]x。
HImode
“Half-Integer”模式,表示一个两字节的整数。
QImode
“Quarter-Integer”模式,表示一个一字节的整数。
*******************************************/
#ifndef __LIB_IO_H
#define __LIB_IO_H
#include "stdint.h"
/* 向端口port写入一个字节*/
static inline void outb(uint16_t port, uint8_t data)
{
/*
a表示用寄存器al或ax或eax,对端口指定N表示0~255, d表示用dx存储端口号, %b0表示对应al,%w1表示对应dx */
asm volatile ( "outb %b0, %w1" : : "a" (data), "Nd" (port));
}
/* 将addr处起始的word_cnt个字写入端口port */
static inline void outsw(uint16_t port, const void* addr, uint32_t word_cnt) {
/*
+表示此限制即做输入又做输出.
outsw是把ds:esi处的16位的内容写入port端口, 我们在设置段描述符时, 已经将ds,es,ss段的选择子都设置为相同的值了,此时不用担心数据错乱。*/
asm volatile ("cld; rep outsw" : "+S" (addr), "+c" (word_cnt) : "d" (port));
}
/* 将从端口port读入的一个字节返回 */
static inline uint8_t inb(uint16_t port) {
uint8_t data;
asm volatile ("inb %w1, %b0" : "=a" (data) : "Nd" (port));
return data;
}
/* 将从端口port读入的word_cnt个字写入addr */
static inline void insw(uint16_t port, void* addr, uint32_t word_cnt) {
/*
insw是将从端口port处读入的16位内容写入es:edi指向的内存,
我们在设置段描述符时, 已经将ds,es,ss段的选择子都设置为相同的值了,
此时不用担心数据错乱。*/
asm volatile ("cld; rep insw" : "+D" (addr), "+c" (word_cnt) : "d" (port) : "memory");
}
#endif
至此只需要调用idt_init就可以构造idt了 ,在interrupt.h把idt_inti函数声明补上。
void idt_init(void);
OS/kernel/init.c
init.c用于方便调度所有初始化函数。
#include "init.h"
#include "print.h"
#include "interrupt.h"
/*负责初始化所有模块 */
void init_all() {
put_str("init_all\n");
idt_init(); //初始化中断
}
OS/kernel/init.h
init.h声明函数,方便在main中调用
#ifndef __KERNEL_INIT_H
#define __KERNEL_INIT_H
void init_all(void);
#endif
OS/kernel/main.c
main函数调用初始化并打开中断。
#include "print.h"
#include "init.h"
void main(void) {
put_str("I am kernel\n");
init_all();
asm volatile("sti"); // 为演示中断处理,在此临时开中断
while(1);
}
接下来进行编译,为了目录不至于太乱,建立build目录(myos/build)用于将所有目标文件和编译后的内核文件都放在此目录中
编译代码
nasm -f elf -o build/print.o lib/kernel/print.S
nasm -f elf -o build/kernel.o kernel/kernel.S
gcc-4.4 -m32 -I lib/kernel/ -I lib/ -I kernel/ -c -fno-builtin -o build/interrput.o kernel/interrupt.c
gcc-4.4 -m32 -I lib/kernel/ -I lib/ -I kernel/ -c -fno-builtin -o build/init.o kernel/init.c
gcc-4.4 -m32 -I lib/kernel/ -I lib/ -I kernel/ -c -fno-builtin -o build/main.o kernel/main.c
ld -o build/kernel.bin -m elf_i386 -Ttext 0xc0001500 -e main build/main.o build/print.o build/interrput.o build/kernel.o build/init.o
dd if=build/kernel.bin of=/home/moyao/Desktop/bochs/hd60M.img bs=512 count=200 seek=9 conv=notrunc
改进中断处理程序
在本节中,我们将在C语言中建立目标中断处理函数数组idt_table,数组元素是C版本中的中断处理函数地址,提供给汇编语言中的intrXXentry 调用。这需要修改两个文件,interrupt.c 和 kernel.S。
b/kernel/interrupt.c
#include "interrupt.h"
#include "stdint.h"
#include "global.h"
#include "io.h"
#include "print.h"
#define PIC_M_CTRL 0x20 // 这里用的可编程中断控制器是8259A,主片的控制端口是0x20
#define PIC_M_DATA 0x21 // 主片的数据端口是0x21
#define PIC_S_CTRL 0xa0 // 从片的控制端口是0xa0
#define PIC_S_DATA 0xa1 // 从片的数据端口是0xa1
#define IDT_DESC_CNT 0x21 // 目前总共支持的中断数
/*中断门描述符结构体*/
struct gate_desc {//定义越低,地址越高
uint16_t func_offset_low_word; //中断处理程序在目标程序的低16位
uint16_t selector; //中断处理程序目标代码段描述符选择子
uint8_t dcount; //此项为双字计数字段,是门描述符中的第4字节。此项固定值,不用考虑
uint8_t attribute; //属性值,具体数值在global.h中定义
uint16_t func_offset_high_word; //中断处理程序在目标程序的高16位
};
// 静态函数声明,非必须
static void make_idt_desc(struct gate_desc* p_gdesc, uint8_t attr, intr_handler function);
static struct gate_desc idt[IDT_DESC_CNT]; //定义 idt是中断描述符表,本质上就是个中断门描述符数组
char* intr_name[IDT_DESC_CNT]; // 用于保存异常的名字
intr_handler idt_table[IDT_DESC_CNT]; // 中断处理程序地址的数组.在kernel.S中定义的intrXXentry只是中断处理程序的入口,最终调用的是ide_table中的处理程序
extern intr_handler intr_entry_table[IDT_DESC_CNT]; // 声明引用定义在kernel.S中的中断处理函数入口数组
/* 初始化可编程中断控制器8259A */
static void pic_init(void) {
/* 初始化主片 */
outb (PIC_M_CTRL, 0x11); // ICW1: 边沿触发,级联8259, 需要ICW4.
outb (PIC_M_DATA, 0x20); // ICW2: 起始中断向量号为0x20,也就是IR[0-7] 为 0x20 ~ 0x27.
outb (PIC_M_DATA, 0x04); // ICW3: IR2接从片.
outb (PIC_M_DATA, 0x01); // ICW4: 8086模式, 正常EOI
/* 初始化从片 */
outb (PIC_S_CTRL, 0x11); // ICW1: 边沿触发,级联8259, 需要ICW4.
outb (PIC_S_DATA, 0x28); // ICW2: 起始中断向量号为0x28,也就是IR[8-15] 为 0x28 ~ 0x2F.
outb (PIC_S_DATA, 0x02); // ICW3: 设置从片连接到主片的IR2引脚
outb (PIC_S_DATA, 0x01); // ICW4: 8086模式, 正常EOI
/* 打开主片上IR0,也就是目前只接受时钟产生的中断 */
outb (PIC_M_DATA, 0xfe);
outb (PIC_S_DATA, 0xff);
put_str(" pic_init done\n");
}
/* 创建中断门描述符 */
// 三个参数,中断描述符的指针、中断描述符内的属性及中断描述符内对应的中断处理程序
static void make_idt_desc(struct gate_desc* p_gdesc, uint8_t attr, intr_handler function) {
//使用后面两个参数来构建中断描述符的指针指向的元素
p_gdesc->func_offset_low_word = (uint32_t)function & 0x0000FFFF;
p_gdesc->selector = SELECTOR_K_CODE;
p_gdesc->dcount = 0;
p_gdesc->attribute = attr;
p_gdesc->func_offset_high_word = ((uint32_t)function & 0xFFFF0000) >> 16;
}
/*初始化中断描述符表*/
//填充中断描述符表
static void idt_desc_init(void) {
int i;
for (i = 0; i < IDT_DESC_CNT; i++) {
make_idt_desc(&idt[i], IDT_DESC_ATTR_DPL0, intr_entry_table[i]);
}
put_str(" idt_desc_init done\n");
}
//中断处理函数
static void general_intr_handler(uint8_t vec_nr)
{
if (vec_nr == 0x27 || vec_nr == 0x2f) { // 0x2f是从片8259A上的最后一个irq引脚,保留
return; //IRQ7和IRQ15会产生伪中断(spurious interrupt),无须处理。
}
//打印中断向量号
put_str("int vector: 0x");
put_int(vec_nr);
put_char('\n');
}
/* 完成一般中断处理函数注册及异常名称注册 */
static void exception_init(void)
{ // 完成一般中断处理函数注册及异常名称注册
int i;
for (i = 0; i < IDT_DESC_CNT; i++)
{
idt_table[i] = general_intr_handler;//指向中断处理函数的地址
intr_name[i] = "unknown"; //先定义为unknown,异常只有20个,这里是保证intr_name[20~30]不指空
}
//赋予异常正确的异常名称
intr_name[0] = "#DE Divide Error";
intr_name[1] = "#DB Debug Exception";
intr_name[2] = "NMI Interrupt";
intr_name[3] = "#BP Breakpoint Exception";
intr_name[4] = "#OF Overflow Exception";
intr_name[5] = "#BR BOUND Range Exceeded Exception";
intr_name[6] = "#UD Invalid Opcode Exception";
intr_name[7] = "#NM Device Not Available Exception";
intr_name[8] = "#DF Double Fault Exception";
intr_name[9] = "Coprocessor Segment Overrun";
intr_name[10] = "#TS Invalid TSS Exception";
intr_name[11] = "#NP Segment Not Present";
intr_name[12] = "#SS Stack Fault Exception";
intr_name[13] = "#GP General Protection Exception";
intr_name[14] = "#PF Page-Fault Exception";
// intr_name[15] 第15项是intel保留项,未使用
intr_name[16] = "#MF x87 FPU Floating-Point Error";
intr_name[17] = "#AC Alignment Check Exception";
intr_name[18] = "#MC Machine-Check Exception";
intr_name[19] = "#XF SIMD Floating-Point Exception";
}
/*完成有关中断的所有初始化工作*/
void idt_init() {
put_str("idt_init start\n");
idt_desc_init(); // 初始化中断描述符表
exception_init(); // 异常名初始化并注册通常的中断处理函数
pic_init(); // 初始化8259A
/* 加载idt */
uint64_t idt_operand = ((sizeof(idt) - 1) | ((uint64_t)(uint32_t)idt << 16));
asm volatile("lidt %0" : : "m" (idt_operand));
put_str("idt_init done\n");
}
b/kernel/kernel.S
[bits 32]
%define ERROR_CODE nop ; 若在相关的异常中cpu已经自动压入了错误码,为保持栈中格式统一,这里不做操作.
%define ZERO push 0 ; 若在相关的异常中cpu没有压入错误码,为了统一栈中格式,就手工压入一个0
extern idt_table ;idt_table是C中注册的中断处理程序数组
section .data
global intr_entry_table
intr_entry_table:
%macro VECTOR 2
section .text
intr%1entry: ; 每个中断处理程序都要压入中断向量号,所以一个中断类型一个中断处理程序,自己知道自己的中断向量号是多少
%2 ; 中断若有错误码会压在eip后面
; 以下是保存上下文环境,汇编中使用c文件的函数一定会打乱原有的,所以全部压入栈中
push ds
push es
push fs
push gs
pushad ; PUSHAD指令压入32位寄存器,其入栈顺序是: EAX,ECX,EDX,EBX,ESP,EBP,ESI,EDI
; 如果是从片上进入的中断,除了往从片上发送EOI外,还要往主片上发送EOI
mov al,0x20 ; 中断结束命令EOI
out 0xa0,al ; 向从片发送
out 0x20,al ; 向主片发送
push %1 ; 不管idt_table中的目标程序是否需要参数,都一律压入中断向量号,调试时很方便,这里也是中断处理函数的参数
call [idt_table + %1*4] ; 调用idt_table中的C版本中断处理函数
jmp intr_exit
section .data
dd intr%1entry ; 存储各个中断入口程序的地址,形成intr_entry_table数组
%endmacro
section .text
global intr_exit
intr_exit:
; 以下是恢复上下文环境
add esp, 4 ; 跳过中断号
popad
pop gs
pop fs
pop es
pop ds
add esp, 4 ; 跳过error_code
iretd
VECTOR 0x00,ZERO
VECTOR 0x01,ZERO
VECTOR 0x02,ZERO
VECTOR 0x03,ZERO
VECTOR 0x04,ZERO
VECTOR 0x05,ZERO
VECTOR 0x06,ZERO
VECTOR 0x07,ZERO
VECTOR 0x08,ERROR_CODE
VECTOR 0x09,ZERO
VECTOR 0x0a,ERROR_CODE
VECTOR 0x0b,ERROR_CODE
VECTOR 0x0c,ZERO
VECTOR 0x0d,ERROR_CODE
VECTOR 0x0e,ERROR_CODE
VECTOR 0x0f,ZERO
VECTOR 0x10,ZERO
VECTOR 0x11,ERROR_CODE
VECTOR 0x12,ZERO
VECTOR 0x13,ZERO
VECTOR 0x14,ZERO
VECTOR 0x15,ZERO
VECTOR 0x16,ZERO
VECTOR 0x17,ZERO
VECTOR 0x18,ERROR_CODE
VECTOR 0x19,ZERO
VECTOR 0x1a,ERROR_CODE
VECTOR 0x1b,ERROR_CODE
VECTOR 0x1c,ZERO
VECTOR 0x1d,ERROR_CODE
VECTOR 0x1e,ERROR_CODE
VECTOR 0x1f,ZERO
VECTOR 0x20,ZERO
可编程计数器/定时器 8253简介
前提知识
定时器 8253是硬件,用来自动发送一个输出信号例如提示处理器响应中断或者触发事件。其中,8253 是一个倒计时,即每一次时钟脉冲将计数值减一,为0表示时间已到。
8253 内部有3个独立的计数器,他们的端口分别是0x40~0x42。计数器内部有寄存器资源,包括计数初值寄存器、一个减法计数器和一个输出锁存值。在编程时,将初值放入计数初值寄存器,然后放入减法计数器计算,再把计算的放在输出锁存器中方便外部查看。
每个计数器都有三个引脚:CLK、GATE、OUT。
CLK表示时钟输入信号,每当此引脚收到一个信号,减法计数器就减一,8253最高脉冲频率为2MHz。
GATE表示门控输入信号,是计数器开始工作的条件之一。
OUT表示计数器输出信号,计数值为0时,根据计数器的工作方式会在OUT引脚上输出相应的信号。以此来通知处理器或某个设备。
这里我们只需要修改计数器0。
我们使用控制器寄存器来指定计数器的工作方式。
这里我们着重讲解方式2:比率发生器,即经过N个CLK才触发一次OUT。当处理器把控制字写入到计数器后,OUT端变成高电平。在GATE为高电平的前提下,处理器将计数初值写入后,再写一个CLK时钟脉冲的下降沿,计数器启动计数。当计数值为1时,OUT端由高电平变为低电平,次此低电平的状态一直到计数为0时,OUT端右边为高电平。同时,计数初值又会被载入减法计数器,周而复始地循环计数。
计数器的工作频率是 1.19318MHz,即一秒内会有 1193180 此脉冲信号。每次初始值减为0时,中断一次,所以:
初始化步骤
1、往控制器寄存器端口 0x43 中写入控制字
2、 在所指定使用的计数器端口中写入计数初值
提高时钟中断的频率(代码)
我们在此节将要提高时钟1秒内发生中断的次数。默认的时钟中断信号的频率是18.026Hz,即一秒内大约发出18此中断信号。我们将把其提高到100Hz。
在开始代码之前,我们先梳理要做的工作。
- IRQ0 引脚上的时钟中断信号频率是由 8253 的计数器0 设置的,我们要使用计数器 0 。
- 时钟发出的中断信号不能只发一次,必须是周期性发出的,即我们需要采取循环计数的工作方式,这里我们选择方式2。
- 这里我们要为计算器0赋予合适的计数初值。
好啦,综上所述,我们通过控制字指定使用计数器0,工作方式选择,并为计数器0赋予合适的计数初值。计算出11932就是计数器0的计数初值。
c/device/time.c
#include "timer.h"
#include "io.h"
#include "print.h"
#define IRQ0_FREQUENCY 100 //频率为100Hz,用做后续的计算
#define INPUT_FREQUENCY 1193180 //8253计数器一秒之内的网络脉冲
#define COUNTER0_VALUE INPUT_FREQUENCY / IRQ0_FREQUENCY //计算出 计算器初值
#define CONTRER0_PORT 0x40 //计数器0的端口
#define COUNTER0_NO 0 //控制字的第7~8位,表示选择计数器0
#define COUNTER_MODE 2 //控制字第2~4位,选择工作方式2
#define READ_WRITE_LATCH 3 //选择读写方式,先读写低字节后读写高字节
#define PIT_CONTROL_PORT 0x43 //控制字的端口0x43
//把操作的计数器counter_no、读写锁属性rwl、计数器工作模式counter_mode写入模式控制寄存器并赋予计算初值counter_value
static void frequency_set(uint8_t counter_port, \
uint8_t counter_no, \
uint8_t rwl, \
uint8_t counter_mode, \
uint16_t counter_value) {
//往控制字寄存器端口0x43中写入控制字
outb(PIT_CONTROL_PORT, (uint8_t)(counter_no << 6 | rwl << 4 | counter_mode << 1));
//先写入counter_value的低8位,这里写入的是计算初值,后续计数器的操作会自动完成
outb(counter_port, (uint8_t)counter_value);
//再写入counter_value的高8位
outb(counter_port, (uint8_t)counter_value >> 8);
}
//初始化PIT8253
void timer_init() {
put_str("timer_init start\n");
//设置8253的定时周期,也就是发中断的周期
frequency_set(CONTRER0_PORT, COUNTER0_NO, READ_WRITE_LATCH, COUNTER_MODE, COUNTER0_VALUE);
put_str("timer_init done\n");
}
c/device/time.h
#ifndef __DEVICE_TIME_H
#define __DEVICE_TIME_H
#include "stdint.h"
void timer_init(void);
#endif
c/device/init.c
添加进init_all函数当中,记得修改time.h的地址,但也可以在连接中把device文件放进去
#include "init.h"
#include "print.h"
#include "interrupt.h"
#include "/home/moyao/Desktop/bochs/device/timer.h"
/*负责初始化所有模块 */
void init_all() {
put_str("init_all\n");
idt_init(); // 初始化中断
timer_init(); // 初始化PIT
}
编译代码
nasm -f elf -o build/print.o lib/kernel/print.S
nasm -f elf -o build/kernel.o kernel/kernel.S
gcc-4.4 -m32 -I lib/kernel/ -I lib/ -I kernel/ -c -fno-builtin -o build/interrput.o kernel/interrupt.c
gcc-4.4 -m32 -I lib/kernel/ -I lib/ -I kernel/ -c -fno-builtin -o build/time.o device/time.c
gcc-4.4 -m32 -I lib/kernel/ -I lib/ -I kernel/ -c -fno-builtin -o build/init.o kernel/init.c
gcc-4.4 -m32 -I lib/kernel/ -I lib/ -I kernel/ -c -fno-builtin -o build/main.o kernel/main.c
ld -o build/kernel.bin -m elf_i386 -Ttext 0xc0001500 -e main build/main.o build/print.o build/interrput.o build/kernel.o build/init.o build/time.o
dd if=build/kernel.bin of=/home/moyao/Desktop/bochs/hd60M.img bs=512 count=200 seek=9 conv=notrunc