时钟中断

在本节中,我们处理一种很重要的中断:时钟中断。这种中断我们可以设定为每隔一段时间硬件自动触发一次,在其对应的中断处理程序里,我们回到内核态,并可以强制对用户态或内核态的程序进行打断、调度、监控,并进一步管理它们对于资源的使用情况。

riscv 中的中断寄存器

S 态的中断寄存器主要有 sie(Supervisor Interrupt Enable,监管中断使能), sip (Supervisor Interrupt Pending,监管中断待处理)两个,其中 s 表示 S 态,i 表示中断, e/p 表示 enable (使能)/ pending (提交申请)。 处理的中断分为三种:

  1. SI(Software Interrupt),软件中断
  2. TI(Timer Interrupt),时钟中断
  3. EI(External Interrupt),外部中断

比如 sie 有一个 STIE 位, 对应 sip 有一个 STIP 位,与时钟中断 TI 有关。当硬件决定触发时钟中断时,会将 STIP 设置为 1,当一条指令执行完毕后,如果发现 STIP 为 1,此时如果时钟中断使能,即 sieSTIE 位也为 1 ,就会进入 S 态时钟中断的处理程序。

时钟初始化

  1. // src/lib.rs
  2. mod timer;
  3. // src/timer.rs
  4. use crate::sbi::set_timer;
  5. use riscv::register::{
  6. time,
  7. sie
  8. };
  9. // 当前已触发多少次时钟中断
  10. pub static mut TICKS: usize = 0;
  11. // 触发时钟中断时间间隔
  12. // 数值一般约为 cpu 频率的 1% , 防止过多占用 cpu 资源
  13. static TIMEBASE: u64 = 100000;
  14. pub fn init() {
  15. unsafe {
  16. // 初始化时钟中断触发次数
  17. TICKS = 0;
  18. // 设置 sie 的 TI 使能 STIE 位
  19. sie::set_stimer();
  20. }
  21. // 硬件机制问题我们不能直接设置时钟中断触发间隔
  22. // 只能当每一次时钟中断触发时
  23. // 设置下一次时钟中断的触发时间
  24. // 设置为当前时间加上 TIMEBASE
  25. // 这次调用用来预处理
  26. clock_set_next_event();
  27. println!("++++ setup timer! ++++");
  28. }
  29. pub fn clock_set_next_event() {
  30. // 调用 OpenSBI 提供的接口设置下次时钟中断触发时间
  31. set_timer(get_cycle() + TIMEBASE);
  32. }
  33. // 获取当前时间
  34. fn get_cycle() -> u64 {
  35. time::read() as u64
  36. }

开启内核态中断使能

事实上寄存器 sstatus 中有一控制位 SIE,表示 S 态全部中断的使能。如果没有设置这个SIE控制位,那在 S 态是不能正常接受时钟中断的。

  1. // src/interrupt.rs
  2. pub fn init() {
  3. unsafe {
  4. extern "C" {
  5. fn __alltraps();
  6. }
  7. sscratch::write(0);
  8. stvec::write(__alltraps as usize, stvec::TrapMode::Direct);
  9. // 设置 sstatus 的 SIE 位
  10. sstatus::set_sie();
  11. }
  12. println!("++++ setup interrupt! ++++");
  13. }

响应时钟中断

让我们来更新 rust_trap 函数来让它能够处理多种不同的中断——当然事到如今也只有三种中断:

  1. 使用 ebreak 触发的断点中断;
  2. 使用 ecall 触发的系统调用中断;
  3. 时钟中断。
  1. // src/interrupt.rs
  2. use riscv::register::{
  3. scause::{
  4. self,
  5. Trap,
  6. Exception,
  7. Interrupt
  8. },
  9. sepc,
  10. stvec,
  11. sscratch,
  12. sstatus
  13. };
  14. use crate::timer::{
  15. TICKS,
  16. clock_set_next_event
  17. };
  18. #[no_mangle]
  19. pub fn rust_trap(tf: &mut TrapFrame) {
  20. // 根据中断原因分类讨论
  21. match tf.scause.cause() {
  22. // 断点中断
  23. Trap::Exception(Exception::Breakpoint) => breakpoint(&mut tf.sepc),
  24. // S态时钟中断
  25. Trap::Interrupt(Interrupt::SupervisorTimer) => super_timer(),
  26. _ => panic!("undefined trap!")
  27. }
  28. }
  29. // 断点中断处理:输出断点地址并改变中断返回地址防止死循环
  30. fn breakpoint(sepc: &mut usize) {
  31. println!("a breakpoint set @0x{:x}", sepc);
  32. *sepc += 2;
  33. }
  34. // S态时钟中断处理
  35. fn super_timer() {
  36. // 设置下一次时钟中断触发时间
  37. clock_set_next_event();
  38. unsafe {
  39. // 更新时钟中断触发计数
  40. // 注意由于 TICKS 是 static mut 的
  41. // 后面会提到,多个线程都能访问这个变量
  42. // 如果同时进行 +1 操作,会造成计数错误或更多严重bug
  43. // 因此这是 unsafe 的,不过目前先不用管这个
  44. TICKS += 1;
  45. // 每触发 100 次时钟中断将计数清零并输出
  46. if (TICKS == 100) {
  47. TICKS = 0;
  48. println!("* 100 ticks *");
  49. }
  50. }
  51. // 发生外界中断时,epc 指向的指令没有完成执行,因此这里不需要修改 epc
  52. }

同时修改主函数 rust_main

  1. // src/init.rs
  2. #[no_mangle]
  3. pub extern "C" fn rust_main() -> ! {
  4. crate::interrupt::init();
  5. // 时钟初始化
  6. crate::timer::init();
  7. unsafe {
  8. asm!("ebreak"::::"volatile");
  9. }
  10. panic!("end of rust_main");
  11. loop {}
  12. }

我们期望能够同时处理断点中断和时钟中断。断点中断会输出断点地址并返回,接下来就是 panic,我们 panic 的处理函数定义如下:

  1. // src/lang_items.rs
  2. #[panic_handler]
  3. fn panic(info: &PanicInfo) -> ! {
  4. println!("{}", info);
  5. loop {}
  6. }

就是输出 panic 信息并死循环。我们可以在这个死循环里不断接受并处理时钟中断了。

最后的结果确实如我们所想:

breakpoint & timer interrupt handling

  1. ++++ setup interrupt! ++++
  2. ++++ setup timer! ++++
  3. a breakpoint set @0x8020002c
  4. panicked at 'end of rust_main', src/init.rs:11:5
  5. * 100 ticks *
  6. * 100 ticks *
  7. ...

如果出现问题的话,可以在这里找到目前的代码。