《操作系统真想还原》第七章 中断

中断是什么,为什么要有中断

通俗的讲,中断就是打断正在做的事。
在计算机中的中断是由于CPU获知了计算机中发生的某种事,CPU暂停正在执行的程序,转而去执行处理该事件的程序,当这段程序执行完毕后,CPU继续执行刚才的程序。整个过程称为中断处理,也称为中断。
中断虽然是打断的意思,但它恰恰却是提升整个系统利用率最有效的方式,没有之一,因为有了中断,系统才能并发运行。

中断分类

外部中断

一般指CPU外部硬件传来的中断,由于中断源必定为硬件,所以也被称为硬件中断,而我们CPU为了知道外面有人需要中断,所以我们需要一个中间人,这里就存在两个中间人,也就是两根信号线,他们分别是INTR和NMI
在这里插入图片描述
上图中给出了两种信号线的区别,其中INTR收到的信号都是不影响系统运行的,可以随时处理,但是NMI收到的信号是必须要立刻处理,NMI收到信号后,一切其他的工作都失去了意义,先把这个中断处理了才是重中之重。
首先我们先看看可屏蔽中断,可屏蔽中断是指由INTR线传递的中断,外部设备如硬盘,网卡等发出的中断都是可屏蔽中断。可屏蔽的意思是此外部设备发出的中断,CPU可以不理会,因为他不会让系统宕机,所以可以通过eflags寄存器中的IF位将所有这些外部设备的中断屏蔽,另外,这些设备都是接在某个中断过滤设备的,通过该中断过滤也可以单独屏蔽某个设备的中断。
然后我们介绍不可屏蔽中断,它是由NMI线传递的中断信号,只要是这里传递了中断,说明计算机就是遭到了严重的问题,必须立刻处理。此时上述的eflags寄存器的IF位对它毫无影响。
CPU收到中断后,得知道法神了什么事情才能执行相应的处理方法。着是通过中断向量表和中断描述符表来实现的,首先为每一种中断分配一个中断向量号,中断向量号就是一个整数,他就是中断向量表或中断描述符表中的索引下标,用来索引中断项。中断发起时,相应的中断向量号通过NMI和INTR引脚被传入CPU,中断向量号是中断向量表或者中断描述符表里中断项的下标,CPU根据此中断向量号在中断向量表或者中断描述符表中检索对应的中断处理程序并去执行。

外部中断

内部中断又分为软中断和异常

软中断

是由软件主动发起的中断,因为它来自软件,所以称之为软中断。由于该中断是软件运行中主动发起的,所以它是主观上的,并不是客观上的某种内部错误。
下面就是咱们可以发起中断的指令:
1.int (8位数据):8位可表示256种中断,这条指令在之后我们会经常使用
2.int3 :调试断点指令,它所出发的中断向量号是3,当我们使用gdb或bochs进行调试时,实际上就是调试器fork了一个子进程。调试器中设置断点实际上就是父进程修改子进程,将其用int3指令替换,从而实现中断使得咱们可以停在断点,
3.into :这是中断溢出指令,它所触发的中断向量号是4。不过,能否引发4号中断是要看 eflags志寄存器中的 OF 位是否为1,如果是1才会引发中断,否则该指令悄悄地什么都不做,低调得很
4.bound :这是检查数组索引越界指令,它可以触发5号中断,用于检查数组的索引下标是否在上下边界之内。该指令格式是“bound 16/32 位寄存器, 16/32 位内存飞目的操作数是用寄存器来存储的,其内容是待检测的数组下标值。源操作数是内存,其内容是数组下标的下边界和上边界 当执行 bound 指令时,若下标处于数组索引的范围之外,则会触发5号中断。
5.ud2 :触发6号中断,无实际用途
而异常是我们程序运行时出现的错误,他同样不受eflags寄存器中的标志位的影响,也就是说不可屏蔽,因为都出现错误了计算机说想看不见也不行,异常可以分为以下3种:

1.Fault,故障,可修复,例如缺页异常
2.Trap,陷阱,自己想陷入中断,所以中断返回后执行下一条指令。
3.Abort, 终止,无法修复,操作系统为求自保,只能把该程序从进程表移除
在这里插入图片描述
其中我们的中断向量号的作用类似于选择子,都是某个表的下标

中断描述符表

中断描述符表是保护模式下存放中断处理程序入口的一个表,这里注意同实模式下的中断表区分开。
中断描述符表中不止有中断描述符,还有任务门描述符和陷阱们描述符。而由于所有的描述符都指向了一段程序,这里就体现出来他与GDT的不同,所以在这里的描述符有个另外的名字,那就是门。
所有的门都是8字节的,这里我们想一想前面描述符中的字段类型,type字段指明了该描述符的类型,其中的S位若为1则说明该段为数据段,为0则表示为系统段,咱们这里的门就属于系统段。
我们看看之前门的描述符结构
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述在这里插入图片描述
同时给出之前段描述符的概念结构:
在这里插入图片描述
可以看出这里的结构都是类似,只不过有的不同位的功能发生了变化,这也导致了我们的门可以正常放在段描述表或者中断描述表中了
说一下各门的区别:
1.任务门:配合TSS使用实现特权级的切换,可存放在GDT,LDT,IDT中,描述符中任务门的type字段二进制为0101
2.中断门:包含中断处理程序所在段的段选择子以及偏移,当通过此方式进入中断之后,eflags寄存器的IF位自动置0,也就是关中断,防止中断嵌套。Linux就是使用中断门实现系统调用,也就是著名的int 0x80。中断门只允许存放在IDT中。描述符中中断门的type字段二进制为1110
3.陷阱门:类似于中断门,区别就是IF位不会置0,只允许存放在IDT中,陷阱门的type字段二进制为1111
4.调用门:提供用户进程进入特权0级,其DPL为3,只能用call或jmp指令调用,可以安装在GDT和LDT中,type值为1100
既然内存中应该存在一个IDT供我们使用,所以我们寻找他的方式类似与GDT,也就是说有一个寄存器来存放IDT的物理地址,这个寄存器就是IDTR,下面是IDTR的结构:
在这里插入图片描述
其中低16位代表段界限,高32位代表IDT物理地址,16位的表界限可以表示2^16B,也就是64KB,而一个门占8字节,所以一共可以存放64KB/8B = 8192个,这里注意虽然GDT第0表项为全0不可用,但IDT却无此限制,中断向量为0表示除法错。
同加载GDTR一样,加载IDTR也有个专门的指令–lidt,其用法是 lidt 48位内存数据

中断的处理过程及保护

中断的过程分为CPU外的和CPU内两部分
CPU外:外部设备的中断是由中断过滤芯片接收,处理后将该中断向量号发送到CPU
CPU内:CPU执行该中断向量号对应的中断处理程序
我们先看处理器内部的内容
1.处理器根据中断向量号定位中断门描述符
2.处理器进行特权级检查
由于中断是通过中断向量号通知到处理器的,中断向量号只是各整数,其中并不包含RPL,所以在对由中断引起的特权级转移做特权级做特权级检查中不会涉及RPL。中断门的特全级检查同调用门类似,对于软件主动发起的软中断,当前特权级CPL必须在门描述符和目标代码段DPL之间。
①.若是由软中断int n,int3,into引发的中断,这些是由用户自主发起的,所以处理器要检查当前特权级和门描述符DPL,这是检查进门的特权下线,若是检查通过,也就是CPL特权级是高于门DPL的,那么将进入下一步“门框”的检查,否则处理器将会报出异常
②.这一步检查特权级的上限“门框”,处理器要检查当前特权级CPL和门描述符中所记录选择子对应的目标代码段DPL,若CPL特权级小于目标代码段的DPL,则检查通过,否则处理器引发异常。
③.若中断是由外部设备和异常引起的,则只检查CPL和目标代码段的DPL,若CPL小于目标代码段特权,则检查通过,否则处理器引发异常
3.执行中断处理程序
特权级检查通过后,将门描述符目标代码段选择子加载到代码段寄存器CS中,将门描述符中的偏移地址加载至EIP中,然后执行中断处理程序
过程如下图
在这里插入图片描述
中断发生后,eflags中的NT位和TF位会被置0,若中断对应的是中断门,则在进入中断门后eflags的IF位会自动置0以此来防止中断嵌套,但是我们依然可以在中断处理中将IF位打开,我们先前说过修改eflags寄存器的内容只能通过pushf压栈然后恢复栈来修改,但此关系到内存访问了,效率想必十分低效的,所以处理器专门提供了一个可以修改IF位的指令。那就是cli和sti,其中cli指令使得IF位为0,sti指令使得IF位为1,分别称为关中断和开中断。
IF位只限制外部设备中断,而对其他影响系统正常运行的中断无效。
这里我们解释IF,即Trap Flag,也就是陷阱标志位,这用在调试环境中,当TF位0表示禁止单步执行。
而NT表示Nest Task Flag,即任务嵌套位,用来标记任务嵌套调用的情况。任务嵌套调用就是指CPU挂起当前的任务转而去执行另一个任务,待到该任务执行完再转回去执行之前的任务,而CPU能如此是因为他会执行以下操作:
1.将旧任务的TSS段选择字写到新任务TSS的“上一个任务TSS的指针”字段中
2.将新任务eflags寄存器中NT置为1,表示新任务之所以能够执行是因为有别的任务调用了他
而当CPU从新任务返回到旧任务是通过iret指令,他有两个功能,一个是中断返回,一个是返回旧任务,所以这里就需要用到NT位,因为执行iret的是时候会去检查NT位的值,若位1则说明当前任务是嵌套执行的,若为0则说明实在中断处理环境下,与是执行正常的中断退出流程。

中断压栈

中断发生时,处理器收到一个中断向量,根据该中断向量号在IDT中的偏移,然后找到对应的门然后通过其中的选择子,然后将该选择子移到CS中,再将门描述符中的偏移字段移入EIP。这时由于CS和EIP会被刷新,所以处理器会将被中断的程序中的CS和EIP保存到当前中断处理程序使用的栈当中,至于说中断处理用的哪个栈,这里不好说,因为中断在任何特权级下都有可能发生,所以我们除了保存CS,EIP外还需要保存eflags,如果涉及到特权级的变化还要压入SS和EIP寄存器,下面介绍寄存器入栈情况以及顺序:
1.当处理器通过中断向量找到对应的中断描述符后,比较CPL和中断门描述符中选择子对应目标代码段的DPL对比,若发现向3高特权级转移,则需要切换到高特权级的栈,这也意味着当我们执行完中断处理程序后需要恢复到旧栈。因此处理器先临时保存旧SS和EIP的值,我们就记作SS_old和EIP_old,然后在TSS中寻找到对应的目标代码段同特权级的栈加载到寄存器SS和EIP中,记作SS_new和EIP_new,再将临时保存的SS_old和EIP_old压栈备份;
在这里插入图片描述
2.然后压入EFLAGS寄存器
在这里插入图片描述
3.然后因为需要切换代码段,所以也要将CS和EIP保存到栈中进行备份,用以在中断结束后恢复被中断的进程,如下图:在这里插入图片描述
4.某些异常会爆出错误码,这个错误码是用于报告一场是在哪个段上发生的,也就是发生异常的位置,所以错误码中包含选择子等信息。他一般紧跟EIP后入栈,记为ERROR_CODE.如下图:在这里插入图片描述
处理器执行完中断处理程序后需要返回到被中断进程,也就是使用iret指令进行弹站,这里需要保证上述顺序。如果说有中断错误码,处理器并不知晓,所以这需要我们手动将其跳过,也就是说当我们准备用iret指令返回时当前栈指针必须得指向栈中备份的EIP_old所在的位置,这样才能依次对号入座。

中断错误码

错误码用来指明中断发生在哪个段上,所以说错误码最主要的部分是选择子,这里给出错误码的结构:
在这里插入图片描述
可以看出和选择子有些相似
1.EXT表示EXTernal event,即外部事件,用来指明中断源是否来自处理器内部,如果中断源是不可屏蔽中断NMI或外部设备,EXT为1,否则为0。
2.IDT表示选择子是否指向中断描述符表IDT,IDT位为1则表示此选择子指向中断描述符表,否则指向GDT或者是IDT
其中TI和选择子TI是一致的,为0知指选择子是从GDT中检索描述符,为1是从LDT中检索,而这个位起效的前提是IDT为0。
通常能够压入错误码的中断属于中断向量号在0~32之内的异常,而外部33~255之间和int软中断通常不会产生错误码,因此也不会产生错误码。

中断处理程序实战

这里我们分几块来分别处理,首先就是init_all(),这函数咱们用来初始化所有设备和数据结构,这里他会首先调用idt_init(),他用来初始化中断相关内容,这一步咱们也分一下,那就是先执行pic_init(),他用来初始化可编程中断控制器8259A,然后idt_desc_init()来初始化中断描述符表IDT。
这里我们首先定义kernel.S,存放在kernel目录下:

;------ kernel/kernel.S ----------
[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             ;这里定义一个多行宏,后面的2是指传递两个参数,里面的%1,%2代表参数,类似linux shell脚本
section .text
intr%1entry:               ;每个中断处理程序都要压入中断向量号,所以一个中断类型一个中断处理程序,自己知道自己的中断向量号是多少
  %2                        ;手工压入32位数据或者空操作,保证最后都会需要跨过一个4字节来进行iret
  push intr_str 
  call put_str
  add esp, 4                ;清理栈空间

  ;如果是从片上进入的中断,除了往片上发送EOI外,还要往主片上发送EOI
  mov al, 0x20              ;中断结束命令EOI,这里R为0,SL为0,EOI为1
  out 0xa0, al              ;向从片发送,这里端口号可以使用dx或者立即数
  out 0x20, al              ;向主片发送

  add esp, 4                ;跨过error_code
  iret                      ;从中断返回

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,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

这里我们定义了一个中断处理程序的宏,也就是VECTOR 参数1,参数2,我们先来测试测试,所以所有的中断处理程序的功能就一个,打印一段字符串。然后我们需要将这些中断处理程序安装到中断描述表当中。
于是我们再编写一个interrupt.c,存放在kernel目录下,interrupt.c的功能就是初始化IDT描述表和IDT描述符,简单来讲就是准备中断相关数据而已。
现在我们在kernel目录下创建一个global.h用来定义一些门描述符的类型。

//  "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 TL_GDT 0
#define TL_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位的门,不会用到
#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

目前我们的代码实现了一些初始化中断描述符以及表,所以我们还需要设置好中断过滤8259A,这里我们使用内联汇编来写.

// lib/kernel/io.h
/*************** 机器模式 ******************/
#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,%wl表示对应dx 
***************************************************/
  asm volatile("outb %b0, %w1" :: "a"(data)."Nd"(port));    //input为data,port,约束分别为al寄存器和dx
/**************************************************/
}

/* 将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), "+cx"(word_cnt) : "d"(port)); //output为addr和word_cnt,约束分别为si和cx,input包含前两位和后面的port,约束为dx
/**************************************************/
}

/* 将从端口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指向的内存
***************************************************/
  asm volatile("cld; rep insw" : "+D"(addr),"+c"(word_cnt) : "d"(port) : "memory");
/**************************************************/
}

#endif

上面的内容很简单,也就是简单的定义了几个端口输入输出函数而已,之后我们就要正式开始对8259A进行编程了.这里说是编程,其实就是往对应端口写入控制指令字而已。
这里给出初始化8259A的部分代码:
这是包含在interrupt.c中的

/* 初始化可编程中断控制器 */
static void pic_init(void){
  /* 初始化主片 */
  outb(PIC_M_CTRL, 0x11);           //ICW1:边沿触发,级联8259,需要ICW4
  outb(PIC_M_DATA, 0x20);           //ICW2:起始中断向量号为0x20,也就是IRQ0的中断向量号为0x20
  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
  outb(PIC_S_DATA, 0x02);           //ICW3:设置从片连接到主片的IR2引脚
  outb(PIC_S_DATA, 0x01);           //ICW4:同上

  /* 打开主片上的IR0,也就是目前只接受时钟产生的中断 */
  outb(PIC_M_DATA, 0xfe);           //OCW1:IRQ0外全部屏蔽
  outb(PIC_S_DATA, 0xff);           //OCW1:IRQ8~15全部屏蔽

  put_str(" pic init done!\n");
}

这里来总结一下,我们目前所完成的工作有:
1.编写kernel.S,里面包含一个中断处理程序宏,且目前所有中断程序都一样,都是输出一个字符串
2.编写interrupt.c,里面我们初始化了IDT,以及门描述符
3.编写io.h,里面包含端口写数据的一些函数,使用了内联汇编
4.完善inaterrupt.c,里面我们初始化了8259A,且目前只支持时钟中断
现在继续我们的工作,我们需要加载IDT,跟我们之前加载GDT类似,也就是将IDT的地址存入IDTR而已,这里我们需要使用指令lidt,为了避免麻烦我们继续采用内联汇编实现

  /* 加载idt */
  UINT64_t idt_operand = (sizeof(idt)-1) | ((uint64_t)((uint32_t)idt << 16));   //这里(sizeof(idt)-1)是表示段界限,占16位,然后我们的idt地址左移16位表示高32位,表示idt首地址
  asm volatile("lidt %0" : : "m" (idt_operand));

这里也是我们修改的interrupt.c
到此为止,我们的一切准备工作已经就绪,现在我们再将其全部封装起来,在kernel目录下创建一个init.c文件

//kernel/init.c
#include "init.h"
#include "print.h"
#include "interrupt.h"

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

之后我们修改我们的main.c函数就可以了

#include "print.h"
#include "init.h"
void main(void){
  put_str("I am Kernel\n");
  init_all();
  asm volatile("sti");  //为演示中断处理,在此临时开中断
  while(1);
}

我们依次进行编译链接

gcc -no-pie -fno-pic -fno-stack-protector -m32 -I lib/kernel/ -I lib/ -I kernel/ -c -fno-builtin -o build/main.o kernel/main.c \
nasm -f elf -o build/print.o kernel/print.S \
nasm -f elf -o build/kernel.o kernel/kernel.S \
gcc -no-pie -fno-pic -m32 -fno-stack-protector -I lib/kernel/ -I lib/ -I kernel/ -c -fno-builtin -o build/interrupt.o kernel/interrupt.c \
gcc -no-pie -fno-pic -m32 -fno-stack-protector -I lib/kernel/ -I lib/ -I kernel/ -c -fno-builtin -o build/init.o kernel/init.c \
ld -m elf_i386 -T kernel/link.script -Ttext 0xc0001500 -e main -o build/kernel.bin  build/main.o build/init.o build/interrupt.o build/print.o build/kernel.o \
dd if=./build/kernel.bin of=./bochs/hd60M.img bs=512 count=200 seek=9 conv=notrunc

然后我们到bochs里面运行一下试试看,效果如下图:
在这里插入图片描述
可以看到时钟每次发送中断信号,我们的中断处理程序都会输出一个字符串,我们也可以在bochs中调试命令info idt来查看idt的信息,如下图:在这里插入图片描述

改进中断程序

中断处理程序简单的不能再简单,所以这里我们是来写处理程序的,这里我们采用C语言编写然后汇编代码调用C语言函数即可。
我们首先需要在C函数中声明出一个数组idt_table,这里面存放我们所需要的中断程序地址,因此一个元素占有4字节,而我们中断程序是按照中断向量号区分的。那么该如何找到对应的程序地址呢,这里只需要进行一个简单的计算,由于中断处理程序的地址在idt_table中占4字节,所以中断程序地址 = idt_table地址+中断向量号×4,简单的计算。
本次修改我们需要修改两个文件,那就是interrupt.ckernel.S,
这里贴出关键代码,首先是修改的interrupt.c部分

/* 通用的中断处理函数,一般用在出现异常的时候处理 */
static void general_intr_handler(uint64_t vec_nr){
  /* IRQ7和IRQ15会产生伪中断,IRQ15是从片上最后一个引脚,保留项,这俩都不需要处理 */
  if(vec_nr == 0x27 || vec_nr == 0x2f){
    return;
  }
  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数组中的函数是在进入中断后根据中断向量号调用的
 * */
    idt_table[i] = general_intr_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]是保留项,未使用
  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";
}

然后修改kernel.S部分

%macro VECTOR 2             ;这里定义一个多行宏,后面的2是指传递两个参数,里面的%1,%2代表参数,类似linux shell脚本
section .text
intr%1entry:               ;每个中断处理程序都要压入中断向量号,所以一个中断类型一个中断处理程序,自己知道自己的中断向量号是多少
  %2                        ;手工压入32位数据或者空操作,保证最后都会需要跨过一个4字节来进行iret
  push ds
  push es
  push fs
  push gs
  pushad                    ;压入EAX,ECX,EDX,EBX,ESP,EBP,ESI,EDI

  ;如果是从片上进入的中断,除了往片上发送EOI外,还要往主片上发送EOI
  mov al, 0x20              ;中断结束命令EOI,这里R为0,SL为0,EOI为1
  out 0xa0, al              ;向从片发送,这里端口号可以使用dx或者立即数,OCW2
  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或者说我们自己压入的0
  iretd

这里再给出压栈情况,注意这里无特权变换
在这里插入图片描述
像上次一样编译看结果
在这里插入图片描述

定时器

上面我们已经成功实现了中断,但是本节还没完,我们要知道上面是我们实现的中断处理程序,但是这个中断是谁发出的呢,没错就是咱们的标题定时器,他也有个名字叫做计数器。
他的存在是为了缓和内部时钟和外部时钟,解决处理器和外部设备同步数据时的时序配合问题,其大致思路就是计时器定时发信号,用该信号向处理器发送中断,这样处理器就会去执行相应的中断处理程序。
这里我们介绍一个叫做8253的定时器。而硬件计时器一般有两种计时的方式:
正计时:每一次时钟脉冲发生,将当前计数值加一,直到与设定值相等,类似与闹钟。
倒计时:先设定好计数器的值,每次时钟脉冲发生就将计数减一,直到为0,类似于秒表定时
我们先来看看8253内部结构:
在这里插入图片描述
8253中有3个独立的计数器,分别0~3,他们对应的端口号分别是0x40~0x42,且都是16位大小。下图是独立一个计数器的结构图:在这里插入图片描述
计数器本质上是一个减法器,因为咱们是倒计时嘛。每个计数器有三个引脚:
CLK,时钟输入信号,这里是指计数器自己的工作节拍,每收到一个这样的信号,计数器的值就减1
GATE,门控制输入信号,后面介绍
OUT,计数器输出信号,定时工作结束之后,会从该引脚发出信号来说明定时完成,这样处理器或外部设备就可以执行相应的动作
然后我们依次来介绍计数器中的寄存器们:
计数初值寄存器,占16位,根据名字即可理解,他是存放的我们对8253初始化时写入的计数初始值。
计数器执行部件,实际上为16位减法器,它将初值寄存器的值拿下来不断原地减1。
输出锁存寄存器,用于把当前减法计数器中的计数值保存下来

8283中的三个计数器都有自己独特的使命
在这里插入图片描述
我们这里只需要了解计数器0,他的主要作用是产生时钟信号,这个时钟连接到主片IRQ0引脚上,也就是说计数器0决定了时钟中断信号的频率。在这里插入图片描述
这里依次介绍相关字段
SC1和SC0:如图,表示计数器0~3
RW1和RW2:如图
M2~M0:之后详解
BCD:也就是BCD码,用4位二进制来表示十进制,这一位为0表示使用2进制计数,若为1表示使用BCD
上面的M2~M0是表示的工作方式,
在这里插入图片描述
所以计数器什么时候开始记时呢,有可能会认为是我们写入计数器初始值之后,但实际上不是如此,开始时机与工作方式相关,他需要两个条件:
1.GATE为高电平,即GATE为1,由硬件控制
2.计数器初值写入了减法器,这是由软件out指令来控制的

上面的条件根据哪个未完成来划分,分为软件启动和硬件启动:
1.条件1达成,现在只需写入计数初值。工作方式0,2,3,4都是用软件启动计数过程
2.条件2达成,现在只需要坐等GATE由0变成1的上升沿出现时,计数器才会开始计数,工作方式1,5都是用硬件启动计数过程

计数器是按上述满足两个条件启动的,而对于终止的话,根据不同的工作方式,分为强制终止和自动终止:
1.强制终止,计时到(为0)后,减法计数器会将计数初始值重新载入,继续下一轮计数,工作方式2和3都是采用这种技术方式,对于这种采用循环计数的方式,我们只能施加外力来结束他,不然他一直运行下去了,这里我们只需要将GATE置0,破坏其运行条件即可。
2.自动终止,工作方式0,1,4,5都是单次计数,也就是一轮之后就终止,此时也可以在过程中将GATE置0强制终止。

然后介绍3种工作方式
1.方式0:计数结束输出正跳变信号,在方式0下,会将该计数器通道的OUT变为低电平,这直到我们的计数值为0.技术工作由软件启动,也就是处理器用out指令将计数初值写入计数器,然后到计数器就开始减1,注意这里可能会有一个时钟脉冲延迟,因为计数器的工作是按照自己的时钟CLK来进行的。在之后CLK每收到一次脉冲信号,减法计数器就将计数值减一。
2.方式1:硬件可重触发单稳方式,触发信号是GATE,在方式1下,计数初值写入计数器后,OUT引脚变为高电平,但是注意这里GATE不论是高电平还是低电平他都不会启动计数,而是要等到GATE从低电平到高电平那个上升沿才会启动计数,此后在一个CLK下降沿开始计数,OUT变为低电平,之后CLK每收到一个时钟脉冲,减法器工作,直到计数值为0,此时OUT由低到高电平,产生正跳变
3.方式2:比率发生器:处理器把控制字写入到计数器当中,OUT变为高电平,GATE为高电平的前提下,处理器将计数值初值写入后,在下一个CLK时钟脉冲下沿,计数器开始计数,属于软件启动。当计数值为1时,OUT变为低电平,然后知道计数值为0,OUT又变为高电平,同时计数初值又会被载入减法器,这一过程不断重复。

最后的改进

首先我们梳理一下过程:

IRQ0引脚的时钟中断信号频率是由8253计数器0设置的,所以我们要使用计数器0
时钟中断信号肯定要循环,所以这里我们采用方式2
计数器初值我们采用11932,这是因为计数器0的工作频率是1.19318MHz,这里我们为了实现100Hz的中断信号,所以我们需要1.19318M/100,这样就得出了我们需要的初始值了。
本次我们构建一个device目录,然后目录下创建timer.c程序

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

#define IRQ0_FREQUENCY 100                      //咱们所期待的频率
#define INPUT_FREQUENCY 1193180                 //计数器平均CLK频率
#define COUNTRE0_VALUE  INPUT_FREQUENCY / IRQ0_FREQUENCY    //计数器初值
#define CONTRER0_PORT   0x40                    //计数器0的端口号
#define COUNTER0_NO     0                       //计数器0
#define COUNTER_MODE    2                       //方式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)); //计数器0,rwl高低都写,方式2,二进制表示
  /* 先写入counter_value的低8位 */
  outb(CONTRER0_PORT, (uint8_t)counter_value);
  /* 再写入counter_value的高8位 */
  outb(COUNTER_MODE, (uint8_t)counter_mode >> 8);
}

/* 初始化PIT8253 */
void timer_init(){
  put_str("timer_init start\n");
  /* 设置8253的定时周期,也就是发中断的周期 */
  frequency_set(CONTRER0_PORT, COUNTER0_NO, READ_WRITE_LATCH, COUNTER_MODE, COUNTRE0_VALUE);
  put_str("timer_init_done\n");
}

之后我们照常编译链接
在这里插入图片描述
他的频率被重新设置了,比以前快多了。

  • 15
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值