/ x86架构操作系统内核的实现 / hurlex <八> 完成中断请求和定时器中断

hurlex <八> 完成中断请求和定时器中断

2014-09-13 posted in [hurlex开发文档]

在上一章中我们完成了中断处理程序的框架,本章在其基础上讨论中断请求的实现。

我们在上一章中提到,外设的所有中断由中断控制芯片8259A统一汇集之后连接到CPU的INTR引脚。1这章我们就来探究8259APIC的初始化和实现定时器的中断处理。

8259A PIC每一片可以管理8个中断源,显然一般情况下设备数量会超过这个值。这里就要提到IBM PC/AT 8259A PIC架构了,IBM的设计方案是使用8259APIC的级联功能,使用两片级联(分为主、从片)的方式来管理硬件中断。其中主片的INT端连接到CPU的INTR引脚,从片的INT连接到主片的IR2引脚。结构如下图所示:

8259A PIC级联

图中时钟中断连接在主片的IRQ0引脚,键盘中断连接在了主片的IRQ1引脚。其它的引脚暂时用不到就不说了。在上一张描述中断描述符表时我们知道了0~31号中断是CPU使用和保留的,用户可以使用的中断从32号开始。所以这里的IRQ0对应的中断号就是32号,IRQ1就是33号,然后以此类推。

理论就暂时阐述到这里,接下来是实现代码。首先是对8259A PIC的初始化,在设置中断描述符表的函数init_idt最前面加入如下代码:

// 初始化中断描述符表
void init_idt()
{ 
    // 重新映射 IRQ 表
    // 两片级联的 Intel 8259A 芯片
    // 主片端口 0x20 0x21
    // 从片端口 0xA0 0xA1
        
    // 初始化主片、从片
    // 0001 0001
    outb(0x20, 0x11);
    outb(0xA0, 0x11);
    
    // 设置主片 IRQ 从 0x20(32) 号中断开始
    outb(0x21, 0x20);

    // 设置从片 IRQ 从 0x28(40) 号中断开始
    outb(0xA1, 0x28);
    
    // 设置主片 IR2 引脚连接从片
    outb(0x21, 0x04);

    // 告诉从片输出引脚和主片 IR2 号相连
    outb(0xA1, 0x02);
    
    // 设置主片和从片按照 8086 的方式工作
    outb(0x21, 0x01);
    outb(0xA1, 0x01);
    
    // 设置主从片允许中断
    outb(0x21, 0x0);
    outb(0xA1, 0x0);
    
      ... ...
}

对8259A PIC具体的设置我们不再阐述,这种资料网上铺天盖地的都是。相信结合注释很容易理解这个简单的初始化过程。

完成了初始化之后,我们继续添加对IRQ处理函数的添加。首先是在idt.h头文件末尾添加如下内容:

// IRQ 处理函数
void irq_handler(pt_regs *regs);

// 定义IRQ
#define  IRQ0     32    // 电脑系统计时器
#define  IRQ1     33    // 键盘
#define  IRQ2     34    // 与 IRQ9 相接,MPU-401 MD 使用
#define  IRQ3     35    // 串口设备
#define  IRQ4     36    // 串口设备
#define  IRQ5     37    // 建议声卡使用
#define  IRQ6     38    // 软驱传输控制使用
#define  IRQ7     39    // 打印机传输控制使用
#define  IRQ8     40    // 即时时钟
#define  IRQ9     41    // 与 IRQ2 相接,可设定给其他硬件
#define  IRQ10    42    // 建议网卡使用
#define  IRQ11    43    // 建议 AGP 显卡使用
#define  IRQ12    44    // 接 PS/2 鼠标,也可设定给其他硬件
#define  IRQ13    45    // 协处理器使用
#define  IRQ14    46    // IDE0 传输控制使用
#define  IRQ15    47    // IDE1 传输控制使用

// 声明 IRQ 函数
// IRQ:中断请求(Interrupt Request)
void irq0();        // 电脑系统计时器
void irq1();        // 键盘
void irq2();        // 与 IRQ9 相接,MPU-401 MD 使用
void irq3();        // 串口设备
void irq4();        // 串口设备
void irq5();        // 建议声卡使用
void irq6();        // 软驱传输控制使用
void irq7();        // 打印机传输控制使用
void irq8();        // 即时时钟
void irq9();        // 与 IRQ2 相接,可设定给其他硬件
void irq10();       // 建议网卡使用
void irq11();       // 建议 AGP 显卡使用
void irq12();       // 接 PS/2 鼠标,也可设定给其他硬件
void irq13();       // 协处理器使用
void irq14();       // IDE0 传输控制使用
void irq15();       // IDE1 传输控制使用

然后是idt_s.s中添加相应的处理过程:

; 构造中断请求的宏
%macro IRQ 2
[GLOBAL irq%1]
irq%1:
    cli
    push byte 0
    push byte %2
    jmp irq_common_stub
%endmacro

IRQ   0,    32  ; 电脑系统计时器
IRQ   1,    33  ; 键盘
IRQ   2,    34  ; 与 IRQ9 相接,MPU-401 MD 使用
IRQ   3,    35  ; 串口设备
IRQ   4,    36  ; 串口设备
IRQ   5,    37  ; 建议声卡使用
IRQ   6,    38  ; 软驱传输控制使用
IRQ   7,    39  ; 打印机传输控制使用
IRQ   8,    40  ; 即时时钟
IRQ   9,    41  ; 与 IRQ2 相接,可设定给其他硬件
IRQ  10,    42  ; 建议网卡使用
IRQ  11,    43  ; 建议 AGP 显卡使用
IRQ  12,    44  ; 接 PS/2 鼠标,也可设定给其他硬件
IRQ  13,    45  ; 协处理器使用
IRQ  14,    46  ; IDE0 传输控制使用
IRQ  15,    47  ; IDE1 传输控制使用

[GLOBAL irq_common_stub]
[EXTERN irq_handler]
irq_common_stub:
    pusha                ; pushes edi, esi, ebp, esp, ebx, edx, ecx, eax
    
    mov ax, ds
    push eax             ; 保存数据段描述符
    
    mov ax, 0x10         ; 加载内核数据段描述符
    mov ds, ax
    mov es, ax
    mov fs, ax
    mov gs, ax
    mov ss, ax
    
    push esp
    call irq_handler
    add esp, 4
    
    pop ebx              ; 恢复原来的数据段描述符
    mov ds, bx
    mov es, bx
    mov fs, bx
    mov gs, bx
    mov ss, bx
    
    popa                 ; Pops edi,esi,ebp...
    add esp, 8           ; 清理压栈的 错误代码 和 ISR 编号
    iret                 ; 出栈 CS, EIP, EFLAGS, SS, ESP
.end:

最后是init_idt函数构造IRQ的相关描述符和具体的IRQ处理函数了。

// 初始化中断描述符表
void init_idt()
{ 
    ... ...
    idt_set_gate(31, (uint32_t)isr31, 0x08, 0x8E);

    idt_set_gate(32, (uint32_t)irq0, 0x08, 0x8E);
    idt_set_gate(33, (uint32_t)irq1, 0x08, 0x8E);
    idt_set_gate(34, (uint32_t)irq2, 0x08, 0x8E);
    idt_set_gate(35, (uint32_t)irq3, 0x08, 0x8E);
    idt_set_gate(36, (uint32_t)irq4, 0x08, 0x8E);
    idt_set_gate(37, (uint32_t)irq5, 0x08, 0x8E);
    idt_set_gate(38, (uint32_t)irq6, 0x08, 0x8E);
    idt_set_gate(39, (uint32_t)irq7, 0x08, 0x8E);
    idt_set_gate(40, (uint32_t)irq8, 0x08, 0x8E);
    idt_set_gate(41, (uint32_t)irq9, 0x08, 0x8E);
    idt_set_gate(42, (uint32_t)irq10, 0x08, 0x8E);
    idt_set_gate(43, (uint32_t)irq11, 0x08, 0x8E);
    idt_set_gate(44, (uint32_t)irq12, 0x08, 0x8E);
    idt_set_gate(45, (uint32_t)irq13, 0x08, 0x8E);
    idt_set_gate(46, (uint32_t)irq14, 0x08, 0x8E);
    idt_set_gate(47, (uint32_t)irq15, 0x08, 0x8E);

    // 255 将来用于实现系统调用
    idt_set_gate(255, (uint32_t)isr255, 0x08, 0x8E);

     ... ...
}

// IRQ 处理函数
void irq_handler(pt_regs *regs)
{
    // 发送中断结束信号给 PICs
    // 按照我们的设置,从 32 号中断起为用户自定义中断
    // 因为单片的 Intel 8259A 芯片只能处理 8 级中断
    // 故大于等于 40 的中断号是由从片处理的
    if (regs->int_no >= 40) {
        // 发送重设信号给从片
        outb(0xA0, 0x20);
    }
    // 发送重设信号给主片
    outb(0x20, 0x20);

    if (interrupt_handlers[regs->int_no]) {
        interrupt_handlers[regs->int_no](regs);
    }
}

结合代码中详细的注释和本章开始的8259A PIC的结构图,详细很容易理解这个处理过程。其实IRQ和ISR的处理过程很类似:

写到这里具体的IRQ处理过程就完成了,以后只需要设置好相应的处理函数就好了,接下来我们实现时钟中断的产生和处理。

时钟中断对于操作系统内核来说很重要的一种中断,它使得CPU无论在执行任何用户或者内核的程序时,都能定义的将执行权利交还到CPU手中来。2除了记录时间之外,时钟中断的处理函数里通常都是对进程的调度处理。

具体的时钟中断源是8253/8254 Timer产成的,要按照需要的频率产生中断,需要先配置8253/8254 Timer芯片。代码如下:

#include "timer.h"
#include "debug.h"
#include "common.h"
#include "idt.h"

void timer_callback(pt_regs *regs)
{
    static uint32_t tick = 0;
    printk_color(rc_black, rc_red, "Tick: %d\n", tick++);
}

void init_timer(uint32_t frequency)
{
    // 注册时间相关的处理函数
    register_interrupt_handler(IRQ0, timer_callback);

    // Intel 8253/8254 PIT芯片 I/O端口地址范围是40h~43h
    // 输入频率为 1193180,frequency 即每秒中断次数
    uint32_t divisor = 1193180 / frequency;

    // D7 D6 D5 D4 D3 D2 D1 D0
    // 0  0  1  1  0  1  1  0
    // 即就是 36 H
    // 设置 8253/8254 芯片工作在模式 3 下
    outb(0x43, 0x36);

    // 拆分低字节和高字节
    uint8_t low = (uint8_t)(divisor & 0xFF);
    uint8_t hign = (uint8_t)((divisor >> 8) & 0xFF);
    
    // 分别写入低字节和高字节
    outb(0x40, low);
    outb(0x40, hign);
}

对应的头文件如下:

#ifndef INCLUDE_TIMER_H_
#define INCLUDE_TIMER_H_

#include "types.h"

void init_timer(uint32_t frequency);

#endif  // INCLUDE_TIMER_H_

8253/8254 Timer有三种工作模式,我们使用第三种。init_timer函数的参数是所需的时钟中断的频率,具体的设置原理不再赘述。最后,修改入口函数进行测试:

#include "console.h"
#include "debug.h"
#include "gdt.h"
#include "idt.h"
 #include "timer.h"

int kern_entry()
{
    init_debug();
    init_gdt();
    init_idt();

    console_clear();
    printk_color(rc_black, rc_green, "Hello, OS kernel!\n");

    init_timer(200);

    // 开启中断
    asm volatile ("sti");

    return 0;
}

最后编译执行,我们看到了如下的输出:

8253/8254 Timer 中断

  1. 这里肯定会有读者提出来现代的计算机主板上早就使用APIC(Advanced Programmable Interrupt Controller,高级可编程中断控制器)来进行外设的中断管理了。没错,但是我相信在本科阶段的微机原理和接口技术中学的是8259APIC(Programmable Interrupt Controller),而且无论硬件怎么发展,始终会兼容以前的接口。本着大家熟悉易理解的原则,我们依旧使用兼容的8259APIC(Programmable Interrupt Controller, 可编程中断控制器)的设置方法进行设置。 

  2. 当然了,屏蔽中断就没办法了。