操作系统:内核的基本实现(二)中断
中断
操作系统是中断驱动的。
中断的分类
外中断
- INTR 引脚进入的 可屏蔽中断:由外部设备引起,CPU可以选择性处理。
- NMI 引脚进入不可屏蔽中断:发生了致命错误,即将导致宕机,一般软件无法处理的错误。
一般每一种可屏蔽中断都能分配到中断向量号,但是,不可屏蔽中断一律都由中断向量 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");
}