第三节 时钟中断

内核在中断处理方面,与异常处理有着许多重合的部分,这主要体现在向量表机制上。这一节首先来扩展异常中断向量表,让内核具备处理中断的能力,然后实现对全局和具体中断的启用和关闭。让我们先从时钟中断入手。

扩展异常中断向量表的具体处理逻辑:

// axhal/src/riscv64/trap.rs
#[no_mangle]
fn riscv_trap_handler(tf: &mut TrapFrame) {
    let scause = scause::read();
    match scause.cause() {
		... ...
        Trap::Interrupt(_) => handle_irq_extern(scause.bits()),
		... ...
    }
}

/// Call the external IRQ handler.
#[allow(dead_code)]
pub(crate) fn handle_irq_extern(irq_num: usize) {
    call_interface!(TrapHandler::handle_irq, irq_num);
}

这里用到了 crate_interface,具体的 TrapHandler::handle_irq 实现在 axruntime 中:

// axruntime/src/trap.rs
#[cfg(all(target_os = "none", not(test)))]
struct TrapHandlerImpl;

#[cfg(all(target_os = "none", not(test)))]
#[crate_interface::impl_interface]
impl axhal::trap::TrapHandler for TrapHandlerImpl {
    fn handle_irq(irq_num: usize) {
        axhal::irq::dispatch_irq(irq_num);
    }
}

// axhal/src/riscv64/irq.rs
static TIMER_HANDLER: BootOnceCell<IrqHandler> = unsafe {
    BootOnceCell::new()
};

pub fn dispatch_irq(scause: usize) {
    match scause {
        S_TIMER => {
            log::trace!("IRQ: timer");
            TIMER_HANDLER.get()();
        },
        _ => panic!("invalid trap cause: {:#x}", scause),
    }
}

pub fn register_handler(scause: usize, handler: IrqHandler) -> bool {
    match scause {
        S_TIMER => {
            if !TIMER_HANDLER.is_init() {
                TIMER_HANDLER.init(handler);
                true
            } else {
                false
            }
        },
        _ => panic!("invalid trap cause: {:#x}", scause),
    }
}

中断处理同样基于中断的编号 scause 来确定具体的例程,就是上面的 dispatch_irq(scause),看到它会再调用TIMER_HANDLER 这个代表时钟处理例程的句柄。

内核在 axruntime 启动过程中会初始化中断 irq 的设施,并注册时钟处理的例程:

#[no_mangle]
#[cfg(all(target_os = "none", not(test)))]
pub extern "C" fn rust_main(hartid: usize, dtb: usize) -> ! {
    ... ...
    axtask::init_scheduler();

    info!("Initialize interrupt handlers...");
    #[cfg(all(target_os = "none", not(test)))]
    init_interrupt();

    #[cfg(not(test))]
    unsafe {
        main();
    }
	... ...
}

#[cfg(all(target_os = "none", not(test)))]
fn init_interrupt() {
    use axhal::irq::TIMER_IRQ_NUM;

    // Setup timer interrupt handler
    const PERIODIC_INTERVAL_NANOS: u64 =
        axhal::time::NANOS_PER_SEC / axconfig::TICKS_PER_SEC as u64;

    static mut NEXT_DEADLINE: u64 = 0;

    fn update_timer() {
        let now_ns = axhal::time::current_time_nanos();
        // Safety: we have disabled preemption in IRQ handler.
        let mut deadline = unsafe { NEXT_DEADLINE };
        if now_ns >= deadline {
            deadline = now_ns + PERIODIC_INTERVAL_NANOS;
        }
        unsafe { NEXT_DEADLINE = deadline + PERIODIC_INTERVAL_NANOS };
        trace!("now {} deadline {}", now_ns, deadline);
        axhal::time::set_oneshot_timer(deadline);
    }

    axhal::irq::register_handler(TIMER_IRQ_NUM, || {
        update_timer();
        debug!("On timer tick!");
    });

    // Enable IRQs before starting app
    axhal::irq::enable_irqs();
}

注册了一个时钟中断的例程:设置一个 1 秒后触发的定时器,每次触发都会重置定时器,让其下个 1 秒后再次触发,由此形成一个周期定时器。其中,设置定时器通过 axhal::time 模块实现:

// axhal/src/riscv64/time.rs
const TIMER_FREQUENCY: u64 = 10_000_000;    // 10MHz
pub const NANOS_PER_SEC: u64 = 1_000_000_000;
const NANOS_PER_TICK: u64 = NANOS_PER_SEC / TIMER_FREQUENCY;

#[inline]
pub fn current_ticks() -> u64 {
    time::read() as u64
}
#[inline]
pub const fn ticks_to_nanos(ticks: u64) -> u64 {
    ticks * NANOS_PER_TICK
}
#[inline]
pub fn current_time_nanos() -> u64 {
    ticks_to_nanos(current_ticks())
}

pub fn set_oneshot_timer(deadline_ns: u64) {
    sbi_rt::set_timer(nanos_to_ticks(deadline_ns));
}

OpenSBI 本身已经封装了对定时器处理的功能,所以函数 set_oneshot_timer 只需要调用 SBI 的 ecall 即可设置定时器。

最后需要注意:在 init_interrupt 的最后,需要开启全局中断。

// axhal/src/riscv64/irq.rs
#[inline]
pub fn enable_irqs() {
    unsafe { sstatus::set_sie() }
}

测试一下 make run LOG=trace,从跟踪日志中可以看到,定时器以 1 秒为周期进行调度,测试通过!