操作系统:内核的基本实现(二)中断

中断

操作系统是中断驱动的。

中断的分类

外中断

  1. INTR 引脚进入的 可屏蔽中断:由外部设备引起,CPU可以选择性处理。
  2. NMI 引脚进入不可屏蔽中断:发生了致命错误,即将导致宕机,一般软件无法处理的错误。

一般每一种可屏蔽中断都能分配到中断向量号,但是,不可屏蔽中断一律都由中断向量 2 处理。

内中断

  1. 软中断:由软件主动发起的中断。一般是软件试图调用系统调用。
  2. 异常:异常指令、非法指令操作、越界、溢出等引发的内中断。
    • Fault:可被操作系统/软件修复的:缺页异常
    • Trap:用于调试器断点
    • Abort:软件试图进行非法操作,将被操作系统终止。

IDT 与中断过程

保护模式下使用中断描述符表 IDT 保存中断门以完成中断。

操作系统需要加载 IDTR 指明 IDT 所在位置。与 GDT 不同的是,LDT 的第0描述符是可用的。

当中断发生时,CPU 将会

  • 根据 IDT 选择子找到中断处理程序,执行特权级检测
  • 如果特权级发生改变,SP、ESP将被压入新栈中,并根据 TSS 修改 SP,ESP,CS,EIP
  • 旧 CS,EIP 压入栈中
  • 对于中断门,CPU 会将 EFLAGS 的 TF,NT位置为0,压入栈中
  • 执行中断处理程序

中断处理程序必须用 iret 返回,CPU 将自动恢复以上步骤。

可编程中断控制器 8259A

8259A 控制器负责所有来自外部的中断(可屏蔽中断),包括时钟中断。具体来说,8259A 负责将多个外部中断信号汇总后传递给 CPU,并根据设定的优先级来确定哪个中断应当先处理。它能够屏蔽不需要的中断信号,控制中断的优先级,支持嵌套中断等功能。

每个 8259A 控制器可以管理 8 个中断信号(IRQ0 - IRQ7)。不过,通过级联方式,最多可以将 9 个 8259A 控制器级联,从而支持 最多 64 个中断信号。(使用联级方式每个从片都要占据一个 IRQ 位置,因此是 \(9 \times 8 - 9 = 64\))

现代 x86 机器中断管理
现代 x86 系统不再直接使用 8259A,而是采用 APIC 等更先进的中断管理机制。不过,有些系统在兼容模式下仍然可以支持 8259A 机制以便处理某些老旧设备。

8259A 负责将 IRQ 接口号映射为实际系统中的中断向量号。

为内核初始化中断

初始化 PIC

向主从片写入 ICW,OCW 初始化:

static void PicInit(void)
{
    outb(PIC_M_CTRL, 0x11);
    outb(PIC_M_DATA, 0x20);
    outb(PIC_M_DATA, 0x04);
    outb(PIC_M_DATA, 0x01);
    outb(PIC_S_CTRL, 0x11);
    outb(PIC_S_DATA, 0x28);
    outb(PIC_S_DATA, 0x02);
    outb(PIC_S_DATA, 0x01);
    outb(PIC_M_DATA, 0xfe);
    outb(PIC_S_DATA, 0xff);
}

初始化中断描述符表

IDT 表中的中断处理程序指向千篇一律的,由宏生成的自动处理入口程序,该入口程序负责:

  • 保护中断发生前的环境上下文
  • 向8259A主从片发送中断结束信号
  • 将自身的中断号压入栈,以作为第一个参数调用实际 C 中断处理程序
  • 特别的,对于个别的中断处理号,CPU 会在中断发生后向栈中压入 ERROR_CODE,入口程序为此保证行为的一致,如果中断不会压入ERROR_CODE则人工压入 0, 以保证 add esp, 4 行为的一致性。

IntrHandlerTable 是 C 程序中实际中断处理函数所在处,向量号本身就是指向IntrHandlerTable的索引。

%define ERROR_CODE nop      
%define ZERO push 0

%macro VECTOR 2
section .text
intr%1entry:		 
   %2
   push ds
   push es
   push fs
   push gs
   pushad
   
   mov al,0x20                   
   out 0xa0,al                   
   out 0x20,al                   

   push %1
   call [IntrHandlerTable + %1*4]
   jmp IntrExit

   add esp,4			 
   iret				 

section .data
   dd    intr%1entry	 
%endmacro
/* Kernel.S 中的处理程序只是入口程序 */
extern IntrHandler IntrEntryTable[IDT_DESC_CNT];
/* 中断名称 */
const char *IntrName[IDT_DESC_CNT];
/* IntrEntryTable 中的函数将会重定向到实际处理事件的函数 */
IntrHandler IntrHandlerTable[IDT_DESC_CNT];

/* 创建中断门描述符 */
static void MakeIdtDesc(struct GateDesc *p_gdesc, uint8_t attr, IntrHandler function)
{
    p_gdesc->OffsetLowWord = (uint32_t)function & 0x0000FFFF;
    p_gdesc->Selector = SELECTOR_K_CODE;
    p_gdesc->DCount = 0;
    p_gdesc->Attribute = attr;
    p_gdesc->OffsetHighWord = ((uint32_t)function & 0xFFFF0000) >> 16;
}

/*初始化中断描述符表*/
static void IdtDescInit(void)
{
    int i;
    for (i = 0; i < IDT_DESC_CNT; i++)
    {
        MakeIdtDesc(&idt[i], IDT_DESC_ATTR_DPL0, IntrEntryTable[i]);
    }
}

初始化中断处理程序数组

为中断分配指定的中断处理程序。实际上,除了页访问异常以外,前 32 个异常对于操作系统而言都是“无能为力”的。因此DefaultIntrHandler的行为就是什么也不做。在系统初始化时,我们总是将所有中断初始化 DefaultIntrHandler。

/* 对于中断的默认处理程序 */
static void DefaultIntrHandler(int vectorNumber)
{
    // spurious interrupt
    if (vectorNumber == 0x27 || vectorNumber == 0x2f)
        return;

#ifndef __HIMUOS_RELEASE__
    PrintStr("INTR (0x");
    PrintAddr((void *)vectorNumber);
    PrintStr(") : ");
    if (IntrName[vectorNumber] != 0)
        PrintStr(IntrName[vectorNumber]);
    PrintChar('\n');
#endif // ^^ !__HIMUOS_RELEASE__ ^^

}

static void IntrHandlerInit(void)
{
    int i;
    for (i = 0; i < IDT_DESC_CNT; i++)
    {
        IntrHandlerTable[i] = DefaultIntrHandler;
        IntrName[i] = "Unknown";
    }
    IntrName[0] = "#DE Divide Error";
    IntrName[1] = "#DB Debug Exception";
    IntrName[2] = "NMI Interrupt";
    IntrName[3] = "#BP Breakpoint Exception";
    IntrName[4] = "#OF Overflow Exception";
    IntrName[5] = "#BR BOUND Range Exceeded Exception";
    IntrName[6] = "#UD Invalid Opcode Exception";
    IntrName[7] = "#NM Device Not Available Exception";
    IntrName[8] = "#DF Double Fault Exception";
    IntrName[9] = "Coprocessor Segment Overrun";
    IntrName[10] = "#TS Invalid TSS Exception";
    IntrName[11] = "#NP Segment Not Present";
    IntrName[12] = "#SS Stack Fault Exception";
    IntrName[13] = "#GP General Protection Exception";
    IntrName[14] = "#PF Page-Fault Exception";
    // IntrName[15] = "#15 (Intel reserved. Do not use.)";
    IntrName[16] = "#MF x87 FPU Floating-Point Error";
    IntrName[17] = "#AC Alignment Check Exception";
    IntrName[18] = "#MC Machine-Check Exception";
    IntrName[19] = "#XF SIMD Floating-Point Exception";
    // IntrName[32] = "Timer Interrupt";
}

加载 IDT

初始化以上工作后即可,将 IDT 加载到 CPU 中。

void InitIdt()
{
    PrintStr("InitIdt...");
    IdtDescInit();
    IntrHandlerInit();
    PicInit();
    uint64_t idtOperand = ((sizeof(idt) - 1) | ((uint64_t)(uint32_t)idt << 16));
    asm volatile("lidt %0" : : "m"(idtOperand));
    PrintStr("Done\n");
}

时钟中断与定时器

在经典 IBM PC 中使用8253 芯片作为可编程定时器,在每隔设定的时间间隔后发送时钟中断 (中断号 0x20)。在启动时,操作系统会将8253(或者其它计数器)的频率调整为一个较为合适的数值(一般 100HZ, 10ms 发生一次时钟中断)。

每当时钟中断发生,控制权回到时钟中断处理程序(也即内核)手中,操作系统此时负责跟踪系统时间,进行时间片轮转调度等。

#define INPUT_FREQUENCY  1193180
#define CONTRER0_PORT    0x40
#define COUNTER0_NO      0
#define IRQ0_HZ          100
#define IRQ0_FREQUENCY   (INPUT_FREQUENCY / IRQ0_HZ)
#define COUNTER_MODE     2
#define READ_WRITE_LATCH 3
#define PIT_CONTROL_PORT 0x43

void SetClockIrq()
{
    PrintStr("SetClockIrq...");
    outb(PIT_CONTROL_PORT, (uint8_t)(COUNTER0_NO << 6 | READ_WRITE_LATCH << 4 | COUNTER_MODE << 1));
    outb(CONTRER0_PORT, (uint8_t)IRQ0_FREQUENCY);
    outb(CONTRER0_PORT, (uint8_t)(IRQ0_FREQUENCY >> 8));
    PrintStr("DONE\n");
}

posted on 2024-10-28 16:38  Himu  阅读(25)  评论(0编辑  收藏  举报