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

配合视频学习效果更佳:
第一节:https://www.bilibili.com/video/BV1Nk4y1s7Qg/?vd_source=701807c4f8684b13e922d0a8b116af31
第二节:https://www.bilibili.com/video/BV1rs4y1i7pE/?vd_source=701807c4f8684b13e922d0a8b116af31
第三节:https://www.bilibili.com/video/BV1Yh4y1s7nS/?vd_source=701807c4f8684b13e922d0a8b116af31

代码仓库地址:https://github.com/xukanshan/the_truth_of_operationg_system

中断就是发生了事情通知CPU,但是处不处理就看情况。中断机制的本质是来了一个中断信号后,cpu去处理,然后调用相应的中断处理程序。

外部中断:

来自CPU外部的中断,因为外部中断源必须是某个硬件,所以也叫硬件中断。CPU为外部中断提供了两条信号线,如下图:
在这里插入图片描述

  • 从INTR引脚接收到的中断信号都是不影响系统运行的,可以随时处理,所以是可屏蔽的,也叫可屏蔽中断,每个中断都有中断向量号。

  • 从NMI引脚,全是对系统运行有致命伤害的,所以是不可屏蔽的,自然叫不可屏蔽中断,共用一个中断向量号,为2。

  • Linux把可屏蔽中断分为上半部分和下半部分,上半部分需要立即执行,下半部分可以推迟执行,例子见书p300。

内部中断:

  • 软中断和异常,软中断是软件主动发起的。
  • 异常是指令执行期间CPU内部产生的错误引起的,不可屏蔽。异常按照轻重程度分为:Fault(故障,可以被修复,比如缺页异常page fault),Trap(陷阱,软件掉进了CPU设下的陷阱,比如调试过程中用的int3),Abort(终止,一旦发生,操作系统为了自保只能将此程序从进程表中去掉)

异常和不可屏蔽中断的中断向量号是由CPU自动提供的,而来自外部设备的可屏蔽中断号是由中断代理提供的,软中断是由软件提供的。

当CPU接收到一个中断时,需要用中断向量在中断段描述符表中检索对应的描述符(中断门描述符),在该描述符中找到中断处理程序的起始地址(一个段描述符选择子与偏移),然后执行中断处理程序。

扫描二维码关注公众号,回复: 15330185 查看本文章

计算机为了实现对中断的高效管理,而引入了中断控制器,由它负责接收外部设备的中断,负责对所有中断进行仲裁,决定哪个中断优先被CPU受理。

接下来我们来实现一个简陋的时钟中断。大致流程如下图:
在这里插入图片描述
init_all函数用来初始化所有的设备及数据结构,我们打算在主函数中调用它来完成初试化工作。init_all首先调用idt_init,它用来初始化中断相关的内容,其初始化也要分成几部分来做,pic_init用来初始化可编程中断控制器8259A,idt_desc_init用来初始化中断描述符表IDT,最后再加载IDT。

本节代码核心逻辑:

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

首先我们先写好中断发生后的中断处理程序

p320剖析kernel.S代码:

1、代码功能

创建33个中断处理函数

2、实现原理

中断信号进入中断控制器进行处理之后,会被分配中断号,通过特定的中断号码,可以调用特地的中断处理程序去处理。0—19中断号为处理器内部固定的异常类型,20-31是Intel保留的。同时为了演示中断机理,写一个时钟中断处理程序,所以共33个。

3、代码逻辑

定义33个中断处理程序,每个程序包含处理部分与本程序的地址

4、怎么写代码?

A、定义没有压入错误码但为了统一管理需要压入0的宏参数;定义要压入错误码所以我们什么都不做的宏参数

B、定义一个文本段,里面放着要打印的字符串信息,然后定义一个标号(就在文本段下方)。由于编译器的特性,会将同一类型的SECTION组合成一个大的SEGMENT,所以D中调用宏所形成的每个中断处理程序中的入口地址部分(这个入口地址会被定义成文本段)会统一聚集在这个要打印的字符串这里(因为它是被定义成了文本段),也就是字符串信息下面的标号处,于是这个标号便可以管理所有的中断处理程序地址

C、定义一个中断处理程序宏,宏中包含程序段:程序处理部分(打印字符串信息)、文本段:本程序的入口地址部分

D、调用C定义的宏实现33个中断处理程序的定义(传不同的参数),要理清楚哪些中断要压入错误码,哪些中断不会压入错误码。不压入错误码的我们压入一串0,这样能实现中断统一定义(p303表7-1)

5、代码实现如下: myos/kernel/kernel.S

[bits 32]
%define ERROR_CODE nop		                ; 有些中断进入前CPU会自动压入错误码(32位),为保持栈中格式统一,这里不做操作.
%define ZERO push 0		                    ; 有些中断进入前CPU不会压入错误码,对于这类中断,我们为了与前一类中断统一管理,就自己压入32位的0
extern put_str			                    ;声明外部函数,为的是调用put_str

section .data
intr_str db "interrupt occur!", 0xa, 0       ;第二个是一个换行符,第三个定义一个ascii码为0的字符,用来表示字符串的结尾
global intr_entry_table
intr_entry_table:                           ;编译器会将之后所有同属性的section合成一个大的segment,所以这个标号后面会聚集所有的中断处理程序的地址

%macro VECTOR 2                             ;汇编中的宏用法见书p320
section .text                               ;中断处理函数的代码段
intr%1entry:		                        ; 每个中断处理程序都要压入中断向量号,所以一个中断类型一个中断处理程序,自己知道自己的中断向量号是多少,此标号来表示中断处理程序的入口
    %2                                      ;这一步是根据宏传入参数的变化而变化的
    push intr_str
    call put_str
    add esp,4			                    ; 抛弃调用put_str压入的字符串地址参数

                                            ; 如果是从片上进入的中断,除了往从片上发送EOI外,还要往主片上发送EOI 
    mov al,0x20                             ; 中断结束命令EOI
    out 0xa0,al                             ;向主片发送OCW2,其中EOI位为1,告知结束中断,详见书p317
    out 0x20,al                             ;向从片发送OCW2,其中EOI位为1,告知结束中断

    add esp,4			                    ;对于会压入错误码的中断会抛弃错误码(这个错误码是执行中断处理函数之前CPU自动压入的),对于不会压入错误码的中断,就会抛弃上面push的0
    iret				                    ; 从中断返回,32位下等同指令iretd

section .data                               ;这个段就是存的此中断处理函数的地址
    dd    intr%1entry	                    ; 存储各个中断入口程序的地址,形成intr_entry_table数组,定义的地址是4字节,32%endmacro

VECTOR 0x00,ZERO                            ;调用之前写好的宏来批量生成中断处理函数,传入参数是中断号码与上面中断宏的%2步骤,这个步骤是什么都不做,还是压入0看p303
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

6、其他代码详解查看书p321;以上代码的核心就是33个中断处理函数,并且这些中断处理函数的入口地址形成了一个数组。

然后我们进行IDT表建构工作,核心就是为上文写下的中断处理函数建立对应的中断描述符表。下图是中断描述符的结构,字段含义参考段描述符P152
在这里插入图片描述
p324剖析interrupt.c代码:

1、代码功能

构建IDT表,为上文写下的中断处理函数建立对应的中断描述符表

2、实现原理

依据中断描述符表格式,将中断描述符表与中断处理函数建立映射

3、代码逻辑

创建33个中断门描述符结构体,然后通过循环将中断门描述符与特定中断处理函数建立映射

4、怎么写代码?

A、定义中断门描述符结构体,并定义一个中断门描述符结构体数组(33项)

B、写一个函数make_idt_desc,通过传入中断门描述符结构体指针,属性项,特定中断处理函数地址(通过引入intr_entry_table实现引用),将中断门描述符与特定中断处理函数建立联系

C、写一个函数idt_desc_init,循环调用B函数,完成构建中断描述符表

5、代码实现如下:

先定义内核用的段描述符选择子,中断门描述符attr字段 (myos/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

接下来定义了一些数据类型(intr_handler) (myos/kernel/interrupt.h)

#ifndef __KERNEL_INTERRUPT_H
#define __KERNEL_INTERRUPT_H
typedef void* intr_handler;		//将intr_handler定义为void*同类型
#endif

核心代码: (myos/kernel/interrupt.c)

#include "interrupt.h"      //里面定义了intr_handler类型
#include "stdint.h"         //各种uint_t类型
#include "global.h"         //里面定义了选择子         
#include "print.h"

#define INTR_DESC_CONT 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];   //中断门描述符(结构体)数组,名字叫idt

extern intr_handler intr_entry_table[IDT_DESC_CNT];	    //引入kernel.s中定义好的中断处理函数地址数组,intr_handler就是void* 表明是一般地址类型

//此函数用于将传入的中断门描述符与中断处理函数建立映射,三个参数:中断门描述符地址,属性,中断处理函数地址
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(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");
}

6、其他代码详解查看书p325

接下来就是设定中断控制器。对任何硬件的控制都要通过端口,我们现在先把常用的端口读写功能利用内联汇编封装成C函数。这四个函数定义在io.h中,这样包含此.h文件就能够直接使用inline函数(原封不动展开,直接操作寄存器)。比一般的包含一个.h引入一个函数声明再链接一起要快得多,因为一般方式会涉及到call与ret指令。详细理由见p327 。内联汇编基础见p283

代码剖析略,具体代码如下: (myos/lib/kernel/io.h)

#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));
}                                       //S表示寄存器esi/si

/* 将从端口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");
}                                   //D表示寄存器edi/di                       //通知编译器,内存已经被改变了

#endif

接下来就是对中断控制器进行编程了,设定好中断控制器,并只接受来自时钟中断的信号

p330剖析interrupt.c代码:

1、代码功能

设置中断控制器,对中断控制器操作,只接受来自时钟中断的信号

2、实现原理

CPU直接与中断打交道,不仅浪费CPU强大的性能,且CPU为了接收各种各样的中断信号,将会无比冗余(cpu将会有过多引脚),引入专门的中断控制器来先处理一下中断。我们需要先设置中断处理器(初始化),然后操作它来处理对应的中断信号。设置与操作都是通过它暴露在外的寄存器来进行。操作详情见p330

3、代码逻辑

初始化中断处理器

设定只接受时钟中断信号

4、怎么写代码?

A、看书p315设定相应的ICW与OCW

第一轮设定:(因为要按照ICW1-4的顺序推送,先主后从)

主片ICW1:00010001,0x11(送入主片控制端口):

  • 0号位为1,表示要写入ICW4,x86系统必须为1;
  • 1号位为0,表示级联;
  • 2号位为0,用于是设定8085的调用时间间隔,x86不需要设置;
  • 3号位为0,表示边沿触发;
  • 4号位为1,ICW1的标记;
  • 高3位x86不需要设置,直接为0。

主片ICW2:00100000,0x20(送入主片的数据端口):ICW2用来设置起始中断向量号,由于中断控制器上的IRQ接口是按顺序排列的,所以我们这里设定的实际就是IRQ0的中断向量号。这里我们设定32(也就是第33个中断向量号),因为前32个(0-31)已经被占用了。而且只需要填入高5位,也就是填一个8的倍数,然后8295A的8个IRQ接口就在此基础上顺序排号。如第一个主片,八个接口就是,IRQ0 = 32 + 0; IRQ1 = 32 + 1… 第一个从片就是IRQ0 = 32 + 8 + 0, IRQ1 = 32 + 8 + 1;

主片ICW3:00000100,0x04(送入主片数据端口):8位中哪位置1,表示哪个IRQ与从片连接,前面的值表示主片的IRQ2用于与从片级联。

主片ICW4:00000001,0x01(送入主片数据端口):

  • 0号位为1,表示x86处理器;
  • 1号位为0,表示手动结束中断(我们的中断处理程序中有通知主从片结束中断的步骤);
  • 2号位为0,因为3号位设定为0(非缓冲模式工作),所以此位无用;在非缓冲模式下,8259A的数据总线直接连接到系统总线上,而不是通过缓冲器。当中断发生时,8259A会直接向CPU发送中断信号,而不经过任何缓冲或处理。这种模式可能会使系统在处理大量的中断请求时表现得不那么稳定,因为它对系统总线的要求更高。然而,非缓冲模式的系统设计会更简单一些,因为不需要缓冲器的附加硬件。
  • 4号位为0,表示全嵌套模式,也就是优先处理较低中断请求线编号的中断请求(IRQ0最优先),特殊全嵌套模式是可以允许在中断处理过程中,如果来了一个优先级更高的中断请求,就暂停当前正在执行的中断,转而去执行那个优先级更高的中断请求;
  • 高3位无用。

第二轮设定:

从片ICW1:00010001,0x11(送入从片控制端口):含义参照主片ICW1。

从片ICW2:00101000,0x28(送入从片的数据端口):主片起始中断向量号是32,主片自己8个IRQ,所以从片自然从40开始。

从片ICW3:00000010,0x02(送入从片数据端口):用来表明主片哪个IRQ与自己级联,前面的值表明是主片的IRQ2用于与自己级联。

从片ICW4:00000001,0x01(送入从片数据端口),含义参照主片ICW4。

设定只接受时钟中断的OCW1:

主片OCW1:11111110,0xfe(送入主片数据端口),我们先只打开时钟中断看看效果,而时钟中断在主片IRQ0上,所以OCW1的0号位置为0,表示放行IRQ0送入的中断信号。

从片OCW1:11111111,0xff(送入从片数据端口),从片中断信号全部屏蔽

B、将A设定好的按照P330步骤推送至中断控制器的特定寄存器(通过io.h中封装的函数),并封装成一个pic_init函数

5、新加入的代码如下: (myos/kernel/interrupt.c)

#include "io.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

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

6、其他代码详解查看书p331

接下来我们就调用之前我们写好的函数,并定义加载到IDTR寄存器中的值(参照书P306图IDTR结构),并最终加载IDTR来完成整个idt的构建工作,并封装成一个函数idt_init。

myos/kernel/interrupt.c 新加入如下代码,代码剖析略

/*完成有关中断的所有初始化工作*/
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_init,整个idt的建构工作就完成了。

myos/kernel/interrup.h 中声明idt_init函数,其他文件只需要包含interrupt.h就可以调用idt_init函数

myos/kernel/interrupt.h中加入如下代码:

void idt_init(void);

我们写一个init_all调用上面写好的idt_init

myos/kernel/init.c 代码剖析略

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

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

为了其他的函数调用我们的init_all,我们需要建立头文件init.h,声明函数init_all,其他的函数包含我们的头文件,就可以调用我们的函数

myos/kernel/init.h 代码剖析略

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

最后,我们来写一个main.c来验证我们的之前关于中断的所有工作的正确性

myos/kernel/main.c 代码剖析略

#include "print.h"
#include "init.h"
void main(void)
{
    
    
    put_str("\nThis is Kernel!\n");
    init_all();
    asm volatile
    (
        "sti"   //为了演示中断,这里先临时开启中断
    );
    while(1);
    
}

接下来进行编译,为了目录不至于太乱,建立build目录(myos/build)用于将所有目标文件和编译后的内核文件都放在此目录中

nasm -f elf -o build/print.o lib/kernel/print.S

gcc-4.4 -I lib/kernel/ -I lib/ -I kernel/ -c -fno-builtin -o build/main.o -m32 kernel/main.c (-fno-builtin是不使用GCC的内建函数https://blog.csdn.net/baiyu9821179/article/details/73007124)

nasm -f elf -o build/kernel.o kernel/kernel.S

gcc-4.4 -I lib/kernel/ -I lib/ -I kernel/ -c -fno-builtin -o build/interrput.o -m32 kernel/interrupt.c

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/kernel.o build/main.o build/init.o build/interrput.o build/print.o

dd if=build/kernel.bin of=/home/rlk/Desktop/bochs/hd60M.img bs=512 count=200 seek=9 conv=notrunc

我们程序的含义,就是每发生一次时钟中断,就打印一次信息!

之前所做的无非是为了演示中断机制的原理,非常简单,现在要完善中断处理机制。主要就是让每个中断都有自己对应的处理函数,而不是像我们之前那样只是打印一个字符串。

但是我们不太可能采取在汇编中(kernel.S)直接定义中断处理函数,原因:1、汇编编写太麻烦;2、用一个kernel.S来维护所有中断处理函数很不方便;所以我们需要这样一种机制:1、用C语言编写中断处理函数;2、当中断发生的时候,从我们之前写好的kernel.S中跳到我们C语言编写的中断处理函数中执行。接下来,我们来实现这样的机制。

p335,p337剖析interrupt.c与kernel.S新加入代码:

1、代码功能

用C语言定义中断处理函数,然后形成一个中断入口地址表。按照之前定义好的中断处理机制,当中断发生时,系统会用中断号找到对应的中段描述符,然后跳往汇编中我们定义好的中断处理函数(之前是打印字符串)。现在修改这个汇编中的中断处理函数,让其只是做简单处理(如保存程序执行环境),然后调用之前C中形成的中断入口地址表找到C中定义的中断处理函数去执行。汇编中的中断处理部分与C中的中断处理部分关系如下:
在这里插入图片描述
2、实现原理

在原有中断处理机制上修改

3、代码逻辑

在C语言中定义中断处理函数,然后形成地址数组管理C语言中的中断处理函数;汇编通过C形成的地址管理数组去调用C中的中断处理函数

4、怎么写代码?

在interrupt.c中

A、建立一个地址数组用来存储中断处理函数的地址,一个字符串指针数组用来存储中断的名字

B、我们先写一个通用的中断处理函数,功能是打印中断向量号,参数是中断号码

C、将地址数组中每个元素初始化为B写的中断处理函数地址(后面再改);将字符串指针数组对照中断号填入中断名字(书p303),并封装成一个函数execption_init()

D、在idt_init(用来完成中断初始化的函数)中,调用exception_init;

在kernel.S中

A、引入interrupt.c中定义的中断处理程序地址数组

B、中断处理函数模板中删除打印字符串的部分

C、中断处理函数模板中加入保存上下文代码

D、中断处理函数模板中加入压入中断号代码(方便调试)与调用C中形成的中断地址数组找到中断处理函数地址的代码

E、定义恢复上下文环境并退出中断的代码,并在D中调用

5、代码实现如下:

myos/kernel/interrupt.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) {
    
    	//伪中断向量,无需处理。详见书p337
      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数组中的函数是在进入中断后根据中断向量号调用的,
 * 见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";

}


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

myos/kernel/kernel.S修改代码:

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

section .data
global intr_entry_table
intr_entry_table:                           ;编译器会将之后所有同属性的section合成一个大的segment,所以这个标号后面会聚集所有的中断处理程序的地址

%macro VECTOR 2                             ;汇编中的宏用法见书p320
section .text                               ;中断处理函数的代码段
intr%1entry:		                        ; 每个中断处理程序都要压入中断向量号,所以一个中断类型一个中断处理程序,自己知道自己的中断向量号是多少,此标号来表示中断处理程序的入口
    %2                                      ;这一步是根据宏传入参数的变化而变化的
                                            
    push ds                                 ; 以下是保存上下文环境
    push es
    push fs
    push gs
    pushad



    push %1			                        ; 不管idt_table中的目标程序是否需要参数,都一律压入中断向量号,调试时很方便
    call [idt_table + %1*4]                 ; 调用idt_table中的C版本中断处理函数
    jmp intr_exit

section .data                               ;这个段就是存的此中断处理函数的地址
    dd    intr%1entry	                    ; 存储各个中断入口程序的地址,形成intr_entry_table数组,定义的地址是4字节,32%endmacro

section .text
global intr_exit
intr_exit:	     
                                            ; 以下是恢复上下文环境
    add esp, 4			                    ; 跳过中断号
    popad
    pop gs
    pop fs
    pop es
    pop ds
    add esp, 4			                    ;对于会压入错误码的中断会抛弃错误码(这个错误码是执行中断处理函数之前CPU自动压入的),对于不会压入错误码的中断,就会抛弃上面push的0 
                                                ; 如果是从片上进入的中断,除了往从片上发送EOI外,还要往主片上发送EOI 
    mov al,0x20                             ; 中断结束命令EOI
    out 0xa0,al                             ;向主片发送OCW2,其中EOI位为1,告知结束中断,详见书p317
    out 0x20,al                             ;向从片发送OCW2,其中EOI位为1,告知结束中断
    iretd				                    ; 从中断返回,32位下iret等同指令iretd
        
      

6、其他代码详解查看书p336

编译,链接,写入。由于我们只是在原来的代码中新加入了一种管理机制,现在时钟中断仍然是开启的。当时钟中断发生后,系统会根据中断控制器提供的中断号码查询中断描述符,然后去执行中断描述符中的中断函数地址指向的汇编文件写成的中断函数。而汇编文件中的中断函数又通过call[IDT_TABLE+中断号*4]指令跳入C文件管理的中断处理函数中,这里我们初始化成了一个一般的中断处理函数(功能是打印中断号码)。

当发生中断时,处理器会自动压入eflags、cs、eip、error_code(如果有的话)。结合我们之前的代码,当中断发生时,会压入如下值在栈空间中
在这里插入图片描述
时钟是一种使设备间相互配合而避免发生冲突的节拍,可以分为外部时钟和内部时钟。内部时钟由晶体振荡器产生,是指处理器中内部元件,如运算器、控制器的工作时序,主要用于控制、同步内部工作过程的步调,是ns级,无法改变。外部时钟是指处理器与外部设备或外部设备之间通信时采用的一种时序,一般为ms或s级。外部时钟和内部时钟是两套独立运行的定时体系,所以当涉及计算机内部与外部配合工作时,就必须要协调二者的时钟,定时计数器就是用来做这个事情的。

外部定时有两种方法,一种是用软件实现(p347),但是会让处理器空转,浪费处理器资源;另一种是硬件实现,也就是定时计数器,其功能就是达到了所计数的时间,计数器可以自动发出一个输出信号,可以用该信号向处理器发出中断。分为可编程定时器与不可编程定时器两种。一般有两种计时方式,正计时与倒计时。

接下来我们编写代码,来提高时钟中断的频率

p354剖析timer.c代码:

1、代码功能

提高时钟中断的频率

2、实现原理

通过指定端口号,设置可编程定时器

3、代码逻辑

设置可编程定时器

4、怎么写代码?

A、往控制寄存器端口0x43中写入控制字(设置方法详见书p350)

  1. SCI1位与SCI0位置为00,意为操作定时器0。因为个人计算机中,定时器0专用于时钟中断

  2. RW1位与RW0位置为11,意为选择读写方式,这里选择先写入低字节,然后写入高字节

  3. M2位、M1位与M0位置为10,意为设置工作方式,这里设置为比率发生器(p352)

  4. BCD位置为0,意为指计数器的计数方式是二进制

B、在所指定使用的计数器端口(0x40)中写入计数初值,因为我们要设定每秒100个中断,所以初值为1193180/100

C、A与B封装成一个函数frequency_set,定义一个timer_init调用前者

D、然后将在init_all中调用timer_init

5、代码实现如下:myos/device/timer.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未,所以最后送入counterj_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");
}

6、其他代码详解查看书p355

为计时器的初始化函数timer_init在对应头文件timer.h中进行函数声明 myos/device/timer.h

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


修改init.c,调用计时器的初始化函数 myos/kernel/init.c

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

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

​ 注意:在我的虚拟机上,时钟频率高于4031HZ,就会出现GP中断,中断号为D。书上的代码由于疏忽 outb(counter_port, (uint8_t) counter_value>>8 ),写入counter0的初值高字节是全0,实际时钟频率会有7000HZ多

猜你喜欢

转载自blog.csdn.net/kanshanxd/article/details/130962438