《真象还原》读书笔记——第七章 中断处理

7.1 中断是什么,为什么中断

中断可以并发执行多个程序,提升系统利用率。

  • 并发是单位时间内的积累工作量
  • 并行是真正同时进行的工作量
    有了中断,我们才能一边使用键盘一边使用鼠标。

7.2 操作系统是中断驱动的

是操作系统是被动工作的,有事情发生它才会工作,所以它是被事件驱动的,而这个事件是以中断的形式通知操作系统的。

7.3 中断的分类

7.3.1 外部中断

中断源必须是某个硬件,所以外部中断又称为硬件中断
例如:网卡收到了网络的数据包,网卡就会主动通知CPU。

CPU提供了两条信号线。
外部硬件的中断是通过两根信号线通知 CPU 的,这两根信号线就是INTR(INTeRrupt)和 NMI(Non Maskable Interrupt)。
在这里插入图片描述
从 INTR 引脚收到的中断都是不影响系统运行的,可以随时处理,或直接不处理。
NMI 引脚收到的中断,那基本上全是非常严重的。

所以可屏蔽中断的信号是通过 INTR 引脚进入 CPU 的。CPU可以不理会。通过eflags 的IF位,可以将这些外部设备的中断屏蔽。也可以通过中断代理单独屏蔽某个设备。

在理会后,还可以将其划分为上半部分和下半部分。上半部分是需要立即执行的中断,下半部分是可以向后拖延的不紧急的中断。
例如:网卡的接收数据属于上半部分,当数据收到后的处理数据即可以放到下半部分处理。

CPU收到中断后,得知是什么原因后通过中断向量表或中断描述符表来实现。首先为每一个中断分配一个中断向量号。就是中断向量或中断描述符表的下标中。CPU会根据中断向量号在中断向量表或中断描述符表中检索对于的中断处理程序并去执行。

因为不可屏蔽的错误每一个都是硬伤,所以直接用一个中断向量后2为代表。

7.3.2 内部中断

内部中断可分为软中断和异常。
软中断:由于该中断是软件运行中主动发起的,所以它是主观上的

  • int 8 位立即数:系统调用
  • int3 是调试断点指令
  • into。这是中断溢出指令,它所触发的中断向量号是 4,不过要看 eflags 中的OF是否为1,是1才会引发。
  • bound 检查数组索引越界,触发5号中断。
  • ud2 未定义指令,触发6号中断。

中断无视eflags 中的IF位时:

  1. 导致错误的中断类型,会无视IF,如NMI,异常。
  2. int n型软中断用于实现系统调用功能。软中断也无视IF。

异常有轻重程度:
1. Fault,也称为故障。这种错误是可以被修复的一种类型,属于最轻的一种异常
2. Trap,也称为陷阱,这一名称很形象地说明软件掉进了 CPU 设下的陷阱,导致停了下来
3. Abort,也称为终止,从名字上看,这是最严重的异常类型

某些异常会有错误码,error code,进入中断时CPU会把它们压入栈中。在压入 eip 之后。
在这里插入图片描述
中断向量号 0-255 八位。
中断机制的本质是来了一个中断信号后,调用相应的中断处理程序。
每个中断信号分配一个整数,用此整数作为中断的 ID,而这个整数就是所谓的中断向量,然后用此 ID 作为中断描述符表中的索引,这样就能找到对应的表项,进而从中找到对应的中断处理程序
中断向量专用于中断描述符表。
异常和不可屏蔽中断的中断向量号是由 CPU 自动提供的,而来自外部设备的可屏蔽中断号是由中断代理提供的(咱们这里的中断代理是 8259A),软中断是由软件提供的。

7.4 中断描述符表

中断描述符表(IDT)是在保护模式下,用于存储中断处理程序入口的表,CPU接收一个中断时候,需要用中断向量检索对应的描述符。
描述符中有什么
段描述符中描述的是一片内存区域,而门描述符中描述的是一段代码。

各种门
1.任务门
记录的是TSS选择子,可以存在于GDT,LDT,IDT中,不过大多数操作系统都没使用TSS实现任务切换。
type 值为二进制 0101
在这里插入图片描述
2.中断门
包含了中断处理程序所在段的段选择子和段内偏移地址。
进入中断后,eflags中的IF位自动置于0,避免中断嵌套。
中断门只允许存在于 IDT 中。描述符中中断门的 type 值为二进制 1110
在这里插入图片描述
3. 陷阱门
器 eflags 中的 IF 位不会自动置 0。
门只允许存在于 IDT。
type 值为二进制 1111
在这里插入图片描述
4.调用门
提供给用户进程进入特权 0 级的方式,其 DPL 为 3。
记录例程的地址,它不能用int 指令调用,只能用 call 和 jmp 指令。
安装在 GDT 和 LDT。
调用门的 type 值为
二进制 1100。在这里插入图片描述
我们会把精力放在中断门,像 Linux 那样,用它来实现系统调用.
任何中断源都通过中断向量对应到中断描述符表中的门描述符,通过该门描述符就找到了对应的中断处理程序.

中断向量相当于子弹,门描述符相当于靶子,中断描述符表相当于狙击手.
中断描述符表的区别于中断向量表:
1. 中断描述符表地址不限制,在哪里都可以。
2. 中断描述符表中的每个描述符用 8 字节描述。
中断描述符表寄存器:
0-15 表边界(IDT大小-1),16-47 IDT基地址
中断描述符表寄存器
特别注意的是 GDT 中的第 0个段描述符是不可用的,但 IDT 却无此限制,第 0 个门描述符也是可用的,中断向量号为 0 的中断是除法错。但处理器只支持 256个中断,即 0~254。
P位记得在构建IDT时候设置为0.这样表示示门描述符中的中断处理程序不在内存中。
lidt 48位内存数据

7.4.1 中断处理过程及保护

中断代理芯片接收中断后,将该中断的中断向量号发送到 CPU。
CPU 执行该中断向量号对应的中断处理程序。
1 据中断向量号定位中断门描述符
中断描述符是 8 个字节,处理器用中断向量号乘以 8 后,与 IDTR 中的中断描述符表地址相加便是该中断向量号对应的中断描述符。
2 处理器进行特权级检查
当前特权级 CPL 必须在门描述符 DPL 和门中目标代码段 DPL 之间。
a. 用户进程中主动发起的中断,由用户代码控制。 数值上 CPL <= 门描述符DPL,特权级门槛通过,进入下一个检查。
b. 数值CPL > 目标代码 DPL,检查通过。说明,除了用返回指令从高特权级返回,特权转移只发生在由低权到高权。
3 执行中断处理程序
将门描述符目标代码段选择子加载到代码段寄存器 CS 中把门描述符中中断处理程序的偏移地址加载到 EIP.
在这里插入图片描述
中断发生后,eflags 中的 NT 位和 TF 位会被置 0。如果中断对应的门描述符是中断门,标志寄存器 eflags 中的 IF 位被自动置 0,避免中断嵌套.

任务门或陷阱门的话, CPU 是不会将 IF 位清 0 的。因为陷阱门主要用于调试,它允许 CPU 响应更高级别的中断,所以允许中断嵌套。
而对任务门来说,这是执行一个新任务,任务都应该在开中断的情况下进行。

从中断返回的指令是 iret,它从栈中弹出数据到寄存器 cs、eip、eflags 等,根据特权级是否改变,判断是否要恢复旧栈,也就是说是否将栈中位于 SS_old 和 ESP_old 位置的值弹出到寄存 ss 和 esp。
当中断处理程序执行完成返回后,通过 iret 指令从栈中恢复 eflags 的内容.

有时为了避免操作被中断打断,通过控制IF位,控制中断。cli 使 IF 位为 0,这称为关中断,指令 sti 使 IF 位为 1,这称为开中断。IF只能限制外部设备中断。

进入中断时要把 NT 位和 TF 位置为 0。TF 表示 Trap Flag,也就是陷阱标志位,这用在调试环境中,当
TF 为 0 时表示禁止单步执行。NT是1,说明任务是嵌套的,退出时要返回上一个任务。如果 NT 位的值为 0,这表示当前是在中断处理环境下,于是就执行正常的中断退出流程。

7.4.2 中断发生时的压栈

全部算段间转移,也就是远转移。
所以处理器自动把 CS 和 EIP 的当前值保存到中断处理程序使用的栈中。不同特权级别下处理器使用不同的栈,要视它当时所在的特权级别,因为中断是可以在任何特权级别下发生的。除了要保存CS、EIP 外,还需要保存标志寄存器 EFLAGS,如果涉及到特权级变化,还要压入 SS 和 ESP 寄存器。

  1. 从低权到高权
  2. 临时保存 ss_old,esp_old
  3. 加载TSS中高权的ss,esp
  4. 把ss_old和esp_old放入高权栈中,如果是16位的,则填充0到高位直至32位。
  5. 压入 EFLAGS到高权栈中
  6. 压入CS_old和EIP_old,也是如果位数不够则填充0到32位。
  7. 如果有错误码,则压入错误码 ERROR_CODE.
    在这里插入图片描述
    无特权级变化时候
    在这里插入图片描述
    16位模式下用 iretw, 32位模拟下用iretd
    iret是简写,16位和32位都可以用iret.

注意,如果中断有错误码,处理器并不会主动跳过它的位置,咱们必须手动将其跳过,并让其指向栈中eip。
注意返回时候也是要检查特权级的。
返回后的 数值上CPL > 数据段描述符的DPL,则会把段寄存器设置为零的。

7.4.3 中断错误码

在这里插入图片描述

  • EXT 1:中断来自不可屏蔽源NMI或外部设备。
  • IDT 1:是中断描述符表,不是GDT或LDT
  • TI 1:LDT中的检索描述符,0是GDT中的检索描述符。
    通常压入错误码的中断属于中断向量号在 0~32 之内的异常。
    外部中断(中断向量号在 32~255 之间)和 int 软中断并不会产生错误码。
    并且通常我们并不用处理错误码。

7.5 可编程中断控制器 8259A

7.5.1 8259A 介绍

8259A 有哪些功能呢?

  1. 管理和控制可屏蔽中断,屏蔽外设中断.
  2. 实行优先级判决
  3. CPU 提供中断向量号

Intel 处理器可支持256个中断,但8259A只管理8个中断。所以把多个8259A级联起来,最多连9个,也就是最多64个中断。((8-1) * 9 + 1) 。n片8259A通过级联的方式可支持7n+1个中断源。
级联的时候只有一个主片master,其余都是从片slave。
在这里插入图片描述
8259A在收到了中断后,对中断判优,将优先级高的中断转发给 CPU处理。
在这里插入图片描述
在这里插入图片描述

  • IMR 寄存器中的位,为 1,则表示中断屏蔽,为 0,则表示中断放行
  • 是 IRQ 接口号越低,优先级越大,所以 IRQ0 优先级最大
  • 即将刚才选出来的优先级最大的中断在 ISR 寄存器中对应的 BIT 置 1,此寄存器表示当前正在处理的中断,同时要将该中断从“待处理中断队列”寄存器 IRR 中去掉,也就是在IRR 中将该中断对应的 BIT 置 0。
  • 以用起始中断向量号+IRQ 接口号便是该设备的中断向量号
  • CPU 从数据总线上拿到该中断向量号后,用它做中断向量表或中断描述符表中的索引,找到相应的中断处理程序并去执行
  • 8259A 在收到 EOI 后,将当前正处理的中断在 ISR 寄存器中对应的 BIT 置 0。如果8259A的“EOI通知(End Of Interrupt)”若被设置为非自动模式(手工模式),中断处理程序结束处必须有向 8259A 发送 EOI 的代码。
    当然,在进入ISR后,由于优先级的情况还是有可能被换下来。

7.5.2 8259A的编程

在8259A内部有两组寄存器。
一组是初始化命令寄存器组,用于保存初始化命令字(Initialization Command Words, ICW),ICW共4个。
另一组寄存器是操作命令寄存器组,用来保存操作命令字(Operation Command Word, OCW),OCW共3个,OCW1-OCW3。
所以我们的操作也是两部分。初始化和操作。

  • ICW 做初始化,是否级联,设置起始中断向量号,设置中断结束模式。
  • OCW控制8259A,中断屏蔽和中断结束,发送顺序不确定。
    以下是一些介绍:
    ICW1

ICW1:初始化8259A的连接方式(单片还是级联)和中断信号触发方式(电平还是边沿触发)
ICW1需要写入主片的0x20端口和从片的0xA0端口。

IC4表示是否写入 ICW4
IC4为1时候,需要在后面写入ICW4,但x86系统IC4必须为1.

SNGL 表示 single,若 SNGL为1,表示单片,若SNGL为0,说明级联。级联的时候涉及到主片和从片的用哪个 IRQ接口相互连接,所以还是要用到ICW3的。
ADI 表示 call address interval,用来设置8085的调用时间间隔,x86不需要设置。
LTIM 表示 level/edge triggered mode,用来设置中断检测方式,LTIM为0表示边沿触发,LTIM为1表示电平触发。

在这里插入图片描述

ICW2用于设置起始中断向量号。这里的设置就是指定IRQ0映射到的中断向量号,其他IRQ按顺序排下去。所以只需要写前3-7位即可。

ICW2需要写入主片的0x21端口和从片的0xA1端口。
ICW3 仅在级联的情况下才需要,用来设置主片和从片用哪个IRQ接口连接。
在这里插入图片描述

ICW3 需要写入主片的 0x21 端口及从片的 0xA1 端口。
ICW3 中置 1 的那一位对应的 IRQ 接口用于连接从片,若为 0 则表示接外部设备。比如,若主片 IRQ2 和 IRQ5 接有从片,则主片的 ICW3 为 00100100。
在这里插入图片描述

设置从片连接主片的方法是只需要在从片上指定主片用于连接自己的那个 IRQ 接口就行了。也就是从片ICW3的0-2位,例如主片IRQ2接口连接从片A,用IRQ5连接从片B,从片A的ICW3的值应该设置为00000010,B的ICW3设置00000101。所以只要低三位即可。
在这里插入图片描述
SFNM表示特殊全嵌套
0:全嵌套
1:特殊全嵌套
BUF:
0:非缓冲区
1:缓冲区
级联:M/S:如果是非缓冲状态则M/S位无效。
1: 是主片
0:是从片
AEOI:自动结束中断
0 非自动
1 自动结束中断
μPM:
0 表示处理器为8080/8085处理器
1 x86处理器
在这里插入图片描述
OCW1 要写入主片的 0x21 或从片的 0xA1 端口
OCW1 屏蔽连接在 8259A 上的外部设备的中断信号,M0~M7 对应 8259A 的 IRQ0~IRQ7,某位为 1,对应的 IRQ 中断信号屏蔽。为0 ,信号放行。
受标志寄存器 eflags 中的 IF 位的管束,若 IF 为 0,可屏蔽中断全部被屏蔽
在这里插入图片描述
OCW2 用来设置中断结束方式和优先级模式。
OCW2 要写入到主片的 0x20 及从片的 0xA0 端口。

SL,可以针对某个特定优先级的中断进行操作。
1 :可以用 OCW2 的低 3 位(L2~L0)来指定位于 ISR 寄存器中的哪一个中断被终止
0 :L2~L0 便不起作用了,8259A 会自动将正在处理的中断结束,也就是把 ISR 寄存器中优先级最高的位清 0

R:设置优先级控制方式
0:表示固定优先级方式,即 IRQ 接口号越低,优先级越高
在这里插入图片描述

1:表明用循环优先级方式,这样优先级会在 0~7 内循环。如果 SL 为 0,初始的优先级次序为 IR0>IR1>IR2>IR3>IR4>IR5>IR6>IR7

EOI:中断结束命令位。令 EOI 为 1,则会令 ISR 寄存器中的相应位清 0,也就是将当前处理的中断清掉,表示处理结束。向 8259A 主动发送 EOI 是手工结束中断的做法,所以,使用此命令有个前提,就是 ICW4 中的 AEOI 位为 0,非自动结束中断时才用。
在这里插入图片描述
在这里插入图片描述
OCW3 用来设定特殊屏蔽方式及查询方式
OCW3要写入主片的0x20端口或从片的0xA0端口

ESMM和SMM是用来启动或禁止特殊屏蔽模式的。ESMM是允许位——总开关。SMM是特殊屏蔽模式位。ESMM开启时,SMM才有效。
P:
1 设置8259A为中断查询方式,就可以读取寄存器,例如IRS。

RR,读取寄存器指令。与 RIS一起使用,只有RR为1时候才可以读取寄存器。

RIS 读取中断寄存器选择位:
1 选 ISR寄存器
0 选 IRR寄存器

ICW1 和 OCW2、OCW3 是用偶地址端口 0x20(主片)或 0xA0(从片)写入
ICW2~ICW4 和 OCW1 是用奇地址端口 0x21(主片)或 0xA1(从片)写入

8259A 的编程就是写入 ICW 和 OCW

  1. 无论 8259A 是否级联,ICW1 和 ICW2 是必须要有的,并且要顺序写入。
  2. 只有当 ICW1 中的 SNGL 位为 0 时,这表示级联,级联就需要设置主片和从片,这才需要在主片和从片中各写入 ICW3。注意,ICW3 的格式在主片和从片中是不同的。
  3. 只能当 ICW1 中的 IC4 为 1 时,才需要写入 ICW4。不过,x86 系统 IC4 必须为 1。
    总结:在 x86 系统中,对于初始化级联 8259A,4 个 ICW 都需要。初始化单片 8259A,ICW3不要,其余全要。

7.6 编写中断处理程序

在这里插入图片描述
init_all 初始化所有设备和数据结构
idt_init 初始化中断相关
pic_init 初始化可编程中断控制器 8259A
ide_desc_init:最底层核心的部分,填充中断处理程序的地址到 IDT 中.

1. 用编程语言实现中断处理程序

Macro 汇编中定义多行的宏

%macro 宏名字参数个数
宏代码体
%endmacro

使用时候带入的参数,用%数字来引用就像是C中的#define宏一样:
例如:

%macro mul_add 3
mov eax,%1
add eax,%2
add eax,%3
%endmacro

用这个方法调用:mul_add 45, 24, 33,带入其中%1是45,%2是24,%3是33
kernel/kernel.asm

[bits 32]
%define ERROR_CODE nop
%define ZERO	push 0

extern put_str

section .data
intr_str db "interrupt occur!", 0xa, 0
global intr_entry_table
intr_entry_table:

%macro VECTOR	2
section .text
intr%lentry:
	%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
section .data
	dd	intr%1entry
%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,ZERO
VECTOR 0x09,ZERO
VECTOR 0x0a,ZERO
VECTOR 0x0b,ZERO
VECTOR 0x0c,ZERO
VECTOR 0x0d,ZERO
VECTOR 0x0e,ZERO
VECTOR 0x0f,ZERO
VECTOR 0x10,ZERO
VECTOR 0x11,ZERO
VECTOR 0x12,ZERO
VECTOR 0x13,ZERO
VECTOR 0x14,ZERO
VECTOR 0x15,ZERO
VECTOR 0x16,ZERO
VECTOR 0x17,ZERO
VECTOR 0x18,ZERO
VECTOR 0x19,ZERO
VECTOR 0x1a,ZERO
VECTOR 0x1b,ZERO
VECTOR 0x1c,ZERO
VECTOR 0x1d,ZERO
VECTOR 0x1e,ERROR_CODE
VECTOR 0x1f,ZERO
VECTOR 0x20,ZERO

编译器会将属性相同的section合并到同一个大的segment中。
2. 创建中断描述符表 IDT,安装中断处理程序
kernel/interrupt.c

#include "interrupt.h"
#include "stdint.h"
#include "global.h"

//略

#define IDT_DESC_CNT	0x21	//支持的中断数目

// 中断门描述符结构体
struct gate_desc{
    uint16_t    func_offset_low_word;
    uint16_t    selector;
    uint8_t     dcount;
    uint8_t     attribute;
    uint16_t    func_offset_high_word;
};

//静态函数声明 非必须
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 是中断描述符表
// 声明引用定义在 kernel.S 中的中断处理函数入口数组
extern intr_handler intr_entry_table[IDT_DESC_CNT]; 

//略

/*创建中断门描述符*/
static void make_idt_desc(struct gate_desc* p_gdesc, uint8_t attr, intr_handler function) { 
    p_gdesc->func_offset_low_word = (int32_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");
 }

kernel/global.h

#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

3.用内联汇编实现端口 I/O 函数
lib/kernel/io.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){
    /*对端口指定 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 %wl, %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

4. 设置8259A
kernel/interrupt.c

//略
#include "io.h"
#include "interrupt.h"
#include "stdint.h"
#include "global.h"
//略
#define IDT_DESC_CNT	0x21	//支持的中断数目
#define PIC_M_CTRL  0x20    // 主片的控制端口是 0x20 
#define PIC_M_DATA  0x21    // 主片的数据端口是 0x21
#define PIC_S_CTRL  0xa0    // 从片的控制端口是 0xa0
#define PIC_S_DATA  0xa1    // 从片的数据端口是 0xa1
//略
// 中断门描述符结构体
struct gate_desc{
    uint16_t    func_offset_low_word;
    uint16_t    selector;
    uint8_t     dcount;
    uint8_t     attribute;
    uint16_t    func_offset_high_word;
};

//静态函数声明 非必须
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 是中断描述符表
// 声明引用定义在 kernel.S 中的中断处理函数入口数组
extern intr_handler intr_entry_table[IDT_DESC_CNT]; 

/*初始化可编程中断处理器 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); //1111 1110
    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 = (int32_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");
 }

kerne/init.c

#include "init.h"
#include "print.h"
#include "interrupt.h"

/*负责初始化所有模块*/
void init_all()
{
    put_str("init_all\n");
    idt_init(); //初始化中断
}


kernl/main.c

#include "print.h"
#include "init.h"
int main(void){
    put_str("I am kernel\n");
    
    init_all();
    asm volatile("sti");

    while(1);
    return 0;
}

另外把那几个 .h 文件补充一下

新的 Makefile

loader.bin:loader.asm
	nasm -I inc/ -o loader.bin loader.asm

mbr.bin:mbr.asm
	nasm -I inc/ -o mbr.bin mbr.asm

kernel.bin:kernel/main.c lib/kernel/print.asm kernel/kernel.asm kernel/interrupt.c kernel/init.c
	clang -I lib/kernel/ -I lib/ -I kernel/ -m32 -s -w -c -fno-builtin -o build/main.o kernel/main.c
	nasm -f elf -o build/print.o lib/kernel/print.asm
	nasm -f elf -o build/kernel.o kernel/kernel.asm
	clang -I lib/kernel/ -I lib/ -I kernel/ -m32 -s -w -c -fno-builtin -o build/interrupt.o kernel/interrupt.c
	clang -I lib/kernel/ -I lib/ -I kernel/ -m32 -s -w -c -fno-builtin -o build/init.o kernel/init.c

	ld -m elf_i386 -Ttext 0xc0001500 -e main -o build/kernel.bin build/main.o build/init.o build/interrupt.o build/print.o build/kernel.o


dd: dd_mbr dd_loader dd_kernel
dd_mbr:
	dd if=mbr.bin of=hd60M.img bs=512 count=1 conv=notrunc

dd_loader:
	dd if=loader.bin of=hd60M.img bs=512 count=4 seek=2 conv=notrunc

dd_kernel:
	dd if=build/kernel.bin of=hd60M.img bs=512 count=200 seek=9 conv=notrunc

运行结果:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

7.6.2改进中断处理程序

kernel/interrupt.c

//略
#include "io.h"
#include "interrupt.h"
#include "stdint.h"
#include "global.h"
//略
#define IDT_DESC_CNT	0x21	//支持的中断数目
#define PIC_M_CTRL  0x20    // 主片的控制端口是 0x20 
#define PIC_M_DATA  0x21    // 主片的数据端口是 0x21
#define PIC_S_CTRL  0xa0    // 从片的控制端口是 0xa0
#define PIC_S_DATA  0xa1    // 从片的数据端口是 0xa1
//略
// 中断门描述符结构体
struct gate_desc{
    uint16_t    func_offset_low_word;
    uint16_t    selector;
    uint8_t     dcount;
    uint8_t     attribute;
    uint16_t    func_offset_high_word;
};

//静态函数声明 非必须
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 是中断描述符表
// 声明引用定义在 kernel.S 中的中断处理函数入口数组
extern intr_handler intr_entry_table[IDT_DESC_CNT]; 
char *intr_name[IDT_DESC_CNT];  //保存异常的名字
/*定义中断处理程序数组,在kernel.asm中定义的intrXXentry
    只是中断处理程序的入口,最终调用的是 ide_table 中的处理程序*/
intr_handler idt_table[IDT_DESC_CNT];

/*初始化可编程中断处理器 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); //1111 1110
    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 = (int32_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){
        //IRQ7和IRQ5会产生伪中断,无需处理
        //0x2f是从片 8259A 上的最后一个 IRQ 引脚,保留项
        return;
    }
    put_str("int vector : 0x");
    put_int(vec_nr);
    put_char(' ');
    put_str(intr_name[vec_nr]);
    put_char('\n');
 }

/*完成一般中断处理函数注册及异常名称注册*/
static void exception_init(void){
    int i;
    for(i = 0; i < IDT_DESC_CNT; ++i){
        /*idt_table中的函数是在进入中断后根据中断向量好调用的
        见kernel/kernel.asm 的 call[idt_table + %1*4]*/
        idt_table[i] = general_intr_handler;
        //默认为general_intr_handler
        //以后会有 register_handler注册具体的处理函数
        intr_name[i] = "unknown";   //先统一为 "unknown"
    }
    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";
    intr_name[0x20] = "#CLOCK";

}

 /*完成有关中断的所有初始化工作*/
 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");
 }

kernel/kernel.asm

[bits 32]
%define ERROR_CODE nop
%define ZERO	push 0

extern put_str
extern idt_table

section .data
intr_str db "interrupt occur!", 0xa, 0
global intr_entry_table
intr_entry_table:

%macro VECTOR 2
section .text
intr%1entry:
	%2
	push ds
	push es
	push fs 
	push gs 
	pushad 
	;如果是从从片上进入的中断,除了往从片发送EOI,主片也发送EOI
	mov al,0x20	;中断结束命令EOI
	out 0xa0,al ;向从片发送
	out 0x20,al ;向主片发送
	push %1
	call [idt_table + %1 * 4];调用idt_table中的C版本中断处理函数
	jmp intr_exit
section .data
	dd	intr%1entry
%endmacro

section .text 
global intr_exit
intr_exit:
	add esp,4;跳过中断号码
	popad 
	pop gs 
	pop fs 
	pop es 
	pop ds 
	add esp,4
	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,ZERO
VECTOR 0x09,ZERO
VECTOR 0x0a,ZERO
VECTOR 0x0b,ZERO
VECTOR 0x0c,ZERO
VECTOR 0x0d,ZERO
VECTOR 0x0e,ZERO
VECTOR 0x0f,ZERO
VECTOR 0x10,ZERO
VECTOR 0x11,ZERO
VECTOR 0x12,ZERO
VECTOR 0x13,ZERO
VECTOR 0x14,ZERO
VECTOR 0x15,ZERO
VECTOR 0x16,ZERO
VECTOR 0x17,ZERO
VECTOR 0x18,ZERO
VECTOR 0x19,ZERO
VECTOR 0x1a,ZERO
VECTOR 0x1b,ZERO
VECTOR 0x1c,ZERO
VECTOR 0x1d,ZERO
VECTOR 0x1e,ERROR_CODE
VECTOR 0x1f,ZERO
VECTOR 0x20,ZERO

运行的还是 intr_entry_table,但 intr_entry_table 调用了 ldt_table。

时钟中断运行结果:

时钟中断运行结果

7.7 可编程计时器 / 定时器 8253 简介

7.7.1 时钟——给设备打拍子

计算机中的时钟可分为:内部时钟、外部时钟。

内部时钟是指处理器中内部元件,如运算器、控制器的工作时序,主要用于控制、同步内部工作过程的步调。

内部时钟是由晶体振荡器产生的,简称晶振,它位于主板上,其频率经过分频之后就是主板的外频,处理器和南北桥之间的通信就基于外频。就是外部时钟

Intel 处理器将此外频乘以某个倍数(也称为倍频)之后为主频。

内部时钟通常都是纳秒(ns)级的。
外部时钟是指处理器与外部设备或外部设备之间通信时采用的一种时序。一般是毫秒(ms)级或秒(s)级的。

处理器的内部时钟信号由晶振产生,故计时精准稳定。但晶振产生的信号频率过高,因此必须将其送到定时计数器分频,这才能产生所需要的各种定时信号。
对于外部定时,我们有两种实现方式。一种是用软件实现,比如以下代码:

int cycle_cnt = 90000;
while(cycle_cnt -- > 0);

就是让其空转。
另外一种是用硬件实现,这一类硬件称为定时器。定时发信号。

常用的可编程定时计数器有 Intel 8253/8254/82C54A 等,后两是 8253 的加强版和更强版(我自己随意说的词,反正你懂的)。由于我们只用到最基础的东西,所以咱们只介绍 8253 的用法。

硬件定时器一般有两种计时的方式
(1)正计时:每一次时钟脉冲发生时,将当前计数值加 1,直到与设定的目标终止值相等时,提示时间已到,典型的例子就是闹钟。
(2)倒计时:先设定好计数器的值,每一次时钟脉冲发生时将计数值减 1,直到为 0 时提示时间已到,典型的例子就是电风扇的定时

7.7.2 8253入门

8253计时器,他的价值在于计数方面。
三个计数器,端口号0x40-0x42,它们不相互依赖,可以同时工作,各干各的。各自有寄存器资源设备,16为的计数初值寄存器,一个计数器执行部件和一个输出锁存器。
在这里插入图片描述

  1. CLK时钟输入信号
  2. GATE 门控输入信号
  3. OUT 计数器输出信号,用来通知处理器或某个设备。

计数初值寄存器、计数器执行部件和输出锁存器都是 16 位宽度的寄存器,所以高 8 位和低 8 位都可以单独访问。我们之后为其赋予初始计数值时就分为高 8 位和低8 位分别操作

在这里插入图片描述

7.7.3 8253 控制字

在图 7-45 中左下角是控制字寄存器,其操作端口是 0x43,8位。设置所指定的计数器(通道)的工作方式、读写格式及数制。
在这里插入图片描述

7.7.4 8253 的工作方式

这里可以看看书上的讲解
在这里插入图片描述
在这里插入图片描述

7.75 8253 初始化步骤

让 8253 开始工作的方法很简单,只要我们通过控制字选择用哪个计数器,指定该计数器的控制模式,再为该计数器写入计数初值就行了
1.往控制字寄存器端口 0x43 中写入控制字
2.在所指定使用的计数器端口中写入计数初值

7.8 提高时钟中断的频率

它默认的频率是 18.206Hz,即一秒内大约发出 18 次中断信号。我们嫌它有点慢,本节我们将对 8253 编程,使时钟一秒内发 100 次中断信号,即中断信号频率为 100Hz。

  • IRQ0 引脚上的时钟中断信号频率是由 8253 的计数器 0 设置的,我们要使用计数器 0
  • 时钟发出的中断信号不能只发一次,必须是周期性发出的,也就是我们要采取循环计数的工作方式,
    可选的工作方式为方式 2 和方式 3,这里咱们就选择方式 2,这是标准的分频方式
  • 计数器发出输出信号的频率是由计数初值决定的,所以我们要为计数器0 赋予合适的计数初值

公式:1193180/中断信号的频率=计数器 0 的初始计数值
计数器0的初始计数值 = 1193180/100 约等于 11932

device/timer.c


#include "timer.h"
#include "io.h"
#include "print.h"

#define IRQ0_FREQUENCY      100
#define INPUT_FREQUENCY     1193180
#define COUNTER0_VALUE      INPUT_FREQUENCY / IRQ0_FREQUENCY
#define CONTRER0_PORT       0x40
#define COUNTER0_NO         0
#define COUNTER_MODE        2
#define READ_WRITE_LATCH    3  
#define PIT_CONTROL_PORT    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");
}

device/timer.h

#ifndef __TIMER_H
#define __TIMER_H

void timer_init();

#endif

kernel/init.c

#include "init.h"
#include "print.h"
#include "interrupt.h"
#include "../device/timer.h"

/*负责初始化所有模块*/
void init_all()
{
    put_str("init_all\n");
    idt_init(); //初始化中断
    timer_init();
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值