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

本篇对应书籍第七章的内容

本篇内容介绍了操作系统的中断处理机制,建立中断描述符表,填充门描述符,以及中断处理程序,初始化8259A中断控制器实现外部中断功能,控制8253定时计数器实现中断频率的提升

中断 是什么

CPU 暂停正在运行的程序,转而去运行其他程序,处理完在回来执行刚才的程序,这个过程叫做中断处理,也叫中断。

操作系统是个死循环,在死循环的过程中,等待事情的发生,当有事情发生了,就会转而去处理这个事情,事情是通过中断来告知操作系统的。

操作系统是中断驱动的。

中断 的分类

把中断按事件来源分类,来自 CPU 外部的中断就称为外部中断,来自 CPU 内部的中断称为内部中断。

外部中断按是否导致宕机来划分,可分为可屏蔽中断和不可屏蔽中断两种。

内部中断按是否正常来划分,可分为软中断和异常。

外部中断

外部中断的中断源是硬件。外部硬件的中断通过两根信号线通知 CPU 的,分别是 INTR(INTeRrupt) 和 NMI(Non Maskable Interrupt):

  • 通过 INTR 引脚进入 CPU 的是可屏蔽中断,即使屏蔽也不会对 CPU 造成什么影响;
  • 通过 NMI 引脚进入 CPU 的是不可屏蔽中断,一般问题很大。

在 Linux 中,可屏蔽中断分为上半部和下半部:

操作系统是中断驱动的,中断发生后会执行相应的中断处理程序,中断处理程序中需要立即执行的部分在上半部,完成中断应答或硬件复位等重要紧迫工作。中断处理程序中不紧急的部分则被推迟到下半部中去完成。上半部是在关中断的情况下执行,不可被打扰,下半部则不是。

中断发起时,相应的中断向量号通过 NMI 或 INTR 引脚被传入 CPU,中断向量号是中断向量表中断描述符表里中断项的下标,CPU 根据此中断向量号在中断向量表或中断描述符表中检索对应的中断处理程序并去执行。

中断向量表、中断描述符表中存储的是中断号以及中断号对应的处理程序的位置

内部中断

内部中断可分为软中断和异常。

软中断就是由硬件主动发起的中断,因为它来自于软件,所以称之为软中断。

发起中断的指令:

  • int 8位立即数,一般用于系统调用。
  • int3,调试断点指令,其所触发的中断向量号是 3。我们用 gdb 或 bochs 调试程序时,实际上就是调试器 fork 了一个子进程,用于执行被调试的程序。调试器中经常要设置断点,其原理就是父进程修改了子进程的指令,将其用 int3 指令替换,从而子进程调用 int3 触发中断。
  • into,中断溢出指令,中断向量号是 4。
  • bound,检查数组索引越界指令,触发 5 号中断。
  • ud2,未定义指令,触发 6 号中断。

除了int 8位立即数以外的中断也可以算作是异常,异常是指令执行期间 CPU 内部产生的错误引起的,不受 eflags 里的 IF 位约束(只要中断关系到正常运行,就不受 IF 位影响)。

异常按照轻重程度可分为以下三种:

  • Fault,故障,可以被修复,例如 Linux 的虚拟内存就是基于 缺页异常 page fault 的。
  • Trap,陷阱,通常在调试中使用到。
  • Abort,终止,错误无法修复,一般是硬件出错或系统结构出错。

处理器所支持的 256 种中断:

最左边一列就是中断向量号,类似段选择子,不过中断向量号是从中断描述符表中索引中断描述符,其中没有RPL字段,中断号来源:

  • 异常和不可屏蔽中断的中断向量号是 CPU 提供的
  • 来自外设的可屏蔽中断号由中断代理提供的
  • 软中断是由软件提供的

中断描述符表

中断描述符表(Interrupt Descriptor Table,IDT)是保护模式下用于存储中断处理程序的表,实模式下是中断向量表,他们的区别在于:

  • 中段描述符表地址不受限制,中断向量表位于 0x0~0x3ff 共1024 字节
  • 中段描述符表每个描述符 8 字节,中断向量表每个向量 4 字节

中断描述符表不仅有中断描述符,还有陷阱门、中断门等描述符,中断描述符表中的每个描述符都可以叫做,门

段描述符描述的是一段内存区域,门描述符描述的是一段代码,描述符大小都是8字节,门描述符中也有属性,门描述符都属于系统段,S 都为 0,type 不一样,重新回顾一下这几个门的作用:

任务门

  • 任务门和任务状态段 TSS 是 Intel 硬件级提供的任务切换机制,所以需要任务门配合 TSS 使用。
  • 任务门可以位于 GDT、LDT、IDT 中。
  • type值为0101
  • 任务门大多数操作系统都没使用。


中断门

  • 中断门包含了中断处理程序所在段的段选择子和段内偏移地址(中断处理程序的地址)。当通过此方式进入中断后,标志寄存器 eflags 的 IF 位自动置零。
  • 中断门可以位于 IDT 中。
  • type值为1110


陷阱门

  • 陷阱门和中断门非常相似,区别是由陷阱门进入中断后,标志寄存器eflags中的IF位不会自动置0。
  • 陷阱门可以位于 IDT 中。
  • type值1111


调用门

  • 调用门是提供给用户进程进入特权0级的方式,其DPL为3。
  • 调用门可以位于 GDT 和 LDT 中。
  • type值 1100


现代操作系统很少用到任务门和调用门,主要用到的是中断门,和陷阱门

IDT 同 GDT 一样,CPU 硬件上提供了存储其位置的寄存器,中断描述符表寄存器 IDTR:

前 16 位是界限,后 32 位是基址,理论上可以有 64KB/8KB=8192 个描述符,但 CPU 只支持 256个

加载指令:lidt 48位内存数据

中断处理过程

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

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

CPU 内的过程:

  • 处理器根据中断向量号定位中断门描述符。

  • 处理器进行特权级检查。

    • 对于软件发起的软中断,当前特权级 CPL 必须位于 门描述符 DPL 和门中目标代码段描述符 DPL 之间(特权比门高,才能使用门,特权比处理程序低,才能使用门调用处理程序,特权转移只能从低到高进行)。

    • 对于外部设备引起的中断和异常,则只检查 CPL 和目标代码段 DPL ,CPL 权限要小于 DPL 才行。

  • 执行中断处理程序。

中断发生后,eflags 中的 NT 位和 TF 位会被置零,如果是中断门,则 IF 位也置零

TF 位:Trap Flag,陷阱标志位,用在调试环境中,TF 为 0 的时候,禁止单步执行

NT 位:Nest Task Flag,任务嵌套标志位,用来标记任务嵌套调用情况,用于在当前任务中中断进行新的任务,进行完之后再回来完成当前任务的场景

从中断返回的指令是 iret,从栈中弹出数据到寄存器 cs、eip、eflags 等,根据特权级是否改变决定是否要恢复旧栈

处理器提供了专门控制 IF 位的指令:

  • cli使 IF 位置 0
  • sli使 IF 位置 1

中断发生时的压栈

压栈操作如图所示,出栈则是按照压栈的反方向进行的

如果有中断错误码,处理器并不会主动跳过它的位置,必须手动将其跳过,在准备用 iret 指令返回时,当前栈指针 esp 必须指向栈中备份的 EIP_old 所在的位置。


处理器在中断结束后返回的过程中还要进行一次特权级检查:

  • 从 CS_old 和 EIP_old 中检查 RPL 判断是否有特权级变化

    • 如果检查通过,则更新 cs 和 eip

    • 如果没涉及特权级变化,则当前栈指针还是 esp_old,用的还是旧栈

  • 将 eflags 弹出到标志寄存器

  • 如果需要改变特权级,则直接恢复旧栈

中断错误码

中断错误码用来指明中断发生在哪个段上。

  • EXT 表示外部事件,如果中断源来自不可屏蔽中断 NMI 或外部设备,EXT 为 1,否则为 0。
  • IDT 表示选择子是否指向中断描述符表 IDT,IDT 为 1 表示选择子指向中断描述符表,否则指向全局描述符表 GDT 或 局部描述符表 LDT。
  • TI 为 0 是指明选择子是从 GDT 中检索描述符,为 1 时是从 LDT 中检索描述符。
  • 高 13 位索引是在表中索引描述符的下标

通常能够压入错误码的中断属于中断向量号 0 ~ 32 之内的异常,而外部中断(32 ~ 255)和 int 软中断并不会产生错误码。

通常我们不用处理错误码。

可编程中断控制器 8259A

8259A 介绍

8259A 用于管理和控制可屏蔽中断,它表现在屏蔽外设中断,对它们实行优先级判决,向 CPU 提供中断向量号等功能。

一片 8259A 只能管理 8 个中断,通过级联芯片可以支持 7n+1 个中断源。

8259A 芯片的内部结构:

8259A 芯片收到中断信号之后,芯片内部的中断屏蔽寄存器 IMR 会判断该信号是否屏蔽(编程控制),如果屏蔽就丢弃信号,否则送入中断请求寄存器 IRR,IRR 相当于待处理中断队列,时机成熟时,优先级判别器 PR 会从中选择优先级高的中断,通过控制电路 INT 接口向 CPU 发送 INTR 信号;

CPU 处理完成后,通过自己的 INTA 接口向 8259A 的 INTA 接口回复一个响应信号,8259A 收到信号之后,立即将中断服务寄存器 ISR 中对应刚才选择的中断的位设置为 1,表示当前正在处理该中断,同时将该中断从 IRR 队列中去掉,之后 CPU 再次发送 INTA 信号给 8259A,8259A 将中断向量号(编程控制)通过系统数据总线发给 CPU ,CPU 进行执行中断处理程序。


硬件程序是固定的,可编程指的是我们可以控制硬件程序提供的输入和输出,CPU 提供了中断处理的框架,我们只需要提供 CPU 所需要的输入即可让 CPU 自动完成工作,需要的数据是:

  • 中断描述符表
  • 中断向量号

我们只需要在外部设备中设置好中断向量号,然后在中断描述符表中设置好对应的中断处理程序即可。

外部连接的硬件也是固定的:

8259A 编程

8259A 内部有两组寄存器,一组是初始化命令寄存器组,用来保存初始化命令字(ICW),ICW 共 4 个,ICW1 ~ ICW4。另一组是操作命令寄存器,用来保存操作命令字(OCW),OCW 共 3 个,OCW1 ~ OCW3。

对 8259A 的编程,也分为初始化和操作两部分:

  • 一部分是用 ICW 做初始化,用来确定是否需要级联,设置起始中断向量号,设置中断结束模式。其编程就是向端口发送一系列 ICW,后面的某个设置会依赖前面 ICW 的设置,所以必须依次写入 ICW1~4
  • 另一部分是用 OCW 来操作控制 8259A,中断屏蔽和中断结束。写入顺序无所谓了

ICW1~ICW4

ICW1 用来初始化 8259A 的连接方式和中断信号的触发方式。ICW1 需要写入主片的 0x20 端口和从片的 0xA0 端口。

  • IC4 为 1 时表示要写入 ICW4,为 0 时不需要。x86 系统必须为 1
  • SNGL 表示 single,若 SNGL 为 1,表示单片,否则表示级联,为 0 时主片和从片需要 ICW3
  • ADI 用来设置 8085 的调用时间间隔,x86 不需要设置。
  • LTIM 用来设置中断检测方式,LTIM 为 0 表示边沿触发,LTIM 为 1 表示电平触发。
  • 第 4 位的 1 是固定的,标识这是 ICW1
  • 第 5-7 位是服务 8085 CPU的,x86 不需要

ICW2 用来设置起始中断向量号。ICW2 需要写入到主片的 0x21 端口和从片的 0xA1 端口。

只需要设置 IRQ0 的中断向量号,剩下的就会依次顺延

  • 低 3 位:不用管,会依据接口的排列自动导入
  • 高 5 位:起始中断向量号

ICW3 仅在级联的方式下才需要,用来设置主片和从片用哪个 IRQ 接口互连。ICW3 需要写入主片的 0x21 端口及从片的 0xA1 端口。

主片和从片的 ICW3 结构不同。

  • 主片上中置 1 的那一位对应的 IRQ 接口用于连接从片。
  • 从片上低三位用于表示连接到主片上的 IRQ 接口


ICW4 用于设置 8259A 的工作模式,ICW3 需要写入主片的 0x21 端口及从片的 0xA1 端口。

  • SFNM 表示特殊全嵌套模式,若 SFNM 为 0,则表示全嵌套模式,为 1 则表示特殊全嵌套模式。
  • BUF 表示是否工作在缓冲模式。BUF 为 0,表示工作在非缓冲模式,BUF 为 1,表示工作在缓冲模式。
  • 如果工作在缓冲模式下,M/S 为 1 表示是主片,M/S 为 0 表示是从片。若工作在非缓冲模式下,M/S 无效。
  • AEOI 表示自动结束中断,8259A 在收到中断结束信号时才能继续处理下一个中断。若 AEOI 为 0,表示非自动。若 AEOI 为 1,表示自动结束中断。
  • μPM 表示位处理器类型。若 μPM 为 0,表示 8080 或 8085 处理器。若 μPM 为 1,表示 x86 处理器。

OCW1~OCW3

OCW1 用来屏蔽连接在 8259A 上的外部设备的中断信号,某位为 1,对应的 IRQ 上的中断信号就屏蔽了。OCW 要写入主片的 0x21 或从片的 0xA1 端口。


OCW2 用来设置中断结束方式和优先级模式。OCW2 写入主片的 0x20 及从片的 0xA0 端口。

OCW2 其中一个作用是发 EOI 信号结束中断。

  • SL 位针对某个特定优先级的中断进行操作。SL 为 0表示自动将正在处理的中断结束,L0-L2 无效
  • R 位 0 表示固定优先级方式,为 1 表示循环优先级方式。
  • EOI,中断结束命令位,为 1 会令 ISR 寄存器中相应位清 0

OCW2 高位可以组合出多种不同的结束方式:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nHt8PqoB-1674189947520)(E:\操作系统真象还原\note\picture\第七章\ICW2组合.png)]

OCW3:这里用不上

编写中断处理程序

Intel 8259A 芯片位于主板的南桥芯片上,8259A 与外设的连接是内部电路实现了的,直接操作即可使用。

开启 中断机制

流程:

  • init_all():用来初始化所有设备以及数据结构

    • ide_init():初始化中断相关内容

      • pci_init():初始化可编程控制器(Programmable interrupt controller),这里指的就是 8259A 芯片
      • ide_desc_init():初始化中断描述符表 IDT
  • 加载中断

中断处理程序

这里用到了汇编的宏 macro,宏是用来代替重复性输入的,格式如下:

%macro mul_add 3     ;宏声明 宏名称 宏参数
mov eax, %1             ;参数1
add eax, %2             ;参数2
add eax, %3             ;参数3
%endmacro;调用如下:
mul_add 12, 23, 34

中断处理程序如下:

kernel/source/kernel.asm

这一段代码通过宏创建了 intr_entry_table 数组(公开的成员),成员是33个中断处理程序的地址

[bits 32]
%define ERROR_CODE nop  ;为了栈中格式统一,如果 CPU 在异常中已经自动压入错误码,这里不做操作
%define ZERO push 0     ;为了栈中格式统一,如果 CPU 在异常中没有自动压入错误码,这里填充 0extern put_str          ;声明外部函数,告诉编译器在链接的时候可以找到section .data
intr_str db "interrupt occur!", 0xa, 0global intr_entry_table
intr_entry_table:%macro VECTOR 2
section .text
intr%1entry:            ;每个中断处理程序都要压入中断向量号,所以1个中断类型1个处理程序,自己知道自己的中断号是多少%2push intr_strcall put_stradd esp, 4;如果从片上进入中断,除了往片上发送 EOI 外,还要往主片上发送 EOI,因为后面要在 8259A 芯片上设置手动结束中断,所以这里手动发送 EOImov al, 0x20    ;中断结束命令 EOIout 0xa0, al    ;往从片发送out 0x20, al    ;往主片发送add esp, 4iretsection .data           ;这个 section .data 的作用就是让数组里全都是地址,编译器会将属性相同的 Section 合成一个大的 Segmengt,所以这里就是紧凑排列的数组了dd intr%1entry  ;存储各个中断入口程序的地址,形成 intr_entry_table 数组%endmacro     VECTOR 0x00, ZERO
VECTOR 0x01, ZERO
VECTOR 0x02, ZERO
VECTOR 0x03, ZERO
VECTOR 0x04, ZEROVECTOR 0x05, ZERO
VECTOR 0x06, ZERO
VECTOR 0x07, ZERO
VECTOR 0x08, ZERO
VECTOR 0x09, ZEROVECTOR 0x0a, ZERO
VECTOR 0x0b, ZERO
VECTOR 0x0c, ZERO
VECTOR 0x0d, ZERO
VECTOR 0x0e, ZEROVECTOR 0x0f, ZERO
VECTOR 0x10, ZERO
VECTOR 0x11, ZERO
VECTOR 0x12, ZERO
VECTOR 0x13, ZEROVECTOR 0x14, ZERO
VECTOR 0x15, ZERO
VECTOR 0x16, ZERO
VECTOR 0x17, ZERO
VECTOR 0x18, ZEROVECTOR 0x19, ZERO
VECTOR 0x1a, ZERO
VECTOR 0x1b, ZERO
VECTOR 0x1c, ZERO
VECTOR 0x1d, ERROR_CODEVECTOR 0x1f, ZERO
VECTOR 0x20, ZERO
VECTOR 0x21, ZERO

创建中断描述符表,安装中断处理程序

代码中略的部分(书上也写了略),会在后面小节中补充上来

kernel/source/interrupt.c:

这一段代码创建了中断描述符结构体,使用了一个函数来填充这个结构体,用了另一个函数来将各个中断描述符填充到中断描述符表中去,最后通过idt_init()来进行调用:

#include "interrupt.h"
#include "global.h"
#include "stdint.h"
#include "io.h"
#include "print.h"/*略*/#define IDT_DESC_CNT 0x21                   // 目前总共支持的中断数/*中断门描述符结构体*/
struct gate_desc {uint16_t func_offset_low_word;          // 中断处理程序偏移量低16位uint16_t selector;                      // 中断处理程序目标代码段选择子uint8_t  dcount;                        // 此项位双字计数字段,是门描述符第四字节,是固定值uint8_t  attribute;                     // type属性 + S + DPL + Puint16_t func_offset_high_word;         // 中断处理程序偏移量高16位
};// 静态函数声明,非必须
// intr_handler 实际上是 void* 在 interrupt.h 里定义的
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.asm 中的中断处理函数入口数组/*创建中断门描述符*/
// 参数:中断描述符,属性,中断处理函数地址
// 功能:向中断描述符填充属性和地址
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;   // 0000FFFF = 1111 1111 1111 1111,即将前面全置0p_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]); // IDT_DESC_DPL0在global.h定义的}put_str("    idt_desc_init done\n");}/*完成有关中断的所有初始化工作*/
void idt_init() {put_str("idt_init_start\n");idt_desc_init();    // 初始化中断描述符表pic_init();         // 初始化 8259A/* 加载 idt, idt = 32 位表基址 + 16位表界限*/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");
}

kernel/source/interrupt.h:

#ifndef __KERNEL_INTERRUPT_H
#define __KERNEL_INTERRUPT_H
#include "stdint.h"typedef void* intr_handler;void idt_init(void);#endif

kernel/source/global.h:

#ifndef _KERNEL_GLOBAL_H
#define _KERNEL_GLOBAL_H
#include "stdint.h"#define RPL0 0
#define RPLl 1
#define RPL2 2
#define RPL3 3#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)/*-------------- 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

用内联汇编封装端口IO函数

到此,和中断相关的数据(中断描述符表,中断向量号)都准备好了,接下来只要把 8259A 设置好即可

这里先把常用的端口IO操作封装成函数,方便以后调用

lib/kernel/io.h:

这里封装了 4 个函数:

  • 向端口写入一个字节:void outb(uint16_t port, uint8_t data)
  • 向端口写入一个字符串:void outsw(uint16_t port, const void* addr, uint32_t word_cnt)
  • 从端口读入一个字节:uint8_t inb(uint16_t port)
  • 从端口读入一个字符串:void insw(uint16_t port, void* addr, uint32_t word_cnt)

static 表示作用域在本文件内,需要调用需要把本文件包含进入,会导致文件体积增大

加了 inline 关键字,函数会在调用处原地展开,编译后的代码不包含call,也就是不属于函数调用了,减少了函数调用相关 的工作,提升了工作效率

牺牲体积来增加运行速度还是不错的。

/**************   机器模式   ***************b -- 输出寄存器QImode名称,即寄存器中的最低8位:[a-d]l。w -- 输出寄存器HImode名称,即寄存器中2个字节的部分,如[a-d]x。HImode“Half-Integer”模式,表示一个两字节的整数。 QImode“Quarter-Integer”模式,表示一个一字节的整数。
*******************************************/ #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,%w1表示对应dx */asm volatile ("outb %b0, %w1" : : "a"(data), "Nd"(port));/******************************************************/// 这里是 AT&T 语法的汇编语言,相当于: mov al. data//                                   mov dx, port//                                   out dx, al
}/* 将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));/*********************************************************/// 这里是 AT&T 语法的汇编语言,相当于: cld//                                   mov esi, addr//                                   mov ecx, word_cnt//                                   mov edx, 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 指向的内存asm volatile ("cld; rep insw" : "+D"(addr), "+c"(word_cnt) : "d"(port) : "memory");
}#endif

设置 8259A

8259A 的编程就是写入 ICW 和 OCW,其中

  • ICW是初始化控制字, 共4个,ICW1~ICW4, 用千初始化8259A的各个功能。
  • OCW 是操作控制字, 用于同初始化后的8259A进行操作命令交互。 所以,对 8259A 的操作是在其初始化之后,对于8259A的初始化必须最先完成。

因为硬盘是接在了从片的引脚上,将来实现文件系统是离不开硬盘的,所以我们这里使用的8259A要采用主、从片级联的方式。

在x86系统中,对于初始化级联8259A, 4个ICW都需要,必须严格按照ICW1~4顺序写入。

写入端口:

  • ICW1 和 OCW2、OCW3 是用偶地址端口0x20(主片)或0xA0(从片)写入。
  • ICW2 ~ ICW4 和 OCW1 是用奇地址端口0x21(主片)或0xA1(从片)写入。

kernel/source/interrupt.c

这里的内容就是上次写的这个文件中,“略”的部分,这里把对 8259A 芯片的初始化操作加了进去。

//第一个“略”处
#include "io.h"#define PIC_M_CTRL 0x20         //主片
#define PIC_M_DATA 0x21
#define PIC_S_CTRL 0xA0         //从片
#define PIC_S_DATA 0xA1//第二个“略”处
/* 初始化可编程中断控制器 8259A */
static void pic_init(void){//初始化主片outb(PIC_M_CTRL, 0x11);         // ICW1: 0001 0001 ,边沿触发,级联 8259,需要ICW4outb(PIC_M_DATA, 0x20);         // ICW2: 0010 0000 ,起始中断向量号为 0x20(0x20-0x27)outb(PIC_M_DATA, 0x04);         // ICW3: 0000 0100 ,IR2 接从片outb(PIC_M_DATA, 0x01);         // ICW4: 0000 0001 ,8086 模式,正常EOI//初始化从片outb(PIC_S_CTRL, 0x11);         // ICW1: 0001 0001 ,边沿触发,级联 8259,需要ICW4outb(PIC_S_DATA, 0x28);         // ICW2: 0010 1000 ,起始中断向量号为 0x28(0x28-0x2f)outb(PIC_S_DATA, 0x02);         // ICW3: 0000 0010 ,设置连接到主片的 IR2 引脚outb(PIC_S_DATA, 0x01);         // ICW4: 0000 0001 ,8086 模式,正常EOI//打开主片上的 IR0 也就是目前只接受时钟产生的中断//eflags 里的 IF 位对所有外部中断有效,但不能屏蔽某个外设的中断了outb (PIC_M_DATA, 0xfe);outb (PIC_S_DATA, 0xff);put_str("    pic init done\n");
}

pic_init()函数在最后的idt_init中调用

加载IDT,开启中断

kernel/source/interrupt.c

这一段代码之前写了,现在完善了相关函数,再来回顾一下:

初始化中断描述符表和 8259A 芯片之后,通过lidt命令加载 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");
}

kernel/source/init.c

用一个函数专门来启动模块,以后添加新的模块了也添加到这里来启动:

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

kernel/source/init.h

为了让 main.c 调用 init_all 函数,所以建立一个 init.h

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

kernel/source/main.c

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

使用sti指令开启中断(sti指令的作用是将 IF 位 set 为 1)

完整代码:

运行 Bochs

为了文件目录的整洁,将所有的目标文件和编译后的内核文件都放在 build 目录下:

nasm -f elf -o build/print.o lib/kernel/print.asm
nasm -f elf -o build/kernel.o kernel/kernel.asm gcc -m32 -I lib/kernel/ -m32 -I lib/ -m32 -I kernel/ -c -fno-builtin -o build/main.o kernel/main.c
gcc -m32 -I lib/kernel/ -m32 -I lib/ -m32 -I kernel/ -c -fno-builtin -o build/interrupt.o kernel/interrupt.c
gcc -m32 -I lib/kernel/ -m32 -I lib/ -m32 -I kernel/ -c -fno-builtin -o build/init.o kernel/init.cld -m elf_i386 -Ttext=0xc0001500 -e main -o build/kernel.bin build/main.o build/init.o build/interrupt.o build/print.o build/kernel.o

gcc 里用到的新的参数:

-fno-builtin 处理内建函数

-I 参数要把所有相关文件的目录都选上

运行 Bochs:

在 Bochs 里使用 info idt 查看当前 IDT:

改进中断处理程序

前情提要

这里先来回顾一下到此为止所学的内容:

中断处理机制是怎么一个流程:

首先,中断分为软件中断和硬件中断:

  • 软件中断通过指令向系统发送中断向量号
  • 硬件中断通过中断控制器判断当前中断的优先级后向 CPU 的 INTR 引脚发送中断中断向量号

得到中断向量号之后,CPU 通过向 IDTR 寄存器查询中断描述符表 IDT 的地址

通过中断向量号索引当前中断在 IDT 中的位置,也就是门描述符,从中获取该中断响应的中断处理程序的地址

跳转到中断处理程序去执行,通过 iret 返回


代码文件分别是做什么的:

interrupt.c 是中断的主要初始化文件,初始化了 PIC 和 IDT

global.h 定义了门描述符和段选择子

io.h 封装了对端口的读写函数

init.c 将 interrupt.c 封装好的初始化程序再次封装,供 main.c 调用


中断是如何开启的:

开启中断准备工作分两部分:

  • 初始化中断描述符表:建立中断描述符表,填充各种中断描述符(门)
  • 初始化可编程控制器:通过端口发送初始化字设置运行模式

准备工作结束后,通过设置 IF 位来开中断

改进中断处理程序

之前中断处理程序都是汇编写的,写起来太麻烦,可以选择用 C 来编写

在 C 语言中建立中断处理函数数组 idt_table,数组元素是 C 版本的中断处理函数地址,供汇编中的 intrXXentry 使用

这就只需要在中断入口程序中,让中断向量号*4,加上 C 语言数组 idt_table 地址索引到对应的中断处理函数,就可以调用C语言的中断处理函数了。

kernel/source/interrupt.c

在这里添加如下代码:

//添加两个声明
char* intr_name[IDT_DESC_CNT];                          // 用于保存异常的名字
intr_handler idt_table[IDT_DESC_CNT];                   // 用于保存处理程序地址;;;//在idt_init()前添加:
/*通用的中断处理请求*/
static void general_intr_handler(uint8_t vec_nr){if(vec_nr == 0x27 || vec_nr == 0x2f){// IRQ7 IRQ15 会产生伪中断,无需处理// 0x2f 是从片 8259A 上的最后一个 IRQ 引脚,保留项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.S 的 call [idt_table = %1*4]idt_table[i] = general_intr_handler;    // 以后用register_handler 来注册具体的处理函数intr_name[i] = "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 No七 Available Exception"; intr_name[8] = "JIDF 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[l5]第15项是intel保留项,未使用intr_name[16] = "#MF x87 FPU F'loating-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();             //初始化 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");
}

这里是创建了通用的中断处理请求函数,初始化中断处理函数为通用函数,然后初始化函数名称。

接下来只需要让 kernel.S 里的中断描述符中的地址指向 idt_table 中的地址即可

kernel/source/kernel.asm

[bits 32]
%define ERROR_CODE nop                  ; 若在相关的异常中cpu已经自动压入了错误码, 为保持栈中格式统一, 这里不做操作
%define ZERO push 0                     ; 为了栈中格式统一, 如果 CPU 在异常中没有自动压入错误码, 这里填充 0extern idt_table                        ; 声明 c 注册的中断处理函数数组section .data; intr_entry_table位于data段, 之后会和宏中的data段组合在一起(注意: 宏中的text段与intr_entry_table不是同一个段)
global intr_entry_table
intr_entry_table:;--------------- 宏 VECTOR 开始, 参数数目为2, 第一个参数为中断号, 第二个参数为该中断对 ERROR_CODE 的操作 ---------------
%macro VECTOR 2
section .textintr%1entry:                            ; 每个中断处理程序都要压入中断向量号, 所以1个中断类型1个处理程序, 自己知道自己的中断号是多少, %1: 调用宏时的第一个参数%2                                  ; %2: 调用宏时的第二个参数, 有错误码时什么都不做, 没有时压入 0 使格式统一; 保存上下文环境push dspush espush fspush gspushad; 如果从片上进入中断, 除了往从片上发送 EOI 外,还要往主片上发送 EOI, 因为后面要在 8259A 芯片上设置手动结束中断, 所以这里手动发送 EOImov al, 0x20                        ; 中断结束命令 EOI, 0x20 = 0010 0000, 第五位为EOI位out 0xa0, al                        ; 往从片发送out 0x20, al                        ; 往主片发送push %1                             ; 不管中断处理程序是否需要, 一律压入中断向量号call [idt_table + %1*4]           ; 调用中断处理程序jmp intr_exitsection .data                           ; 这个 section .data 的作用就是让数组里全都是地址, 编译器会将属性相同的 Section 合成一个大的 Segmengt, 所以这里就是紧凑排列的数组了dd intr%1entry                      ; 存储各个中断入口程序的地址, 形成 intr_entry_table 数组%endmacro
;---------------宏 VECTOR 结束---------------section .text
global intr_exit
intr_exit:; 恢复上下文环境add esp, 4                          ; 跳过参数中断号popadpop gspop fspop espop dsadd esp, 4                          ; 手动跳过错误码iretdVECTOR 0x00, ZERO
VECTOR 0x01, ZERO
VECTOR 0x02, ZERO
VECTOR 0x03, ZERO
VECTOR 0x04, ZEROVECTOR 0x05, ZERO
VECTOR 0x06, ZERO
VECTOR 0x07, ZERO
VECTOR 0x08, ERROR_CODE
VECTOR 0x09, ZEROVECTOR 0x0a, ERROR_CODE
VECTOR 0x0b, ERROR_CODE
VECTOR 0x0c, ZERO
VECTOR 0x0d, ERROR_CODE
VECTOR 0x0e, ERROR_CODEVECTOR 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_CODEVECTOR 0x19, ZERO
VECTOR 0x1a, ERROR_CODE
VECTOR 0x1b, ERROR_CODE
VECTOR 0x1c, ZERO
VECTOR 0x1d, ERROR_CODEVECTOR 0x1e, ERROR_CODE
VECTOR 0x1f, ZERO
VECTOR 0x20, ZERO

这里主要是修改了宏,现在的宏是先保存上下文环境,然后入栈中断向量号调用 C 语言中 idt_table 相应的处理程序,调用完之后,还原上下文环境,从中断返回。

interrupt.c完整代码:

运行 Bochs

编译,链接,写入硬盘:还是刚才的那一套操作:

可编程计数器/定时器 8253

时钟

计算机上的时钟可以分为两类:内部时钟和外部时钟

  • 内部时钟频率来自晶振,是不可改变的。
  • 外部时钟频率来自内部时钟的分频,叫做外频,外频乘以某个倍数之后称为主频

外部时钟和内部时钟是两套独立运行的定时体系。

定时器和计数器实际上是一回事,都是在做计时的功能,也就是到指定时间后发信号给 CPU

8253 入门

8253 定时/计数器是通过倒计时的方式定时,需要先设置一个初始值,每隔一个时钟周期减去1,减到0就给CPU发送信号,然后重新初始化

8253 芯片的计数器内部有3个主要部件:全都是16位宽的

  • 计数初值寄存器:用来保存初值
  • 减法计数器:每隔一个脉冲信号,减去1,用来计数
  • 输出锁存器:减法计数器的值会存在这里,用来获取当前计数进度

8253 芯片的每隔计数器都有3个引脚:

  • CLK:接时钟输入信号
  • GATE:门控输入信号,用来控制计数器是否开始计数
  • OUT:定时完成后,通过此引脚发出信号通知

8253 芯片内部有3个计数器,工作相互独立,互不影响,作用和端口如图所示:

8253 控制字

端口0x43是控制字寄存器,功能如图所示:

8253 工作方式

计数器启动的条件:

  • 硬件条件:GATE 引脚为高电平,由硬件控制完成
  • 软件条件:计数初值已写入计数器中的减法计数器,由软件 out 指令控制完成

启动类型:

  • 软件启动:硬件条件已经完成,由软件条件来控制启动,工作方式 0/2/3/4
  • 硬件启动:软件条件已经完成,由硬件条件来控制启动,工作方式 1/5

停止类型:

  • 强制终止:将 GATE 信号置 0
  • 自动终止:单次计数完之后自动停止,工作方式 0/1/4/5

六种工作方式:

8253 初始化

让 8253 开始工作的方法比 8259A 简单多了:

  • 通过控制字设置控制模式
  • 向计数器写入初值

device/timer.h:

#ifndef __DEVICE_TIME_H
#define __DEVICE_TIME_H
#include "stdint.h"void timer_init(void);#endif

device/timer.c:

#include "timer.h"
#include "io.h"
#include "print.h"#define IRQ0_FREQUENCY          100                                 // IRQ0 频率
#define INPUT_FREQUENCY         1193180                             // 8253input频率
#define COUNTER0_VALUE          INPUT_FREQUENCY / IRQ0_FREQUENCY    // IRQ0计数初值
#define COUNTER0_PORT           0x40                                // 计数器端口
#define COUNTER0_NO             0                                   // 控制字中使用的计数器号码
#define COUNTER_MODE            2                                   // 计数器工作方式
#define READ_WRITE_LATCH        3                                   // 读写方式, 先读写低8位, 再读写高8位
#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));// 先写入低 8 位outb(counter_port, (uint8_t) counter_value);// 再写入高8位outb(counter_port, (uint8_t) counter_value >> 8);
}/* 初始化 PIT8253 */
void timer_init() {put_str("timer_init start\n");// 设置8253的定时周期, 即发送中断的周期frequency_set(COUNTER0_PORT, COUNTER0_NO, READ_WRITE_LATCH, COUNTER_MODE, COUNTER0_VALUE);put_str("timer_init done\n");
}

kernel/source/init.c

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

运行 Bochs

编译、链接、写入硬盘:

nasm -f elf -o build/print.o lib/kernel/print.asm
nasm -f elf -o build/kernel.o kernel/kernel.asm gcc -m32 -I lib/kernel/ -c -o build/timer.o device/timer.c
gcc -m32 -I lib/kernel/ -m32 -I lib/ -m32 -I kernel/ -c -fno-builtin -o build/main.o kernel/main.c
gcc -m32 -I lib/kernel/ -m32 -I lib/ -m32 -I kernel/ -c -fno-builtin -o build/interrupt.o kernel/interrupt.c
gcc -m32 -I lib/kernel/ -m32 -I lib/ -m32 -I kernel/ -c -fno-builtin -o build/init.o kernel/init.cld -m elf_i386 -Ttext=0xc0001500 -e main -o build/kernel.bin build/main.o build/init.o build/interrupt.o build/print.o build/kernel.o build/timer.o

运行:

参考

gcc -fno-builtin; -flto - 崔超超的个人空间 - OSCHINA - 中文开源技术交流社区

《操作系统真象还原》第七章相关推荐

  1. 计算机软件行业大部分成本是,平狄克微观经济学第七章成本问题

    <平狄克微观经济学第七章成本问题>由会员分享,可在线阅读,更多相关<平狄克微观经济学第七章成本问题(45页珍藏版)>请在人人文库网上搜索. 1.第七章 成本问题,目录,成本的测 ...

  2. 《微观经济学》 第七章

    第七章 不完全竞争市场中的厂商行为 7.1 垄断的成因 竞争和垄断以不同的比例存在于各个市场中,把这个比例叫做市场结构. 在不同的市场结构下,厂商的决策模式是有很大差异的. 完全竞争.垄断竞争.寡头垄 ...

  3. 数字图像处理——第七章 小波和多分辨处理

    数字图像处理--第七章 小波和多分辨率处理 文章目录 数字图像处理--第七章 小波和多分辨率处理 写在前面 1 多分辨率处理 1.1 图像金字塔 1.2 多尺度和多分辨率的区别 2 小波 2.1 连续 ...

  4. 现实迷途 第七章 特殊客户

    第七章 特殊客户 注:原创作品,请尊重原作者,未经同意,请勿转载,否则追究责任. 江北一般都是上午待在办公室里,搜集信息或整理以前做过的系统,下午才出去站街招客. 站街站了一段时间后,江北有点不想去了 ...

  5. stm32 工业按键检测_「正点原子STM32Mini板资料连载」第七章 按键输入实验

    1)实验平台:正点原子STM32mini开发板 2)摘自<正点原子STM32 不完全手册(HAL 库版)>关注官方微信号公众号,获取更多资料:正点原子 第七章 按键输入实验 上一章,我们介 ...

  6. 第七章——DMVs和DMFs(2)——用DMV和DMF监控索引性能

    原文: 第七章--DMVs和DMFs(2)--用DMV和DMF监控索引性能 本文继续介绍使用DMO来监控,这次讲述的是监控索引性能.索引是提高查询性能的关键性手段.即使你的表上有合适的索引,你也要时时 ...

  7. 2017上半年软考 第七章 重要知识点

    第七章项目范围管理 []项目范围管理概念 [][]项目范围管理的含义和作用 项目范围管理内容p289 项目范围对项目管理的重要性?p289 [][]项目范围管理的主要过程 项目范围管理的6个过程是? ...

  8. 服务器架构之性能扩展-第七章(8)

    第七章Cacti系统监控邮件报警和压力测试 7.1 Cacti工作原理 原理简单来说,Cacti就是rrdtool的一个forefront,它内置了快速的获数据取工具.优秀的绘图模板以及许多设计精良的 ...

  9. 鸟哥Linux私房菜_基础篇(第二版)_第七章学习笔记

    第七章 Linux文件和目录管理 绝对路径:以"/"开始 相对路径:以非"/"开始 其中,"."代表当前目录,".."代 ...

  10. 计算机组成原理 输入输出系统,计算机组成原理(第七章输入输出系统

    计算机组成原理(第七章输入输出系统 (6页) 本资源提供全文预览,点击全文预览即可全文预览,如果喜欢文档就下载吧,查找使用更方便哦! 9.9 积分 第七章输入输出系统第一节基本的输入输出方式一. 外围 ...

最新文章

  1. 调用浏览器的打印方法打印页面内容
  2. bzoj2821 作诗(Poetize)分块+二分
  3. 2021-2025年中国定时控制器行业市场供需与战略研究报告
  4. 做游戏,学编程(C语言) 9 贪吃蛇
  5. kuangbin专题一 简单搜索
  6. java案例代码13--斗地主部分代码--静态ArrayList的使用
  7. java语言的优缺点
  8. 转http://www.anyliz.com/blog/article/Software/favorites-software-official-download-url.htm
  9. xp精简版安装iis
  10. uni 获取本地文件_uni-app 图片(文件) 本地存储解决方案
  11. unity剩余高度自适应实现办法
  12. 分频器的Verilog实现
  13. Python—SJ—实验4—DNA翻译
  14. NFT/Web3/区块链项目孵化包装策划,到底该自建运营还是专业外包孵化?
  15. android onGenericMotionEvent(MotionEvent event)
  16. CRM(客户关系管理系统)项目框架搭建
  17. IT Farmer下次更新内容
  18. 基于 Apache Kylin 的微博舆情实时分析(内含 Demo)
  19. HP-UX 11.31 安装RAC 添加共享磁盘的问题(两种办法)
  20. 切绳子【洛谷P1577】【二分】

热门文章

  1. 运动解剖测试题软件,2015教师招聘体育学科运动解剖学经典题
  2. Precision(精准率、查准率)和Recall(召回率、查全率)的应用场景
  3. C#:EXCEL版本与相应dll版本的对应关系
  4. 科研必备工具篇(持续更新)
  5. 分享 82个实用的前端开发工具
  6. Go语言中定时任务库Cron使用详解
  7. java模拟病人就诊过程_模拟医院挂号系统
  8. 高中毕业礼物送什么比较好?第一名的礼物你绝对想不到
  9. 【原神】手机版原神下错版本不能登录怎么办?B服修改为官服
  10. 反激式开关电源芯片是什么?如何对反激开关电源mos管选型?