四,中断:中断程序(汇编和C语言)、idt、IDTR、8259A、8253以及发生中断时候的压栈细节和特权级保护

中断:

由于CPU获知了计算机中发生的某些事,CPU暂停正在执行的程序,转而去执行处理该事件的程序,当这段程序执行完毕后,CPU继续执行刚才的程序,整个过程称为中断处理,也称为中断。没有中断,操作系统几乎什么都做不了,操作系统就是中断驱动的。首先,操作系统就是个死循环,这个死循环什么都做不了,仅仅是保持操作系统能够周而复始的运行下去,运行的目的就是为了等待中断发生。
中断按事件来源分类,来自CPU外部的中断就称为外部中断,来自CPU内部的中断称为内部中断。

外部中断:

CPU提供了两条信号线。外部硬件的中断是通过两根信号线通知CPU的,这两根信号是INTR(可屏蔽中断)和NMI(不可屏蔽中断)
四,中断:中断程序(汇编和C语言)、idt、IDTR、8259A、8253以及发生中断时候的压栈细节和特权级保护_第1张图片
可屏蔽中断可以1,通过lags寄存器的IF位将所有这些外部设备的中断屏蔽。2,通过中断代理8259A来屏蔽单个设备的中断。

内部中断:

内部中断分为软中断和异常。
软中断:就是软件主动发起的中断,具有主观性,它来自于软件运行中指令int 8位立即数(系统调用)、 int3等
异常:指令执行期间CPU内部产生的错误引起的(除以0等)。不受eflags的IF影响,无法向用户隐瞒
总结:外部中断的NMI和内部中断都可以无视IF位。

中断向量号:

中断的本质就是来一个中断信号后,调用相应的中断处理函数。范围0-255。为每一个中断信号分配一个中断号,然后用此中断号作为中断描述符表的索引,找到对应的表项,进而在表项找到对应的地址,从而转去相应的处理程序。
异常和不可屏蔽中断的中断向量号是由CPU自动提供的,来自外部设备(时钟、键盘、硬盘)的可屏蔽中断号是由中断代理设置的,软中断是由软件提供的(int 8位立即数)。

中断描述符表

中断描述符表是保护模式下用于储存中断处理程序入口地址的表。中断描述符表中的中断描述符称为---门。Linux系统只用了中断门
四,中断:中断程序(汇编和C语言)、idt、IDTR、8259A、8253以及发生中断时候的压栈细节和特权级保护_第2张图片

中断门包含了中断处理程序所在段的段选择子和段内偏移地址。当通过此方式进入中断后,eflags中的 IF 位自动置0,即进入中断后,自动把中断关闭,避免嵌套。Linux就是利用中断门实现了系统调用:int 0x80。中断门只能在IDT中。

16位的表界限可以容纳中断描述符的个数为:64KB/8=8192,但是处理器只支持0-255。

中断处理过程及保护

完整的中断过程分为CPU外和CPU内两部分:
CPU外:外部设备的中断由中断代理芯片接受,处理后将该中断向量号发送到CPU
CPU内:CPU执行该中断向量号对应的中断处理程序

处理器内部的过程:

1,处理器根据中断向量号定位中断门描述符
处理器用中断向量号乘以8后与IDTR中的中断描述符表地址相加,所求的地址之和便是该中断向量号对应的中断描述符的线性地址,然后经过页部件翻译得到真实地址。
2,处理器进行特权级检查
若中断由软中断int n,int3(系统调用等)引起,则要进行当前的CPL和门槛DPL_Gate、门框:目标代码段DPL的检查。
若中断是外部设备和异常引起的,只检查当前CPL和目标代码段的DPL,数值上CPL > 目标代码段DPL。
3,执行中断处理程序
特权级检查后,将门描述符目标代码段选择子加载到代码段寄存器CS中,把门描述符中中断处理程序的偏移地址加载到EIP,开始执行中断处理程序。
四,中断:中断程序(汇编和C语言)、idt、IDTR、8259A、8253以及发生中断时候的压栈细节和特权级保护_第3张图片

中断发生后eflags中的 NT 位和 TF 位会被置 0 。用的是中断门的话eflags 的 IF 位会被自动置0,防止中断嵌套,即处理器会将这个中断完全执行完毕后才能出来下一个中断。从中断返回的指令是 iret ,它从栈中弹出数据到寄存器的 cs、eip eflags 等,根据特权级是否改变,判断是否恢复旧栈,即将SS_old 和 ESP_old 位置的值弹出到寄存器 ss和 esp。

中断发生时的压栈

特权级发生转移时:用户进程正在执行时候发生了中断

1,当前进程被中断打断后,处理器根据中断向量号找到对应的中断描述符,拿CPL和中断门描述符中选择子对应的目标代码段的DPL对比,若CPL权限比DPL低,即数值上CPL >DPL,表示要想高特权级转移,需要切换高特权级的栈。即:从用户进程中断去执行内核中断程序。中断执行完返回时也要回到之前的旧栈,所以处理器临时将SS_old 和 ESP_old 保存到一个内存中,然后CPU取出TSS中目标代码段的DPL 级别的栈加载到寄存器 SS 和 ESP 中,记作 SS_new 和 ESP_new,CPU在找到之前的旧栈,将其压入新栈中:将 SS_old 用0 扩展高16位,成为32位数据。

2,在新栈中压入EFLAGS寄存器

3,将要CS_old 和 EIP_old 压入新栈:CS_old 需要用0 来填充高16位为0。

4,某些异常有错误码,错误码用于报告哪个段上发生了异常,所以ERROR_CODE也压入栈。

四,中断:中断程序(汇编和C语言)、idt、IDTR、8259A、8253以及发生中断时候的压栈细节和特权级保护_第4张图片

特权级未发生转移时发生的中断:正在执行内核代码发生了中断

1,在新栈中压入EFLAGS寄存器

2,将要CS_old 和 EIP_old 压入新栈:CS_old 需要用0 来填充高16位为0。

3,某些异常有错误码,错误码用于报告哪个段上发生了异常,所以ERROR_CODE也压入栈。

四,中断:中断程序(汇编和C语言)、idt、IDTR、8259A、8253以及发生中断时候的压栈细节和特权级保护_第5张图片

中断返回的出栈:

用 iret 指令实现,iret 指令并不知道栈内容的正确性,一次弹出4字节,所以在使用iret 之前,一定要调整好esp的位置,使其依次弹出:EIP、CS、EFLAGS、ESP、SS。因此我们要手动跳过 ERROR_CODE。

1,当处理器执行到 iret 时,需要从栈中返回。CPU首先检查CS_old 的选择子的 RPL 位,判断在返回过程中是否要改变特权级。弹出 EIP_old 和 CS_old 。

2,弹出 EFLAGS。若需要改变特权级,则弹出旧栈。若不需要改变特权级,则是平级转移。

如果需要改变特权级,CPU还会检查数据段寄存器DS、ES、FS、GS的内容,如果它们之中某个寄存器的选择子所指向的数据段描述符的DPL权限比返回后的CPL高,则处理器将该段寄存器置为 0 。

可编程中断控制器8259A

8259A的作用是负责所有来自外设的中断,如时钟的中断、键盘的中断等等。8259A它用于管理和控制可屏蔽中断,表现在可以屏蔽想要屏蔽的外设中断,对外设中断实行优先级判决,向CPU提供中断向量号等功能。每个独立运行的外部设备就是一个中断源,可以发中断信号给8259A。
个人电脑中只有两片 8259A 芯片,共16 个 IRQ 接口。
四,中断:中断程序(汇编和C语言)、idt、IDTR、8259A、8253以及发生中断时候的压栈细节和特权级保护_第6张图片
8259A芯片的内部结构:
四,中断:中断程序(汇编和C语言)、idt、IDTR、8259A、8253以及发生中断时候的压栈细节和特权级保护_第7张图片


8259A的编程

8259A内部有两组寄存器,一组是初始化命令寄存器,用来保存初始化命令字,ICW1-ICW4。另一组寄存器是操作命令寄存器组,用来保存操作命令字,OCW1-OCW3。我们对8259A的编程,也分为初始化和操作两部分。

1,用ICW作初始化:确定是否需要级联、设置起始的中断向量号、设置中断结束模式等等

ICW1用来初始化8259A的连接方式:单片还是级联       中断信号触发方式:电平触发还是边沿触发

ICW2用来设置起始中断向量号。指定了 IRQ0 映射到的中断向量号,其他 IRQ 接口对应的中断向量号会顺着自动排列下去。

ICW3用来设置级联方式下主片和从片用哪个IRQ接口互连。

ICW4用来设置中断结束模式:自动还是手动。

2,用OCW来操作控制:中断屏蔽、中断结束

OCW1用来屏蔽连接在8259A上的外部设备中的中断信号。这些没有屏蔽的中断最终还是要受到eflags的IF位管束,IF=0,不管8259A怎么样设置全部屏蔽。

OCW2用来设置中断结束方式:发EOI来终止某一个中断      优先级模式:循环还是固定

OCW3用来设置特殊屏蔽方式和查询方式。

ICW1和OCW2、OCW3是用偶地端口 0x20(主片)或 0xA0(从片)写入。

ICW2-ICW4是用奇地址端口 0x21(主片) 或0xA1(从片)写入。

4个ICW要保证一定的次序写入,8259A就知道写入端口的数据是什么意思了。

OCW的写入顺序无关,各控制字有唯一标识可以辨别。

所以8259A 的编程步骤为:

1,首先必须进行初始化,必须依次写入 ICW1、ICW2 、ICW3 、ICW4

2,初始化后,可以进行OCW的操作。

中断处理程序

Intel的8259A芯片位于主板的南桥芯片中。主片IR0引脚上就是时钟中断,这已经由内部电路实现了。我们只打开时钟中断,屏蔽其他外部设备的中断。中断向量号的前32个(0-31)都被系统的内部中断给占用了,所以我们的时钟中断向量号只能从0x20开始。我们设置0x20为时钟中断的向量号。所以我们要在中断描述符中的第33个中断描述符中加载时钟中断处理程序的选择子和偏移地址。因此我们要定义33个描述符的中断描述符表。由于我们可以屏蔽的中断信号只能是外部设备的可屏蔽中断信号。所以当处理器内部发生内部中断时也会产生0-31的向量号,到时候也会到中断描述符中去找中断处理程序的地址。这些程序都需要我们来写。所以我们写的程序就有中断向量号为0-31这32个内部中断的处理程序和中断向量号为32的时钟中断处理程序。

宏属于预处理指令。预处理指令被编译器的预处理器支持,属于伪指令。这类指令是在编译前,编译器需要预先处理的指令,预处理器会将这些伪指令展开替换成具体的编译器可以识别的语言符号,在预处理后,其中的预处理指令(伪指令)全部都会不见的。比如#include<>、#define等都是预处理指令,都会被预处理器在预处理阶段都替换成具体所代表的代码或数字。

宏:Macro。用来代替重复性输入,是一段代码的模板。

定义单行的宏,汇编中可以用%define 指令实现。

定义多行的宏,汇编中可以用%macro来实现。

%macro 宏名字+参数个数
宏代码体 		;要引用某个参数,需要用“% 数字” 的方式来引用。参数序号从 1 开始
%endmacro

总结我们的中断建立的步骤为:

1,编写中断处理程序,记录每个中断程序的地址

2,构造出中断描述符表,将中断程序的地址加载进去

3,设置完成8259A

4,加载IDTR

5,将eflags的IF位置1

我们采取汇编语言和C语言相结合的方式来编写中断处理程序。因为C语言编写程序更方便,我们用C语言来写中断处理程序。发生中断后,首先执行汇编语言写的中断处理程序,然后在调用C语言写的中断处理程序执行。即汇编中的中断程序只是提供了中断处理程序的入口,进入后在转去执行C语言的中断处理程序,C语言的程序才是真正的处理程序。

在内部中断的前32个中,有些中断类型有错误码,系统就自动压入了错误码,有些中断类型没有错误码,系统没有错误码,为了保持一致性,所以我们要统一压入错误码。

中断程序的汇编中断处理代码:只是提供一个入口功能,然后转去C中断处理程序。我们要编写33个汇编中断处理程序:0-32是内部中断的处理程序,33是时钟的中断程序。

[bits 32]
%define ERROR_CODE nop		 ; 若在相关的异常中cpu已经自动压入了错误码,为保持栈中格式统一,nop是不做操作.
%define ZERO push 0		 ; 若在相关的异常中cpu没有压入错误码,就手工压入一个0

extern idt_table		 ;idt_table是C中注册的中断处理程序数组,数组元素是C语言的中断处理程序的地址。

section .data
global intr_entry_table		;定义intr_entry_table为全局变量数组名,每个数组元素为程序的地址
intr_entry_table:		;因为这个标号是在 .data段内的,所以这个标号指的是 .data里面的地址

%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
;0-31个中断向量号是内部中断,有些有错误码,有些没有。32中断向量号为时钟中断
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

编译器编译程序时会按照每个节的属性,将相同属性的节放在一起的。所以我们编译器会分别将代码段和数据段放在一起的。因为预处理器会在编译前提前将代码展开,所以最后 .data放在一起,可以构成一个数组,数组元素是代码段的地址。 idt_table的数组元素是每一个C处理程序的地址。所以 call  [idt_table+4*num]==call 处理函数地址。

eflags、cs、eip、error_code是CPU自己压入栈的,我们不需要管,我们要负责的是其他入栈参数。注意 iret 时候要跳过error_code 使esp 指向eip。

四,中断:中断程序(汇编和C语言)、idt、IDTR、8259A、8253以及发生中断时候的压栈细节和特权级保护_第8张图片


中断的C语言中断处理程序:里面是真正的中断处理程序。这里我们只设置一个通用的,33个中断都是这一个中断函数。将中断函数的地址循环33编放入数组中即可。

intr_handler idt_table[IDT_DESC_CNT];  //定义C语言中断处理程序数组
/* 通用的中断处理函数,一般用在异常出现时的处理 */
//功能是:出现异常时显示中断向量号;
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[i] = general_intr_handler;      // 默认为general_intr_handler。
}
}

中断描述符表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;  
};

//定义中断描述符表,有33个门描述符,0-31中断向量号被系统占用,我们从32(0x20)开始定义时钟中断
static struct gate_desc idt[IDT_DESC_CNT];

//声明出中断程序入口地址数组,有33个地址,0-31的地址属于内部中断程序地址,0x20的地址属于时钟中断程序地址
//这些地址在汇编的中断程序中已经得出
extern intr_handler intr_entry_table[IDT_DESC_CNT];  //汇编中断处理地址的中的全局数组,元素是每个中断程序的入口地址

//声明中断描述符构造函数
static void make_idt_desc(struct gate_desc* p_gdecs,uint8_t attr,intr_handler function);
//门描述符构造函数
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;
}

//构造出33个门描述符
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");
}

8259A设置程序

设置头文件io.h。通过这个头文件来读写端口。

static函数的作用:普通的函数名是全局变量,在一个文件中定义后,在所有的文件中都可以使用。static函数,在一个文件中定义后,只能在那个文件中使用,其他文件不能用。

inline函数的作用:当主调函数调用被调函数时候,主调函数要压栈,call,然后csip转过去执行等等,被调函数也要备份等等,速度很慢。inline函数是当主调函数调用被调函数时,直接在调用出原封不动的将被调函数的函数体展开,这样就不用call了,这就不属于函数调用了,csip的值也不用变,依次执行即可。

我们将static函数放在头文件中的目的在于:因为这是对端口进行操作,所以需要的条件就是一定要快。static函数放在头文件中,那么所有包含该头文件的文件都有这个static函数,static函数执行起来比普通函数要快很多,因为静态函数会被自动分配在一个一直使用的静态存储区,直到退出应用程序才清空这个区域,而普通的函数还生成了一些其他信息来保证安全,所以static函数速度快很多。

内存循环复制三剑客:cld、(ES:ESIES:EDI)、repmovs[dw]

端口循环读写三剑客:cld、(es:edi)、rep ins[dw]      cld、(es:esi)、rep outs[dw]

两个指令都是在执行 rep前先读取ecx的值与0比较,循环执行后ecx的值减1,然后esiedi自动加上相应的字节。

1,首先我们编写端口读写函数,不管什么端口读写,调用即可。

#ifndef _LIB_IO_H
#define _LIB_IO_H
//向端口port写入一个字节.N为立即数约束:表示0-255个端口号
static inline void outb(unit16_t port, unit_t data)
{
	asm volatile("outb %b0,&w1"::"a"(data),"Nd"(port));//b和w来限制寄存器大小
}

//将addr处起始的 word_cnt 个字写入端口port。
//执行rep指令前先拿ecx与0比较,不相等则可以执行,执行完后edi加一个字,ecx减1。在执行rep之前再比较
static inline void outsw(uint16_t port,const void* addr,unit32_t word_cnt)
{
	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)
{
	asm volatile("cld;rep insw":"+D"(addr),"+c"(word_cnt):"d"(port):"memory");
}
#endif

2,当我们设置8259A时,我们调用端口读写函数即可

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


加载IDTR

根据IDTR的结构,采用内联汇编的形式加载进去。

数组名便是指针,所以,

 uint64_t idt_operand = ((sizeof(idt) - 1) | ((uint64_t)(uint32_t)idt << 16));//将idt的基地址左移16位,
 asm volatile("lidt %0" : : "m" (idt_operand));

将eflags的IF位置1

asm volatile("sti");

到此为止,时钟中断设置完成,在主函数中依次初始化idt、pic(8259A)、idtr,然后开中断即可。


可编程计数器/定时器8253

计算机时钟分为两类:内部时钟和外部时钟

内部时钟:是内部元件,位于主板上,由晶体振荡器产生,其频率经过分频之后就是主板的外频,外频乘以某个倍数就是主频。内部定时无法改变。

外部时钟:是处理器与外部设备之间通信采用的一种时序

内部时钟和外部时钟是两套独立运行的定时体系。对于外部定时,我们有两种实现方式:一种软件实现,一种硬件实现为定时器。

计时器的功能就是定时发出信号,当到达所计数的时间,计数器可以发出一个中断信号,处理器转去执行相应的中断处理程序。计数器内部有计算脉冲信号,每一个时钟脉冲信号到来时都会修改计数值。

8253用的是 倒计时 的方式。比如为了放在DRAM(内存卡)数据丢失,每隔一段时间就要进行充电刷新,就是利用这个向CPU发出中断去刷新的。我们设置8253来调整时钟中断发生的频率。

四,中断:中断程序(汇编和C语言)、idt、IDTR、8259A、8253以及发生中断时候的压栈细节和特权级保护_第9张图片四,中断:中断程序(汇编和C语言)、idt、IDTR、8259A、8253以及发生中断时候的压栈细节和特权级保护_第10张图片

 每个计数器有3个引脚

CLK:时钟输入信号,即计数器自己的时钟频率,连接到8253的CLK的脉冲频率为 2MHZ。

GATE:门控输入信号

OUT:计数器输出信号,当计数值为0时,用于通知处理器或者某个设备:定时完成

计数开始之前的计数初值保存在计数初值寄存器中,减法计数器将此值载入后,CLK 收到一个脉冲信号,减法计数器就将计数值减1,同时将当前值保存到输出锁存器中。当计数值为0时,OUT引脚输出中断信号或者启动某个设备工作。

四,中断:中断程序(汇编和C语言)、idt、IDTR、8259A、8253以及发生中断时候的压栈细节和特权级保护_第11张图片

因此我们使用计数器0来产生时钟信号,这个时钟是连接饿到 IRQ0 引脚上的那个时钟,即计数器0决定了定时中断信号的发生频率。因此计数器0的计时到期后就会发出中断时钟中断信号,中断代理8259A就会感知引脚 IRQ0 有中断信号到来。

我们通过控制字寄存器来设置计数器0的工作方式。我们通过控制字寄存器选择用哪个计数器,指定该计数器的控制模式,再用该计数器写入计数初值就行。

默认情况下,三个计数器的工作频率位 1.19318MHz,即一秒内有 1193180 次脉冲信号,一秒内会减1193189次1,计数器0的默认值是0,即65536,那么一秒内的输出信号次数大约为 : 1193180/65536=18.206,所以时钟中断信号的频率为18.206Hz,大约55毫秒发生一次中断。这就是我们默认情况下的时钟中断频率了。

我们的目的是为了使中断发生的快一点。默认频率为18.206Hz,我们调整为100Hz,即一秒钟100次中断。我们通过8253,通过控制字指定使用计数器0,工作方式选择方式2(比率发生器),并为附上合适的初值 1193180/100=11932。

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

获取当前中断状态并开关中断

可以定义枚举来表示中断状态

enum intr_status
{
   INTR_OFF;
   INTR_ON;
}

获取当前eflags的中断状态状态

我们定义宏函数,首先先看一下宏函数:

宏函数实现:

#define MAX( a, b) ( (a) > (b) (a) : (b) )

普通函数来实现:

int max( int a, int b)
{
return (a > b? a : b);
}

很显然,我们不会选择用函数来完成这个任务,原因有两个:

1,普通的函数会带来额外的开销:将实参压栈、压入地址、弹出地址、释放堆栈等等。这种开销不仅会降低代码效率,而且代码量也会大大增加,而使用宏定义则在代码调用处直接展开,省去了很多工作;

2,普通函数的参数必须被声明为一种特定的类型,所以它只能在类型合适的表达式上使用,我们若要改变类型,只能另外写一个函数。反之,宏定义可以用于整形、长整形、单浮点型、双浮点型以及其他任何可以用“>”操作符比较值大小的类型,也就是说,宏是与类型无关的。

不过和使用函数相比,使用宏的不利之处在于每次使用宏时,一份宏定义代码的拷贝都会插入到程序中。除非宏非常短,否则使用宏会大幅度增加程序的长度。

//返回eflags寄存器到变量中
#define GET_EFLAGS(EFLAG_VAR) asm volatile("pushfl;popl %0":"=g"(EFLAG_VAR))
//获取当前状态
enum intr_status intr_get_status()
{
 int eflags=0;
 GET_EFLAGS(eflags);
 return (EFLAGS_IF & eflags) ? INTR_ON:INTR_OFF;
}
//开中断并返回开中断前的状态
enum intr_status intr_enable()
{
	enum intr_status old_status=INTR_ON;
	if(old_status==intr_get_status())
		return old_status;
	old_status=INTR_OFF;
	asm volatile("sti");
	return old_status;
}

//关闭中断并返回中断前的状态
enum intr_status intr_enable()
{
	enum intr_status old_status=INTR_OFF;
	if(old_status==intr_get_status())
		return old_status;
	old_status=INTR_ON;
	asm volatile("cti");
	return old_status;
}



你可能感兴趣的:(操作系统)