二、3.中断

news2024/12/24 2:10:54

中断上半部和下半部是什么?

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

对于中断是否无视 eflags 中的 IF 位,可以这么理解:

  1. 首先,只要是导致运行错误的中断类型都会无视 IF 位,不受 IF 位的管束,如 NMI 、异常。
  2. 其次,由于 int n 型的软中断用于实现系统调用功能,不能因为 IF 位为 0 就不顾用户请求,所以
    为了用户功能正常,软中断必须也无视 IF 位。

中断描述符表中不仅仅有中断描述符,还可以有任务门描述符和陆阱门描述符 。 由于表中所有描述符都是记录一段程序的起始地址,相当于通向某段程序的“大门”,所以,中断描述符表中的描述符有自己的名称一一门 。


image-20230811100040717


寄存器入栈情况及顺序,这里不再讨论有关特权检查的内容。

  1. 处理器根据中断向量号找到对应的中断描述符后,拿 CPL 和中断门描述符中选择子对应的目标代码段的 DPL 比对,若 CPL 权限比 DPL 低,即数值上 CPL > DPL,这表示要向高特权级转移,需要切换到高特权级的拢。这也意味着当执行完中断处理程序后,若要正确返回到当前被中断的进程,同样需要将 栈恢复为此时的旧拢。于是处理器先临时保存当前旧栈 SS 和 ESP 的值,记作 SS_old 和 ESP_old,然后在TSS 中找到同目标代码段 DPL 级别相同的栈加载到寄存器 SS 和 ESP 中,记作 SS_new 和 ESP_new ,再将之前临时保存的 SS old 和 ESP old 压入新栈备份,以备返回时重新加载到栈段寄存器 SS 和栈指针 ESP 。由于 SS_old 是 16 位数据, 32 位模式下的栈操作数是 32 位,所以将 SS_old 用 0 扩展其高 16 位,成为 32位数据后入栈 。 此时新栈内容如图 7-8 中 A 所示。
  2. 在新栈中压入 EFLAGS 寄存器,新栈内容如图 7-8 中 B 所示。
  3. 由于要切换到目标代码段,对于这种段间转移,要将 CS 和 EIP 保存到 当前栈中备份,记作 CS_old 和 EIP_old,以便中断程序执行结束后能恢复到被中断的进程。同样 CS_old 是 16 位数据, 需要用 0 填充其高 16 位,扩展为 32 位数据后入栈。 此时新栈内容如图 7-8 中 C 所示。当前栈是新栈,还是旧栈,取决于第 1 步中是否涉及到特权级转移。
  4. 某些异常会有错误码,此错误码用于报告异常是在哪个段上发生的,也就是异常发生的位置,所以错误码中包含选择子等信息, 一会介绍。错误码会紧跟在 EIP 之后入栈,记作 ERROR_CODE。此时新栈内容如图 7-8 中 D 所示。

image-20230811104956202

image-20230811105105638


为了安全起见,处理器在返回到被中断过程中也要再进行一次特权级检查,下面咱们聊聊这个从中断处理程序返回的过程,假设此时已经手动将 ERROR_CODE 从栈中弹出,栈顶己位于正确的位置,即指向 EIP_old

  1. 当处理器执行到 iret 指令时,它知道要执行远返回,首先需要从栈中返回被中断进程的代码段选
    择子 CS_old 及指令指针 EIP_old。这时候它要进行特权级检查。先检查栈中 CS 选择子 CS_old,根据其 RPL 位,即未来的 CPL,判断在返回过程中是否要改变特权级。
  2. 栈中 cs 选择子是 CS_old ,根据 CS_old 对应的代码段的 DPL 及 CS_old 中的 RPL 做特权级检查,规则不再赘述。如果检查通过,随即需要更新寄存器 cs 和 EIP。由于 CS_old 在入栈时已经将高 16 位扩充为 0,现在是 32 位数据,段寄存器 CS 是 16 位,故处理器丢弃 CS_old 高 16 位,将低 16 位加载到 CS。将 EIP_old 加载到 EIP 寄存器,之后栈指针指向 EFLAGS 。如果进入中断时未涉及特权级转移,此时栈指针是 ESP_old (说明在之前进入中断后,是继续使用旧栈)。否则栈指针是 ESP_new (说明在之前进入中断后用的是 TSS 中记录的新栈)。
  3. 将栈中保存的 EFLAGS 弹出到标志寄存器 EFLAGS 。如果在第 1 步中判断返回后要改变特权级,此时栈指针是 ESP_new,它指向栈中的 ESP_old。否则进入中断时属于平级转移,用的是旧栈,此时栈指针是 ESP_old,栈中己无因此次中断发生而入栈的数据,栈指针指向中断发生前的栈顶。
  4. 如果在第 1 步中判断出返回时需要改变特权级,也就是说需要恢复旧栈,此时便需要将 ESP_old 和 SS_old 分别加载到寄存器 ESP 及 SS,丢弃寄存器 SS 和 ESP 中原有的 SS_new 和 ESP_new,同时进行特权级检查。补充,由于 SS_old 在入栈时己经由处理器将高 16 位填充为 0,现在是 32 位数据,所以在重新加载到栈段寄存器 SS 之前,需要将 SS_old 高 16 位剥离丢弃,只用其低 16 位加载 SS 。

错误码本质上就是个描述符选择子,通过低 3 位属性来修饰此选择子指向是哪个表中的哪个描述符。

image-20230811134043678

EXT 表示 EXTernal event,即外部事件,用来指明中断源是否来自处理器外部,如果中断源是不可屏蔽中断 NMI 或外部设备, EXT 为 1 ,否则为 0 。
IDT 表示选择子是否指向中断描述符表 IDT, IDT 位为 1 ,则表示此选择子指向中断描述符表,否则指向全局描述符表 GDT 或局部描述符表 LDT 。
TI 和选择子中 TI 是一个意思,为 0 时用来指明选择子是从 GDT 中检索描述符,为 1 时是从 LDT 中检索描述符。当然,只有在 IDT 位为 0 时 TI 位才有意义。
选择子高 13 位索引就是选择子中用来在表中索引描述符用的下标。

中断返回时, iret 指令并不会把错误码从栈中弹出,所以在中断处理程序中需要手动用栈指针跨过错误码或将其弹出。否则栈顶处若不是 EIP (EIP_old)的话, iret 返回时将会载入错误的值到后续寄存器。
通常能够压入错误码的中断属于中断向量号在 0~32 之内的异常,而外部中断(中断向量号在 32~255 之间)和 int 软中断并不会产生错误码。通常我们并不用处理错误码。


中断描述符表本质上就是中断处理程序地址数组,而中断向量号便是此数组的索引下标,这就是中断向量号是个整数的原因。 CPU 不支持“数组名[索引]”的形式,那是高级语言编译器支持的东西,它最终也要编译转换成某种内存寻址方式之一,必须得用最基本的形式一一地址来访问内存。当 CPU 接收到 8259A 送来的中断向量号后要将其乘以 8,再加上中断描述符表的起始地址,经过内存寻址,最终定位到目标中断处理程序。


汇编宏定义——预编译

;单行宏定义
%define ERROR_CODE nop

;多行宏定义
%macro mul_add 3 ; 宏名字 参数个数
mov eax, %l
add eax, %2
add eax, %3
%endmacro
;调用: 
mul_add 45, 24, 33 ;其中%1是45, %2是24, %3是33

汇编版本的中断处理程序

定义了 33 个中断处理程序。每个中断处理程序都一样,就是调用字符串打印函数 put_str 来打印宇符串“ interrupt occur!”,之后直接退出中断。

[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
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			 ; 跨过error_code
   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

编译器会将属性相同的 section 合并到同一个大的 segment 中,而且,为了显得更靠谱一点,我们在 kemel.S 中对所有的数据 section 都用了同一个名字.data, 编译之后,所有中断处理程序的地址都会乖乖地作为数组 intr_entry_table 的元
素紧凑地排在一起。

创建IDT,安装中断处理程序

/*中断门描述符结构体*/
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];   // idt是中断描述符表,本质上就是个中断门描述符数组

typedef void* intr_handler;
extern intr_handler intr_entry_table[IDT_DESC_CNT]; // 声明引用定义在kernel.S中的中断处理函数入口数组

/* 创建中断门描述符 */
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");
}

用内联汇编实现端口I/O函数

#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

它们都是对底层硬件端口直接操作的,通常由设备的驱动程序来调用,不用说,为了快速响应,函数调用上需要更加高效。而且,操作系统是为了让用户程序编写、执行更加方便才诞生的,归根结底是为了用户程序服务,所以它会让处理器的大多数时间花在 3 特权级的用户程序上。为了让处理器更多地为用户程序服务,操作系统(包括硬件驱动程序)必须减少自己占用处理器的时间,所以,对硬件端口的操作,只要求一个字一一快。

但一般的函数调用需要涉及到现场保护及恢复现场,即函数调用前要把相关的栈、返回地址( CS 和EIP )保存到栈中,函数执行完返回后再将它们从栈中恢复到寄存器。栈毕竟是内存,速度低很多,而且入栈、出栈这么多内存操作,对于想方设法提速的内核程序来说是无法忍受的。

因此,为了提速,在我们的实现中,函数的存储类型都是 static,并且加了 inline 关键字,它建议处理器将函数编译为内嵌的方式。内嵌函数大家都清楚吧,就是将所调用的函数体的内容,在该函数的调用处,原封不动地展开,这样编译后的代码中将不包含 call 指令,也就不属于函数调用了,而是顺次执行。虽然这会让程序大一些,但减少了函数调用及返回时的现场保护及恢复工作,提升了效率还是值得的 。

设置8259A

#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	 // 目前总共支持的中断数

/* 初始化可编程中断控制器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");
}

image-20230811182832020

加载IDT,开启中断

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

lidt 的操作数也要符合 IDTR 寄存器的结构,所以恼的操作数也必须是48 位,前 16 位是界限 h由,后 32 位是基址,只不过这 48 位的数据必须位于内存中

由于 C 语言中没有 48 位的数据类型,所以我们用 64 位的变量 idt_operand 来代替,这是没问题的,lidt 中会取出 48 位数据做操作数,所以咱们只要保证 64 位变量中的前 48 位数据是正确的就行。为了给 lidt 凑出 48 位的操作数,在第 76 行。

  1. 先用 sizeof(idt) - 1 得到 idt 的段界限 limit,这用作低 16 位的段界限。
  2. 接下来再将 idt 的地址挪到高 32 位即可,这可以通过把 idt 地址左移 16 位的形式实现。由于数组名便是地址,即指针,故先将其转换成整数才能参与后面的左移运算。考虑到 32 位地址经过左移操作后,高位将被丢弃,万一原地址高 16 位不是 0,这样会造成数据错误,故需要将 idt 地址转换成 64 位整型后再进行左移操作,这样其高 32 位都是 0,经过左移操作依然能够保证其精度。由于指针只能转换成相同大小的整型,故 32 位的指针不能直接转换成 64 位的整型,所以采取迂回的作法,先将其转换成 uint32_t,再将其转换成 uint64_t,之后再对这个 64 位的无符号整型数据进行左移 16 位操作。这样 idt 地址被移到了16~48 位,低 16 位自动填充为 0 。
  3. 之后再将以上两步的结果通过“按位或”运算符 ‘|’ 组合到一起后,存储到变量 idt_operand 中。虽然经过以上的三步得到的操作数是 64 位,但由于 lidt 的操作数是从内存地址处获得的,所以 lidt 依然只在该地址处(&idt_operand)取其中的 48 位数据当作操作数。

gcc -I lib/kernel/ -I lib/ -I kernel/ -m32 -c -fno-builtin -o kernel/main.o kernel/main.c
nasm -f elf -o lib/kernel/print.o lib/kernel/print.S
nasm -f elf -o kernel/kernel.o kernel/kernel.S
gcc -I lib/kernel/ -I lib/ -I kernel/ -m32 -c -fno-builtin -o kernel/interrupt.o kernel/interrupt.c
gcc -I lib/kernel/ -I lib/ -I kernel/ -m32 -c -fno-builtin -o kernel/init.o kernel/init.c
ld -Ttext 0xc0001500 -e main -m elf_i386 -s -o kernel.bin kernel/main.o kernel/init.o kernel/interrupt.o lib/kernel/print.o kernel/kernel.o

C语言实现通用中断函数

char* intr_name[IDT_DESC_CNT];		     // 用于保存异常的名字
intr_handler idt_table[IDT_DESC_CNT];	     // 定义中断处理程序数组.在kernel.S中定义的intrXXentry只是中断处理程序的入口,最终调用的是ide_table中的处理程序

/* 通用的中断处理函数,一般用在异常出现时的处理 */
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数组中的函数是在进入中断后根据中断向量号调用的,
     * 见kernel/kernel.S的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";
}

extern idt_table		 ;idt_table是C中注册的中断处理程序数组

section .data
global intr_entry_table
intr_entry_table:

%macro VECTOR 2
section .text
intr%1entry:		 ; 每个中断处理程序都要压入中断向量号,所以一个中断类型一个中断处理程序,自己知道自己的中断向量号是多少

   %2				 ; 中断若有错误码会压在eip后面 
; 以下是保存上下文环境
   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时钟

#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");
}

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

数据结构(3)

线性表是多个具有相同特征的数据的有限序列。 前驱元素&#xff1a;A在B前面&#xff0c;称A为B的前驱元素。 后继元素&#xff1a;B在A后面&#xff0c;称B为A的后继元素。 线性表特征&#xff1a; 1.一个元素没有前驱元素&#xff0c;就是头结点&#xff1b; 2.最后一个…

自己实现 SpringMVC 底层机制 系列之-实现任务阶段 5- 完成 Spring 容器对象的自动装配 -@Autowried

&#x1f600;前言 自己实现 SpringMVC 底层机制 系列之-实现任务阶段 5- 完成 Spring 容器对象的自动装配 -Autowried &#x1f3e0;个人主页&#xff1a;尘觉主页 &#x1f9d1;个人简介&#xff1a;大家好&#xff0c;我是尘觉&#xff0c;希望我的文章可以帮助到大家&…

数据挖掘技术在智能外呼系统的应用探索

随着科技的不断发展&#xff0c;人们对于智能化的需求也日益增加&#xff0c;在企业获客领域&#xff0c;智能外呼系统应运而生。智能外呼系统是一种基于人工智能技术的客户服务系统&#xff0c;通过自动化的方式实现客户服务&#xff0c;提高客户满意度和企业效率。数据挖掘技…

wifi高通驱动之WCNSS_qcom_cfg.ini以及MCS、空间流数的学习和记录

一、WCNSS_qcom_cfg.ini 这个文件说是可以调优wifi的带宽&#xff0c;还有MIMO技术 Android Wi-Fi MIMO/SISO设置方法&#xff08;基于高通平台&#xff09;_广凯的博客-CSDN博客 不是太了解&#xff0c;先记录一下&#xff0c;个人感觉MCS和MIMO技术最全的应该是下面的网址…

Bigemap在地质工程勘察行业中的应用

Bigemap在地质工程勘察行业中的应用 选择Bigemap的原因&#xff1a; 师兄在测绘局工作&#xff0c;买过全能版&#xff0c;帮我下载过高程数据&#xff0c;我觉得效果可以&#xff0c;于是联系到软件公司进行试用、咨询 使用场景&#xff1a; 影像、等高线、地形等资料下载&…

买空气净化器怎么选 空气净化器哪个牌子性价比高

买空气净化器怎么选 空气净化器哪个牌子性价比高 空气净化器3个选购要点 空气净化器在如今的大气污染严重的环境下越来越受人们关注。然而&#xff0c;在市场上如此多的品牌和型号中&#xff0c;该如何选择一款适合自己的空气净化器呢&#xff1f;以下给出三个选购要点&#…

【校招VIP】网络基础之cookie、session和storage

考点介绍&#xff1a; cookie、session和localstorage 是目前常用的存储机制&#xff0c;不管是大厂还是中小公司&#xff0c;都会对这个问题有比较高的考察频度&#xff0c;而且有一定的深度和对比分析。 本期分享的网络基础之cookie、session和storage&#xff0c;分为试题、…

HarmonyOS/OpenHarmony应用开发-ArkTS语言渲染控制LazyForEach数据懒加载

LazyForEach从提供的数据源中按需迭代数据&#xff0c;并在每次迭代过程中创建相应的组件。当LazyForEach在滚动容器中使用了&#xff0c;框架会根据滚动容器可视区域按需创建组件&#xff0c;当组件划出可视区域外时&#xff0c;框架会进行组件销毁回收以降低内存占用。一、接…

precision指标的average参数

同样适用于recall、F1 分类任务种类 先说一下分类任务分几种&#xff0c;分类任务主要分为二分类、多分类和多标签这三种。 现在假设我们有一个样本&#xff0c;叫s 二分类是最常见的&#xff0c;将s分给A或B这两类。 多分类是将s分给A或B或C或更多的类别。 多标签是有A、B、…

基于C++的QT实现贪吃蛇小游戏

文章目录&#xff1a; 一&#xff1a;效果演示 二&#xff1a;实现思路 三&#xff1a;代码实现 widget.h widget.cpp main.cpp 一&#xff1a;效果演示 效果图◕‿◕✌✌✌ 代码下载 二&#xff1a;实现思路 通过按键控制蛇的移动&#xff0c;每吃一个商品蛇身就会加长…

16.5.6 【Linux】一个网络服务案例及登录文件协助

setroubleshoot --> 错误讯息写入 /var/log/messages 几乎所有 SELinux 相关的程序都会以 se 为开头&#xff0c;这个服务也是以 se 为开头。troubleshoot是错误克服&#xff0c;因此setroubleshoot要启动。这个服务会将关于 SELinux 的错误讯息与克服方法记录到 /var/log/…

优化指南:带宽限制的可行策略

大家好&#xff01;作为一名专业的爬虫程序员&#xff0c;我们经常面临的一个挑战就是带宽限制。尤其是在需要快速采集大量数据时&#xff0c;带宽限制成为了我们提升爬虫速度的一大阻碍。今天&#xff0c;我将和大家分享一些解决带宽限制的可行策略&#xff0c;希望能帮助大家…

问道管理:沪指失守3100点 机构判断“市场底”渐行渐近

8月21日&#xff0c;沪深两市股指盘中全线走低&#xff0c;三大股指收盘均跌超1%&#xff0c;其间沪指收盘指数今年以来初次失守3100点&#xff0c;创业板指更是3年多来初次跌破2100点。截至收盘&#xff0c;沪指跌1.24%报3092.98点&#xff0c;深证成指跌1.32%报10320.39点&am…

ResizeObserver监听元素大小的变化

window.resize不适用于dom的监听。 ResizeObserver ResizeObserver 接口监视 Element 内容盒或边框盒或者 SVGElement 边界尺寸的变化。 方法 ResizeObserver.disconnect() 取消特定观察者目标上所有对 Element 的监听。 ResizeObserver.observe() 开始对指定 Element 的监…

NLP预训练模型超大规模探索

总共从四方面来进行比较。 第一个方面&#xff0c;高层次方法&#xff08;自监督的预训练方法&#xff09;对比&#xff0c;总共三种方式。 语言模型式&#xff0c;就是 GPT-2 那种方式&#xff0c;从左到右预测&#xff1b;BERT-style 式&#xff0c;就是像 BERT 一样将一部…

通过几段代码,详解Python单线程、多线程、多进程

在使用爬虫爬取数据的时候&#xff0c;当需要爬取的数据量比较大&#xff0c;且急需很快获取到数据的时候&#xff0c;可以考虑将单线程的爬虫写成多线程的爬虫。下面来学习一些它的基础知识和代码编写方法。 一、进程和线程 进程可以理解为是正在运行的程序的实例。进程是拥…

卷积神经网络实现天气图像分类 - P3

&#x1f368; 本文为&#x1f517;365天深度学习训练营 中的学习记录博客&#x1f366; 参考文章&#xff1a;Pytorch实战 | 第P3周&#xff1a;彩色图片识别&#xff1a;天气识别&#x1f356; 原作者&#xff1a;K同学啊 | 接辅导、项目定制&#x1f680; 文章来源&#xff…

问道管理:中国十大科技板块?

跟着科技的开展&#xff0c;各种高科技工业在我国迅猛开展&#xff0c;其中十大板块就是一个比较典型的代表。这十大科技板块涵盖了从电子信息、生命健康到新材料等多个范畴&#xff0c;让我们一起来了解一下这十大板块的开展现状。 一、电子信息 作为国家重点支持开展的工业之…

剑指offer(C++)-JZ64:求1+2+3+...+n(算法-位运算)

作者&#xff1a;翟天保Steven 版权声明&#xff1a;著作权归作者所有&#xff0c;商业转载请联系作者获得授权&#xff0c;非商业转载请注明出处 题目描述&#xff1a; 求123...n&#xff0c;要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句&…

如何使用数学将 NumPy 函数的性能提高 50%

一、说明 2D 傅里叶变换是本世纪最重要的计算机科学算法之一。它已在我们的日常生活中得到应用&#xff0c;从Instagram过滤器到MP3文件的处理。 普通用户最常用的实现&#xff0c;有时甚至是在不知不觉中&#xff0c;是 NumPy 的改编。然而&#xff0c;尽管它很受欢迎&#xf…