191 lines
13 KiB
Markdown
191 lines
13 KiB
Markdown
# 外部中断的解决方案尝试
|
||
AtomHeartCoder
|
||
|
||
## DMA 控制器
|
||
在 [Artyom Liu](https://github.com/retrhelo) 同学的努力下,SD 卡驱动可以正常工作了。数据传输通过 SPI 协议进行,
|
||
直接由 CPU 控制,也就是 CPU 死等传输的方式。随后,我们又注意到 K210 是有 DMA 控制器的(Direct Memory Access),
|
||
可以控制总线上数据的传输,从而释放 CPU 资源。而且 Kendryte 也提供了 DMA 控制器的驱动代码,稍加理解便可以直接将
|
||
我们需要的部分移植过来。这个过程很顺利,编译通过后,DMA 方式也能正常读取 SD 上的数据,成功打开 shell。
|
||
但是,此时仍然是 CPU 轮询 DMA 控制器的相关寄存器位来判断传输时候完成,因为 S 态下不能接收到外部中断。不过,
|
||
M 态下是可以收到外部中断的,但我更倾向于在 S 态进行中断响应。
|
||
|
||
## 尝试解决外部中断(一)
|
||
S 态下虽然没有外部中断,但是有软件中断。我一开始的设想是,M 态收到外部中断后,从 PLIC 得到中断源编号并通过 `stval` 传递,
|
||
然后写 `mip.SSIP` 将 S 态的软中断置位。虽然我不会 Rust,但还是不得不去修改 SBI 的代码。随后我发现,在 RustSBI 的代码里,
|
||
其实已经有同样的操作,但是被注释掉了,说明这种方案可能行不通。我仍然尝试了一下,并观察到以下现象:
|
||
|
||
+ 确实能够收到 DMA 控制器发来的中断,键盘输入也可以;
|
||
+ 外部中断源源不绝,期间只偶尔进了几次 S 态软中断处理,无论是 DMA 的中断还是接收键盘输入的串口中断;
|
||
+ 进一步地,我观察到 `mip.MEIP` 位(外部中断等待位)总是为 1。
|
||
|
||
根据我所学习到的 PLIC 的相关知识,在进行 PLIC claim/complete 操作时,PLIC 中对应中断源的 `IP` 寄存器和 `mip.xEIP` 位
|
||
都应该被清零 0。这很奇怪,于是我在每次 PLIC claim/complete 后多一次输出 `mip.MEIP`,并观察到有时它确实可以被清零!
|
||
我们知道,当 `mstatus.MIE`、`mie.MEIE` 以及 `mip.MEIP` 都为 1 时,外部中断会触发。暂且不深究为何外部中断总是不断地来,
|
||
先设想一下这样的情形:
|
||
|
||
在运行时 `mstatus.MIE` 和 `mie.MEIE` 都为 1,即允许外部中断。某时刻,中断源 A 向 PLIC 发送中断请求,通过仲裁后,
|
||
`mip.MEIP` 被置位,中断条件到达,中断信息被发送至中断目标(这里是某一个 hart 的 M 态)。某一 hart 的 M 态外部中断被触发,
|
||
`mstatus.MIE` 被硬件自动置 0,随后 PLIC claim/complete 清除 `mip.MEIP` 位,并设置 S 态软中断。倘若这一时刻,中断源 B
|
||
也向 PLIC 发送了一个中断请求,其优先级高于 A,那么仲裁后 `mip.MEIP` 又会被重新置位。当 M 态通过 `mret` 指令从 A 的中断
|
||
处理中返回时,`mstatus.MIE` 会重新设置为原先的值,中断条件再次到达。那么此时,这个 hart 是先处理先前设置的 S 态软中断,
|
||
还是刚刚到来的 B 外部中断呢?当然是 M 态外部中断优先,处理时又会再次设置 S 态软中断,从而将前一个 A 中断的设置覆盖掉,
|
||
导致我们丢失 S 态下对 A 中断应有的处理!
|
||
|
||
有没有办法解决呢?在 SBI 中的时钟中断处理中,可以找到答案:关闭 `mie.MEIE`。这使得外部中断被暂时屏蔽,从而使得 S 态软中断
|
||
可以紧接着进行。但是这也要求 S 态处理完毕后,向 SBI 发送通知,重新打开 `mie.MEIE`。这与目前我们对时钟中断的处理方式一致。
|
||
但再仔细想想,万一 S 态软中断被 M 态时钟中断插队,在时钟中断处理流程中,需要进行额外判断,才能决定是否重新置位 `mie.MEIE`。
|
||
这些都使得中断处理显得格外繁琐。
|
||
|
||
## 尝试解决外部中断(二)
|
||
还有第二种方法,就是吴一凡学长的方案:将 S 态中断处理函数的指针通过 SBI call 传入 M 态,外部中断到来时,直接以 M 态的身份
|
||
执行 S 态的代码。前不久我们也尝试了这个方法,但是出现了访存异常。我研究了一下 SBI 中的相关代码,如下:
|
||
```rust
|
||
unsafe {
|
||
let mut mstatus: usize;
|
||
llvm_asm!("csrr $0, mstatus" : "=r"(mstatus) ::: "volatile");
|
||
// set mstatus.mprv
|
||
mstatus |= 1 << 17;
|
||
// it may trap from U/S Mode
|
||
// save mpp and set mstatus.mpp to S Mode
|
||
let mpp = (mstatus >> 11) & 3;
|
||
mstatus = mstatus & !(3 << 11);
|
||
mstatus |= 1 << 11;
|
||
// drop mstatus.mprv protection
|
||
llvm_asm!("csrw mstatus, $0" :: "r"(mstatus) :: "volatile");
|
||
fn devintr() {
|
||
unsafe {
|
||
// call devintr defined in application
|
||
// we have to ask compiler save ra explicitly
|
||
llvm_asm!("jalr 0($0)" :: "r"(DEVINTRENTRY) : "ra" : "volatile");
|
||
}
|
||
}
|
||
// compiler helps us save/restore caller-saved registers
|
||
devintr();
|
||
// restore mstatus
|
||
mstatus = mstatus &!(3 << 11);
|
||
mstatus |= mpp << 11;
|
||
mstatus -= 1 << 17;
|
||
llvm_asm!("csrw mstatus, $0" :: "r"(mstatus) :: "volatile");
|
||
}
|
||
```
|
||
其中`DEVINTRENTRY` 就是我们传入的 S 态处理函数的地址。代码还置了 `mstatus.MPRV` 位,这会使得 M 态下以 `mstatus.MPP` 中
|
||
对应特权态的方式(即可以使用页表)进行访存,这样就可以执行 S 态的代码了,执行完毕后再恢复 `mstatus` 寄存器即可。我觉得很妙,
|
||
这不就能访问 S 态的数据和函数了吗?设想是好的,但是在 xv6 的情况下就有问题了:
|
||
|
||
+ 据我了解到的资料,`mstatus.MPRV` 位的设置只对访存指令有效,对 `pc` 指令寻址没有影响;
|
||
+ xv6 的代码段和数据段是直接映射的(即虚拟地址映射到相等的物理地址),有没有页表其实没有很大区别;
|
||
+ xv6 的栈段不是直接映射。
|
||
|
||
我在上述代码中没有注意到对 `sp` 的操作,也就是说,使用 S 态页表时,`sp` 是 M 态下的栈顶指针!这样映射就会出错了!更何况,
|
||
SBI 的地址是低于内核地址的,S 态页表中根本没有对 SBI 的映射。那么交换一下栈顶指针是不是就好了呢?还不见得,也是同样的问题。
|
||
`mstatus` 的原值保存在 M 态的栈上,一旦使用了内核页表,就无法回到 M 态的栈上了。我们可以使用 `mscratch` 寄存器保存一些值,
|
||
但我还是觉得繁琐。即使可以写出代码,我们也不能很明确 S 态的栈是什么情况,贸然让 M 态在上面执行,有可能会产生数据覆盖的风险。
|
||
那么,根据 xv6 直接映射的特点,我索性舍弃这一繁琐操作,直接让 M 态在自己的栈上执行 S 态的代码。代码就很简单了:
|
||
```rust
|
||
fn devintr() {
|
||
unsafe {
|
||
llvm_asm!("jalr 0($0)" :: "r"(DEVINTRENTRY) : "ra" : "volatile");
|
||
}
|
||
}
|
||
devintr();
|
||
```
|
||
结果就是能运行了!只不过又产生了新问题,虽然中断处理代码是从 S 态角度写的,但实际上是 M 态在执行,那么在控制台 IO 的处理上,
|
||
我们就不便使用 SBI call 进行操作了,而应该直接操控串口。好在 Kendryte 也有相关的代码,直接搬过来适配一下就好了。
|
||
|
||
外部中断能顺利处理后,我也发现了之前 DMA 中断源源不断的原因:在 SD 卡的操作中,需要向 SD 卡发送命令,并等待 SD 卡的回复。
|
||
由于命令较短不太耗时(但不一定能一次就读到回复,毕竟 CPU 比 SD 快太多),这个操作采用了轮询方式:
|
||
```C
|
||
...
|
||
int timeout = 0xfff;
|
||
while (--timeout) {
|
||
sd_read_data(&result, 1);
|
||
if (0xfe == result) break;
|
||
}
|
||
...
|
||
```
|
||
我最初把 SD 卡调用 SPI 的接口都改用了 DMA 方式,这么循环多次,才导致了很多次外部中断。我仅将数据传输改用 DMA 方式,而命令收发
|
||
保留 CPU 轮询方式,这样外部中断次数就正常了。至于原本的串口中断为何也很多,我就不得而知了。
|
||
|
||
这期间我和 [Artyom Liu](https://github.com/retrhelo) 同学多次探讨了外部中断的处理方式,考虑过各种方式的优点与不足,
|
||
但都不能找到一个十分完美的做法。在这里我也要感谢 [Artyom Liu](https://github.com/retrhelo) 给予的建议和帮助!就目前来看,
|
||
暂时先使用这种方式处理外部中断吧,我们也只有对各种方案都进行一些尝试,才可能摸索出一套最合适的解决方案吧。
|
||
|
||
## 一个很奇怪的问题
|
||
也是 SD 卡驱动刚做好的那会,我想测试 SD 卡和文件系统在 K210 上的运行情况。那时键盘的输入还是在时钟中断过程中直接从串口取回的。
|
||
在板子上跑,看起来也能命令 `shell`,执行用户程序。但运行没几分钟,RustSBI 就会在某次敲下回车后,报了`panic`,是非法指令异常。
|
||
我起初怀疑这与串口有关,但当时键盘输入也只是临时方案,说不定改成通常的中断方式就可以解决这个问题。但是目前来看,并没有解决,
|
||
问题依然存在。当时外部中断还没起来,问题就已经存在,所以外部中断处理不太可能是问题产生的根本原因。
|
||
|
||
我决定定位这个 BUG。根据 RustSBI 的报错,`mepc` 的值符合用户程序的特征,`mstatus.MPP` 也指明是 U 态下发生的异常。按理说,
|
||
正常编写的用户程序应该不会导致如此严重的异常,况且同样的用户程序在 QEMU 上就没出现过问题。用户态出现非法指令,而我们知道实际上
|
||
并没有非法指令,只能说明,`pc` 可能指向了非代码段。我怀疑在内核中是不是覆盖了 `trapframe` 中的现场,包括 `epc`、`pc`、`ra`等,
|
||
于是我在 `usertrap` 和 `usertrapret` 中检查了这些值,都很正常。从 `ra` 出发,对照用户程序对应的 `asm` 文件,也未发现会将 `pc`
|
||
导向至一个不正确地址的迹象。同时,还有一些其他问题。我总结如下:
|
||
|
||
1. 某次输入命令后,RustSBI 会 panic,大多数情况是非法指令异常,偶尔有访存不对齐
|
||
+ `mepc` 中的值指向了错误位置(在用户程序的汇编文件中不存在对应指令)
|
||
+ 用户程序多是 `sh`,出错前的最后一次系统调用不固定,`write`、`exec`、`wait`等都有,但寻指相关的寄存器未见出错
|
||
+ 有时 `mepc` 指向的指令看起来很正常,是否是 K210 不支持的指令?
|
||
2. 有时 S 态也会捕捉到 U 态的访存异常/取指异常
|
||
+ 对于访存异常,`sepc` 对应的值在汇编文件中可以看到是正常的访存指令,但 `stval` 的值不太正常
|
||
+ 对于取指异常,`sepc` 出现了类似 `0x0000000505050504` 这样的值
|
||
3. 我用 [Artyom Liu](https://github.com/retrhelo) 同学的板子试了几次,也存在问题,但表现有所差别
|
||
+ RustSBI 报 panic 的概率小了很多,可以运行更久
|
||
+ S 态也会捕捉到用户进程的访存异常
|
||
+ 除了板子新一些,其他条件相同,包括板子型号、SD 卡、宿主机等
|
||
|
||
我们怀疑有以下原因,但都不能明确:
|
||
1. K210 板子自身的问题
|
||
|
||
理由:
|
||
+ 用户程序在 QEMU 上没有问题
|
||
+ 内陷 S 态前后,取指相关的寄存器没有问题,可能不是内核问题
|
||
|
||
疑点:
|
||
+ 如果是硬件问题,为何只会在 U 态下出错?
|
||
|
||
2. K210 版 RustSBI 的问题
|
||
|
||
理由:
|
||
+ 如果板子硬件没问题,是否是硬件参数配置不合适?
|
||
+ 外部中断处理时,M 态下执行了 S 态代码,有不稳定因素
|
||
|
||
疑点:
|
||
+ 这个报错在先前就已经存在
|
||
|
||
3. K210 版内核的问题
|
||
|
||
理由:
|
||
+ 内核直接支撑着用户程序的进行,是离用户程序最近的一层,最可能出错
|
||
|
||
疑点:
|
||
+ 我跟踪过内陷前后的情况,用户现场的保存应该没问题。
|
||
|
||
4. 用户栈空间的问题
|
||
|
||
理由:
|
||
+ 栈出错可能导致 `pc`、`ra`、`sp` 等寄存器出错
|
||
|
||
疑点
|
||
+ 用户程序在 QEMU 上没有问题
|
||
|
||
5. 用户页表的问题
|
||
|
||
理由:
|
||
+ `mepc` 指向的指令地址可能未正确映射,才导致取到非法或错误指令
|
||
|
||
疑点:
|
||
+ RustSBI 通过页表和 `mepc` 取到的指令与用户汇编文件中的指令一致,表明页表应当正常
|
||
|
||
|
||
凡事先从自己身上找原因。最近的移植工作涉及较多改动,这个过程中我们还没有能很好地排查内核代码中可能存在的问题,现在也只能慢慢 DEBUG 了。
|
||
|
||
更新:内核代码确实存在问题。内核的中断开关操作置的是 `sstatus` 寄存器的位,而实际上外部中断由 M 态截获,这会导致死锁。
|
||
开关中断可能要改成 SBI call 的形式,让 M 态打开/关闭外部中断。但是,M 态也会经过这一执行路径,遇到 SBI call 的调用,这就有些麻烦了。
|
||
M 态在处理外部中断时,本就处于中断关闭状态,是否可以另写 M 态的锁操作,从而绕过中断开关过程呢?经过讨论,我们认为这样做会造成代码冗杂,
|
||
遂放弃。
|
||
|
||
## 尝试解决外部中断(三)
|
||
这个想法其实是方法(一)的扩展。如果收到中断,无论是时钟中断或是外部中断,都将 `mie` 的 `MEIE`、`MTIE` 关闭,再对应置 S 态的时钟中断、
|
||
软中断,是否就可以保证 M 态收到时钟/外部中断后,接着进入 S 态时钟/软中断了呢?当然,如果这么做,还需要 S 态处理结束后,通过 SBI call
|
||
将中断重新打开。 |