第七章 中断

第七章 中断

本文是对《操作系统真象还原》第七章学习的笔记,欢迎大家一起交流。

a 启用中断

本节的主要任务是打开中断,并且使用时钟中断测试

知识部分

中断分类

中断可以分为外部中断和内部中断,这已经是老生常谈的话题了,不再多说。

外部中断又可以分为可屏蔽中断和不可屏蔽中断,其中可屏蔽中断通过INTR信号线通知cpu,不可屏蔽中断通过NMI通知cpu,如下图所示:

image

中断上半部和下半部

操作系统是中断驱动的,中断发生后会执行相应的中断处理程序,我们希望 CPU 中断响应的时间越短越好,这样便能响应更多设备的中断。但是中断处理程序还是需要完整执行的,不能光为了提高中断响应效率而只执行部分中断处理程序 。 于是,把中断处理程序分为上半部和下半部两部分,把中断处理程序中需要立即执行的部分(分分钟不能耽误的部分)划分到上半部,这部分是要限时执行的,所以通常情况下只完成中断应答或硬件复位等重要紧迫的工作。而中断处理程序中那些不紧急的部分则被推迟到下半部中去完成。由于中断处理程序的上半部是刻不容缓要执行的,所以上半部是在关中断不被打扰的情况下执行的。当上半部执行完成后就把中断打开了,下半部也属于中断处理程序,所以中断处理程序下半部则是在开中断的情况下执行的,如果有新的中断发生,原来这个旧中断的下半部就会被换下 CPU,先执行新的中断处理程序的上半部,等待线程调度机制为旧中断处理程序择一 日期(就是指调度算法认为的某个恰当时机)后,再调度其上 CPU 完成其下半部的执行。

中断描述符表

在实模式下,我们说中断向量,在保护模式下,就变成了中断描述符。

首先我们要明确,中断描述符表( Interrupt Descriptor Table, IDT)是保护模式下用于存储中断处理程序入口的表,当CPU 接收一个中断时,需要用中断向量在此表中检索对应的描述符,在该描述符中找到中断处理程序的起始地址,然后执行中断处理程序。表中不仅仅有中断门描述符,还可以有任务门描述符和陷阱门描述符 。但是我们只用到了中断门描述符,其他的就不多说了。

所有支持的中断和异常都在下表,其中0-19已经被使用,20-31是保留的,我们自定义的只能从32开始

image

中断门描述符

中断门描述符的格式如下,我们将来要通过构造它来填充IDT​(中断描述符表)

image

中断描述符表寄存器

GDT​需要有一个寄存器(IDTR​)用于存储GDT的地址,IDT​也需要有一个寄存器来存储IDT的地址,即IDTR​,以下是IDTR的结构格式

image

其中0 ~ 15位存储IDT的表界限(表大小-1),16 ~ 47位存储IDT表基址

中断处理过程

完整的中断过程分为CPU外和CPU内两部分。

  • CPU外:外部设备的中断由中断代理芯片接收,处理后将该中断的中断号发送到CPU。
  • CPU 内:CPU执行该中断向量号对应的中断处理程序。

中断向量号是中断描述符的索引,当处理器收到一个外部中断号后,它用此号在中断描述符表中查询对应的中断描述符,然后再去执行该中断描述符中的中断处理程序。由于中断描述符是 8 个字节,所以处理器用中断号乘以 8 后,再与 IDTR 中的中断描述符表地址相加,所求的地址之和便是该中断号对应的中断描述符。

流程图如下:

image

中断时压栈情况

如下,error_code是根据实际情况判断有没有的:

imageimage

  • 中断发生后,eflags中的NT位和TF位会被置0

    • 如果中断对应的门描述符是中断门,标志寄存器 eflags中的IF位被自动置0,避免中断嵌套,即中断处理过程中又来了个新的中断,这是为防止在处理某个中断的过程中又来了一个相同的中断。这表示默认情况下,处理器会在无人打扰的方式下执行中断门描述符中的中断处理例程
    • 若中断发生时对应的描述符是任务门或陷阱门的话,CPU是不会将IF位清0的。因为陷阱门主要用于调试,它允许 CPU响应更高级别的中断,所以允许中断嵌套。而对任务门来说,这是执行一个新任务
  • **从中断返回的指令是iret,它从栈中弹出数据到寄存器cs​、eip​、eflags​**等,根据特权级是否改变,判断是否要恢复旧栈。也就是说是否将栈中位于SS_old和ESP_old 位置的值弹出到寄存ss和 esp。当中断处理程序执行完成返回后,通过iret指令从栈中恢复eflags 的内容

可编程中断控制器8259A

为了让CPU获得每个外部设备的中断信号,最好的方式是在CPU中为每一个外设准备一个引脚接收中断,但这是不可能的,计算机中挂了很多外部设备,而且外设数量是没有上限的,无论CPU 中准备多少引脚都不够用。

上一节中我们说到,可屏蔽中断是通过INTR信号线进入CPU的,一般可独立运行的外部设备,如打印机、声卡等,其发出的中断都是可屏蔽中断,都共享这一根INTR信号线通知CPU。

但是,任务是串行在 CPU 上执行的,CPU每次只能执行一个任务,如果同时有多个外设发出中断,而 CPU只能先处理一个,它无法指定先响应哪个。同时,为了不使这些中断丢失,也需要为它们单独维护一个中断队列

这就是中断代理的作用,由它负责对所有可屏蔽中断仲裁,决定哪个中断优先被 CPU受理,同时向CPU提供中断向量号等功能

image

  • 若采用级联方式,即多片8259A芯片串连在一起,最多可级联9个,也就是最多支持 64个中断(片8259A通过级联可支持7n+1个中断源)。
  • 级联时只能有一片 8259A为主片 master,其余的均为从片 slave
  • 来自从片的中断只能传递给主片,再由主片向上传递给 CPU,也就是说只有主片才会向CPU发送INT中断信号

在8259A 内部有两组寄存器

  • 一组是初始化命令寄存器组,用来保存初始化命令字(Imitialization Command Words,ICW),ICW共4个,ICW1~ICW4。
  • 另一组寄存器是操作命令寄存器组,用来保存操作命令字(Operation Command Word,OCW),OCW共3个,OCW1~OCW3。

所以,我们对8259A的编程,也分为初始化和操作两部分

  1. 初始化部分操作,用来确定是否需要级联,设置起始中断向量号,设置中断结束模式。其编程就是往 8259A 的端口发送一系列 ICW。

    • 由于从一开始就要决定 8259A的工作状态,所以要一次性写入很多设置,某些设置之间是具有关联、依赖性的,也许后面的某个设置会依赖前面某个ICW 写入的设置所以这部分要求严格的顺序,必须依次写入ICW1、ICW2、ICW3、ICW4。
  2. 操作部分是用OCW来操作控制8259A,前面所说的中断屏蔽和中断结束,就是通过往8259A端口发送 OCW 实现的。

    • OCW的发送顺序不固定,3个之中先发送哪个都可以。

关于ICW和PCW各个字段具体含义参考P316。

另外:

  • ICW1 和 OCW2、OCW3 是用偶地址端口 0x20(主片)或 0xA0(从片)写入。
  • ICW2~ICW4 和 OCW1 是用奇地址端口 0x21(主片)或 0xA1(从片)写入。
  • ICW1-4应按顺序写入,而OCW不用,其内部有自己的识别方法

代码部分

代码规划

  1. 创建33个中断处理函数
  2. 写函数构建中断描述符表
  3. 写函数初始化中断控制器8295A,并只打开时钟中断
  4. 把2和3封装进入中断始化函数idt_init,调用idt_init函数完成中断描述符表初始化与中断控制器初始化,并加载idtr寄存器的值
  5. 把4封装进入总初始化函数init_all,调用这个函数完成中断初始化
  6. 在main中打开中断测试

书上给出的流程图如下:l

image

我们要做的几件事就是构建中断描述符表、初始化中断控制器8295A、加载idtr寄存器。

新增文件比较多,目录结构如下:

a              
├─ boot        
│  ├─ include  
│  │  └─ boot.inc  
│  ├─ loader.s   
│  └─ mbr.s    
├─ kernel      
│  ├─ global.h   
│  ├─ init.c   
│  ├─ init.h   
│  ├─ interrupt.c  
│  ├─ interrupt.h  
│  ├─ kernel.s   
│  └─ main.c   
├─ lib         
│  ├─ kernel   
│  │  ├─ io.h  
│  │  ├─ print.h   
│  │  └─ print.s   
│  └─ stdint.h   
└─ 命令.txt      

代码

kernel.s

首先是kernel/kernel.s

[bits 32]
%define ERROR_CODE nop		    ; 有些中断进入前CPU会自动压入错误码(32位),为保持栈中格式统一,这里不做操作.
%define ZERO push 0		        ; 有些中断进入前CPU不会压入错误码,对于这类中断,我们为了与前一类中断统一管理,就自己压入32位的0extern put_str                  ; 声明外部函数
extern put_int                  ; 声明外部函数section .data
intr_str db 0xa, "interrupt occur!", 0xa, 0    ;第二个是一个换行符,第三个定义一个ascii码为0的字符,用来表示字符串的结尾
global intr_entry_table
intr_entry_table:               ; 编译器会将之后所有同属性的section合成一个大的segment,所以这个标号后面会聚集所有的中断处理程序的地址
%macro VECTOR 2                 ; 声明一个宏, 名字VECTOR 接受两个参数
section .text
intr%1entry:		            ; 每个中断处理程序都要压入中断向量号,所以一个中断类型一个中断处理程序,自己知道自己的中断向量号是多少,此标号来表示中断处理程序的入口%2                          ; 这一步是根据宏传入参数的变化而变化的push intr_strcall put_str                ; 输出字符串add esp, 4push %1                     ; 直接压入中断号(十六进制数)call put_intadd esp, 4                  ; 弹出参数; 如果是从片上进入的中断,除了往从片上发送EOI外,还要往主片上发送EOI mov al, 0x20                ; 中断结束命令EOIout 0xa0, al                ; 向从片发送out 0x20, al                ; 向主片发送add esp,4			        ;对于会压入错误码的中断会抛弃错误码(这个错误码是执行中断处理函数之前CPU自动压入的),对于不会压入错误码的中断,就会抛弃上面push的0iretsection .data                   ; 这个段就是存的此中断处理函数的地址dd    intr%1entry	        ; 存储各个中断入口程序的地址,形成intr_entry_table数组,定义的地址是4字节,32位
%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

这段代码核心任务是构建intr_entry_table,这个表的每一项将会被放到中断门描述符中,代表着中断处理程序的地址。

12-33行声明了一个宏,接受两个参数,分别是中断号和填充字段:

  • 填充字段:我们在上面说过,压栈时有的有ERROR_CODE,有的没用,在统一处理时比较啰嗦,所以我们通过此字段判断,如果该中断有ERROR_CODE,那就填充nop,什么也不做,如果没有,那就填充push 0,向栈中压0保持统一。
  • 中断号:中断号在两个地方用到了,首先是标号,通过不同中断号可以区分不同中断地址,地址写到了宏的data部分,由于编译器特性,都是data段的部分会合并成最终的data,因此intr_entry_table后面紧挨着就是各个中断向量处理程序地址

17-22行就是输出一些信息,帮助我们了解运行状况

25-28行是给8259a一个信号,告诉他我们中断处理完成,可以接收新的了

35-67行定义了33个中断,然后宏会在这里展开形成代码,33的原因我们之前也说过了,前32个都用了,我们只能从33开始用

interrupt.c

然后是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 // 支持的中断描述符个数33// 按照中断门描述符格式定义结构体
struct gate_desc
{uint16_t func_offset_low_word;  // 函数地址低字uint16_t selector;              // 选择子字段uint8_t dcount;                 // 此项为双字计数字段,是门描述符中的第4字节。这个字段无用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]; // 中断门描述符(结构体)数组,名字叫idtextern intr_handler intr_entry_table[IDT_DESC_CNT]; // 引入kernel.s中定义好的中断处理函数地址数组,intr_handler就是void* 表明是一般地址类型/* 初始化可编程中断控制器8259A */
static void pic_init()
{/* 初始化主片 */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); // 主片除了最低位其他全部置为1outb(PIC_S_DATA, 0xff); // 从片全部置1, 全屏蔽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;
}// 此函数用来循环调用make_idt_desc函数来完成中断门描述符与中断处理函数映射关系的建立,传入三个参数:中断描述符表某个中段描述符(一个结构体)的地址,属性字段,中断处理函数的地址
static void idt_desc_init()
{int i = 0;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();/* 加载idt */uint64_t idt_operand = ((sizeof(idt) - 1) | ((uint64_t)(uint32_t)idt << 16)); // 定义要加载到IDTR寄存器中的值asm volatile("lidt %0" : : "m"(idt_operand));put_str("idt_init done\n");
}

这个文件的核心就是初始化中断描述符表(idt)以及可编程中断控制器8259A

  • 15-22行定义中断描述符的结构体
  • 26行定义的数组就是idt
  • 31-50行初始化8259A,具体字段的含义参考书上,值得注意的是它除了主片IR0其他的中断都屏蔽了,而主片IR0就是时钟中断
  • 52-71行初始化中断描述符表,结合中断描述符格式很好理解
  • 82行加载idt表到IDTR

interrupt.h

kernel/interrupt.h

#ifndef __KERNEL_INTERRUPT_H
#define __KERNEL_INTERRUPT_H
#include "stdint.h"
typedef void *intr_handler; // 将intr_handler定义为void*同类型
void idt_init(void);
#endif

global.h

kernel/global.h 声明了一些描述符所用字段

#ifndef __KERNEL_GLOBAL_H
#define __KERNEL_GLOBAL_H
#include "stdint.h"// 选择子的RPL字段
#define RPL0 0
#define RPL1 1
#define RPL2 2
#define RPL3 3// 选择子的TI字段
#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)// 定义模块化的中断门描述符attr字段,attr字段指的是中断门描述符高字第8到16bit
#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) // DPL为0的中断门描述符attr字段
#define IDT_DESC_ATTR_DPL3 ((IDT_DESC_P << 7) + (IDT_DESC_DPL3 << 5) + IDT_DESC_32_TYPE) // DPL为3的中断门描述符attr字段#endif

io.h

lib/kernel/io.h

声明了一些内敛函数,由于读取端口操作,上面对8295A的操作就用到了

#ifndef __LIB_IO_H
#define __LIB_IO_H
#include "stdint.h"// 一次送一字节的数据到指定端口,static指定只在本.h内有效,inline是让处理器将函数编译成内嵌的方式,就是在该函数调用处原封不动地展开
// 此函数有两个参数,一个端口号,一个要送往端口的数据
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));
}// 利用outsw(端口输出串,一次一字)指令,将ds:esi指向的addr处起始的word_cnt(存在ecx中)个字写入端口port,ecx与esi会自动变化
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" : "+S"(addr), "+c"(word_cnt) : "d"(port) : "memory");// 通知编译器,内存已经被改变了
}#endif

init.c

kernel/init.c

#include "init.h"
#include "print.h"
#include "interrupt.h"/*负责初始化所有模块 */
void init_all()
{put_str("init_all\n");idt_init(); // 初始化中断
}

init.h

#ifndef __KERNEL_INIT_H
#define __KERNEL_INIT_H
void init_all(void);
#endif

main.c

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

结果

运行结果如下:

image

编译脚本

#!/bin/sh
#编译并写入mbr
nasm -I ./boot/include/ -o ./build/mbr.bin ./boot/mbr.s
dd if=./build/mbr.bin of=../../hd60M.img bs=512 count=1 conv=notrunc#编译并写入loader
nasm -I ./boot/include/ -o ./build/loader.bin ./boot/loader.s
dd if=./build/loader.bin of=../../hd60M.img bs=512 count=4 seek=2  conv=notrunc#编译mian
gcc-4.4 -I ./lib/kernel -I ./lib/ -I ./kernel/ -fno-builtin  -c -o ./build/main.o ./kernel/main.c -m32 #编译pirnt
nasm -f elf -o build/print.o lib/kernel/print.s
#编译kernel
nasm -f elf -o build/kernel.o kernel/kernel.s#编译interrupt
gcc-4.4 -I lib/kernel/ -I lib/ -I kernel/ -c -fno-builtin -o build/interrput.o -m32 kernel/interrupt.c#编译init
gcc-4.4 -m32 -I lib/kernel/ -I lib/ -I kernel/ -c -fno-builtin -o build/init.o kernel/init.c#链接成内核
ld -m elf_i386 -Ttext 0x00001500 -e main -o build/kernel.bin  build/main.o build/kernel.o build/init.o build/interrput.o build/print.o#写入内核
dd if=./build/kernel.bin of=../../hd60M.img bs=512 count=200 seek=9  conv=notrunc#清除build文件夹内所有的编译好的二进制文件
rm -rf build/*

b 改进中断

知识部分

我们在上一节中实现的中断处理程序很简单,且只用汇编实现,这肯定是不可以的,在这一小节中,我们要尝试用c语言写一个中断处理程序,然后在汇编中进行调用,流程图如下:

image

即我们要新建一个idt_table数组,数组中每一项就是对应中断程序入口,然后在intr_entry中调用

代码部分

kernel.s

[bits 32]
%define ERROR_CODE nop		    ; 有些中断进入前CPU会自动压入错误码(32位),为保持栈中格式统一,这里不做操作.
%define ZERO push 0		        ; 有些中断进入前CPU不会压入错误码,对于这类中断,我们为了与前一类中断统一管理,就自己压入32位的0extern idt_table		        ; idt_table是C中注册的中断处理程序数组section .data
intr_str db "interrupt occur!", 0xa, 0    ;第二个是一个换行符,第三个定义一个ascii码为0的字符,用来表示字符串的结尾
global intr_entry_table
intr_entry_table:               ; 编译器会将之后所有同属性的section合成一个大的segment,所以这个标号后面会聚集所有的中断处理程序的地址
%macro VECTOR 2                 ; 声明一个宏, 名字VECTOR 接受两个参数
section .text
intr%1entry:		            ; 每个中断处理程序都要压入中断向量号,所以一个中断类型一个中断处理程序,自己知道自己的中断向量号是多少,此标号来表示中断处理程序的入口%2                          ; 这一步是根据宏传入参数的变化而变化的push ds                                 ; 以下是保存上下文环境push espush fspush gspushad; 如果是从片上进入的中断,除了往从片上发送EOI外,还要往主片上发送EOI mov al, 0x20                ; 中断结束命令EOIout 0xa0, al                ; 向从片发送out 0x20, al                ; 向主片发送push %1call [idt_table+%1*4]jmp intr_exitsection .data                   ; 这个段就是存的此中断处理函数的地址dd    intr%1entry	        ; 存储各个中断入口程序的地址,形成intr_entry_table数组,定义的地址是4字节,32位
%endmacro                       ; 宏结束section .text
global intr_exit
intr_exit:	                                 ; 以下是恢复上下文环境add esp, 4			                    ; 跳过中断号popadpop gspop fspop espop dsadd esp, 4			                    ;对于会压入错误码的中断会抛弃错误码(这个错误码是执行中断处理函数之前CPU自动压入的),对于不会压入错误码的中断,就会抛弃上面push的0                  iretd				                    ; 从中断返回,32位下iret等同指令iretdVECTOR 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

改动主要是宏定义,call [idt_table+%1*4]​就是调用c语言的中断处理函数,并且在其上下都要保存/恢复堆栈

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 // 支持的中断描述符个数33// 按照中断门描述符格式定义结构体
struct gate_desc
{uint16_t func_offset_low_word;  // 函数地址低字uint16_t selector;              // 选择子字段uint8_t dcount;                 // 此项为双字计数字段,是门描述符中的第4字节。这个字段无用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]; // 中断门描述符(结构体)数组,名字叫idtextern intr_handler intr_entry_table[IDT_DESC_CNT]; // 引入kernel.s中定义好的中断处理函数地址数组,intr_handler就是void* 表明是一般地址类型char *intr_name[IDT_DESC_CNT];        // 中断/异常名字
intr_handler idt_table[IDT_DESC_CNT]; // 定义中断处理程序数组.在kernel.S中定义的intrXXentry只是中断处理程序的入口,最终调用的是ide_table中的处理程序/* 初始化可编程中断控制器8259A */
static void pic_init()
{/* 初始化主片 */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); // 主片除了最低位其他全部置为1outb(PIC_S_DATA, 0xff); // 从片全部置1, 全屏蔽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 general_intr_handler(uint8_t vec_nr)
{if (vec_nr == 0x27 || vec_nr == 0x2f){ // 伪中断向量,无需处理。详见书p337return;}put_str("int vector: 0x");put_int(vec_nr);put_char('\n');
}/* 完成一般中断处理函数注册及异常名称注册 */
static void exception_init(void)
{int i = 0;/* idt_table数组中的函数是在进入中断后根据中断向量号调用的,* 见kernel/kernel.S的call [idt_table + %1*4] */for (i = 0; i < IDT_DESC_CNT; i++){intr_name[i] = "unknown";            // 先统一赋值为unknownidt_table[i] = general_intr_handler; // 默认为general_intr_handler,以后会由register_handler来注册具体处理函数。}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";
}// 此函数用来循环调用make_idt_desc函数来完成中断门描述符与中断处理函数映射关系的建立,传入三个参数:中断描述符表某个中段描述符(一个结构体)的地址,属性字段,中断处理函数的地址
static void idt_desc_init()
{int i = 0;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();  // 调用上面写好的函数完成中段描述符表的构建exception_init(); // 异常名初始化并注册通常的中断处理函数pic_init();       // 设定化中断控制器,只接受来自时钟中断的信号/* 加载idt */uint64_t idt_operand = ((sizeof(idt) - 1) | ((uint64_t)(uint32_t)idt << 16)); // 定义要加载到IDTR寄存器中的值asm volatile("lidt %0" : : "m"(idt_operand));put_str("idt_init done\n");
}

改动主要是66-108行

其中66-75行定义了通用的中断处理函数,后面会再注册具体处理函数。

75-108行定义了两个数组,分别是intr_name和idt_table,用于指示中断名字和中断函数入口地址

结果如下:

image

提高时钟中断频率

先梳理一下要做的工作:

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

代码部分

time.c

device/time.c

#include "timer.h" 
#include "io.h"
#include "print.h"#define IRQ0_FREQUENCY	    100    //定义我们想要的中断发生频率,100HZ                     
#define INPUT_FREQUENCY	    1193180     //计数器0的工作脉冲信号评率
#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); 作者这句代码会先将16位的counter_value强制类型转换为8位值,也就是原来16位值只留下了低8位,然后//又右移8未,所以最后送入counter_port的counter_value的高8位是8个0,这会导致时钟频率过高,出现GP异常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");
}

time.h

/device/time.h

#ifndef __DEVICE_TIME_H
#define __DEVICE_TIME_H
void timer_init(void);
#endif

结果如下:

image

可以感受一下频率快很多

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/869560.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

PHP语法进阶

PHP语法进阶 数组 数组能够在单个变量中存储多个值,并且可以根据 键 访问其中的 值PHP有两种数组:数值数组、关联数组。 数值和关联两个词都是针对数组的键而言的。 先介绍下数值数组,数值数组是指数组的键是整数的数组,并且键的整数顺序是从0开始,依次类推。 数值数组 $m…

Agentic RAG 系统的崛起

探秘智能检索新境界:Agentic RAG 系统的崛起 📖阅读时长:10分钟 🕙发布时间:202探秘智能检索新境界:Agentic RAG 系统的崛起 📖阅读时长:10分钟 🕙发布时间:2025-01-15近日热文:全网最全的神经网络数学原理(代码和公式)直观解释 欢迎关注知乎和公众号的专栏内…

5、提升Java的并发性

CompletableFuture及反应式编程背后的概念 :::info ❏线程、Future以及推动Java支持更丰富的并发API的进化动力 ❏ 异步API ❏ 从“线框与管道”的角度看并发计算 ❏ 使用CompletableFuture结合器动态地连接线框❏ 构成Java 9反应式编程Flow API基础的“发布-订阅”协议❏ 反应…

goal vs objective vs target

goal 680 objective 2421 target 1284GOAL vs OBJECTIVE left 4WORD 1: GOAL 过滤200WORD W1 W2SCORED 1423 1 He has scored a further five goals in the Spanish Supercup and the Champions League.他在西班牙超级杯和冠军联赛中又打进了五个进球。 scored Barcas fourth…

大模型备案流程-简易易懂

大模型备案除了资料撰写难度高外,难点还在于各省没有统一标准。备案流程、资料要求、考察重点都会有些许差异。不过,各省的大体申报流程都如下文所示(各省主要差异点我会标出,具体内容可以一起沟通交流): 一、备案申请 报请申请者所在省份/直辖市/自治区网信:向企业注册地…

KingbaseES RAC集群案例之---jmeter压测

KingbaseES RAC、jmeter案例说明: 通过jmeter压测,测试KingbaseES RAC集群负载均衡功能。 数据库版本: test=# select version();version ---------------------KingbaseES V008R006 (1 row)测试架构:一、jmeter版本 1、系统jiava版本 [root@node203 ~]# java -version ope…

{LOJ #6041. 「雅礼集训 2017 Day7」事情的相似度 题解

\(\text{LOJ \#6041. 「雅礼集训 2017 Day7」事情的相似度 题解}\) 解法一 由 parent 树的性质得到,前缀 \(s_i,s_j\) 的最长公共后缀实质上就是 \(i,j\) 在 SAM 中的 \(\operatorname{LCA}\) 在 SAM 中的 \(\operatorname{len}\)。让我们考虑如何处理 \((l,r)\) 区间内的询问…

解决Hyper-V保留端口导致各种端口占用报错的问题

0.有时候在本地启用一个服务比如MySQL服务,或者在启用IDEA的调试的时候,或者在本地启用一个监听端口的时候可能会出现监听失败的情况,经过查找之后会发现并没有应用占用相应的端口。 1.经过查找发现其实是在启用了Hyper-V之后系统会保留一些端口,这些端口如果包含了你应用要…

D. Madoka and The Corruption Scheme -- (贪心,组合数学,构造)

题目链接:Problem - D - Codeforces 题目大意: 一共n轮比赛,有\(2^n\)个参赛者,第\(i\)轮有\(2^{n - i}\) 场比赛,Madoka能安排第一局的比赛,她想让最后的赢家编号更小,主办方最多有k次操作,能修改任意每一场比赛的获胜情况,可以让最终赢家编号更 大,求Madoka在主办方…

PHP语法基础

PHP语法基础php文档拓展名是.phpphp文件通常包含html标签以及一些php脚本运行代码 ,注意:html js css可以在php文件执行但是,php不能在html js css在php文件执行php语法用;结尾 <!DOCTYPE html> <html> <body> <h1>我的第一张php页面><h1>…

本地打包docker images并上传到服务器.250115

情景: 服务器docker Pull 拉不下来 docker pull easzlab/kubeasz-k8s-bin:v1.31.2 Get "https://registry-1.docker.io/v2/": net/http: request canceled while waiting for connection (Client.Timeout exceeded while awaiting headers) 2025-01-14 17:06:35 [ez…