时钟中断
在本节中,我们处理一种很重要的中断:时钟中断。这种中断我们可以设定为每隔一段时间硬件自动触发一次,在其对应的中断处理程序里,我们回到内核态,并可以强制对用户态或内核态的程序进行打断、调度、监控,并进一步管理它们对于资源的使用情况。
riscv 中的中断寄存器
S 态的中断寄存器主要有 sie(Supervisor Interrupt Enable,监管中断使能), sip (Supervisor Interrupt Pending,监管中断待处理)两个,其中 s 表示 S 态,i 表示中断, e/p 表示 enable (使能)/ pending (提交申请)。 处理的中断分为三种:
- SI(Software Interrupt),软件中断
- TI(Timer Interrupt),时钟中断
- EI(External Interrupt),外部中断
比如
sie有一个STIE位, 对应sip有一个STIP位,与时钟中断 TI 有关。当硬件决定触发时钟中断时,会将STIP设置为 1,当一条指令执行完毕后,如果发现STIP为 1,此时如果时钟中断使能,即sie的STIE位也为 1 ,就会进入 S 态时钟中断的处理程序。
时钟初始化
1. // src/lib.rs
3. mod timer;
5. // src/timer.rs
7. use crate::sbi::set_timer;
8. use riscv::register::{
9. time,
10. sie
11. };
13. // 当前已触发多少次时钟中断
14. pub static mut TICKS: usize = 0;
15. // 触发时钟中断时间间隔
16. // 数值一般约为 cpu 频率的 1% , 防止过多占用 cpu 资源
17. static TIMEBASE: u64 = 100000;
18. pub fn init() {
19. unsafe {
20. // 初始化时钟中断触发次数
21. TICKS = 0;
22. // 设置 sie 的 TI 使能 STIE 位
23. sie::set_stimer();
24. }
25. // 硬件机制问题我们不能直接设置时钟中断触发间隔
26. // 只能当每一次时钟中断触发时
27. // 设置下一次时钟中断的触发时间
28. // 设置为当前时间加上 TIMEBASE
29. // 这次调用用来预处理
30. clock_set_next_event();
31. println!("++++ setup timer! ++++");
32. }
34. pub fn clock_set_next_event() {
35. // 调用 OpenSBI 提供的接口设置下次时钟中断触发时间
36. set_timer(get_cycle() + TIMEBASE);
37. }
39. // 获取当前时间
40. fn get_cycle() -> u64 {
41. time::read() as u64
42. }
开启内核态中断使能
事实上寄存器 sstatus 中有一控制位 SIE,表示 S 态全部中断的使能。如果没有设置这个SIE控制位,那在 S 态是不能正常接受时钟中断的。
1. // src/interrupt.rs
3. pub fn init() {
4. unsafe {
5. extern "C" {
6. fn __alltraps();
7. }
8. sscratch::write(0);
9. stvec::write(__alltraps as usize, stvec::TrapMode::Direct);
10. // 设置 sstatus 的 SIE 位
11. sstatus::set_sie();
12. }
13. println!("++++ setup interrupt! ++++");
14. }
响应时钟中断
让我们来更新 rust_trap 函数来让它能够处理多种不同的中断——当然事到如今也只有三种中断:
- 使用
ebreak触发的断点中断; - 使用
ecall触发的系统调用中断; - 时钟中断。
1. // src/interrupt.rs
3. use riscv::register::{
4. scause::{
5. self,
6. Trap,
7. Exception,
8. Interrupt
9. },
10. sepc,
11. stvec,
12. sscratch,
13. sstatus
14. };
15. use crate::timer::{
16. TICKS,
17. clock_set_next_event
18. };
20. #[no_mangle]
21. pub fn rust_trap(tf: &mut TrapFrame) {
22. // 根据中断原因分类讨论
23. match tf.scause.cause() {
24. // 断点中断
25. Trap::Exception(Exception::Breakpoint) => breakpoint(&mut tf.sepc),
26. // S态时钟中断
27. Trap::Interrupt(Interrupt::SupervisorTimer) => super_timer(),
28. _ => panic!("undefined trap!")
29. }
30. }
32. // 断点中断处理:输出断点地址并改变中断返回地址防止死循环
33. fn breakpoint(sepc: &mut usize) {
34. println!("a breakpoint set @0x{:x}", sepc);
35. *sepc += 2;
36. }
38. // S态时钟中断处理
39. fn super_timer() {
40. // 设置下一次时钟中断触发时间
41. clock_set_next_event();
42. unsafe {
43. // 更新时钟中断触发计数
44. // 注意由于 TICKS 是 static mut 的
45. // 后面会提到,多个线程都能访问这个变量
46. // 如果同时进行 +1 操作,会造成计数错误或更多严重bug
47. // 因此这是 unsafe 的,不过目前先不用管这个
48. TICKS += 1;
49. // 每触发 100 次时钟中断将计数清零并输出
50. if (TICKS == 100) {
51. TICKS = 0;
52. println!("* 100 ticks *");
53. }
54. }
55. // 发生外界中断时,epc 指向的指令没有完成执行,因此这里不需要修改 epc
56. }
同时修改主函数 rust_main :
1. // src/init.rs
3. #[no_mangle]
4. pub extern "C" fn rust_main() -> ! {
5. crate::interrupt::init();
6. // 时钟初始化
7. crate::timer::init();
8. unsafe {
9. asm!("ebreak"::::"volatile");
10. }
11. panic!("end of rust_main");
12. loop {}
13. }
我们期望能够同时处理断点中断和时钟中断。断点中断会输出断点地址并返回,接下来就是 panic,我们 panic 的处理函数定义如下:
1. // src/lang_items.rs
3. #[panic_handler]
4. fn panic(info: &PanicInfo) -> ! {
5. println!("{}", info);
6. loop {}
7. }
就是输出 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. ...
如果出现问题的话,可以在这里找到目前的代码。
