From cdbc08df78d365ea08c9b7a365cbe864164c19bb Mon Sep 17 00:00:00 2001 From: Lu Sitong <614149243@qq.com> Date: Sat, 13 Mar 2021 14:23:50 +0800 Subject: [PATCH] Update docs. --- ...中断.md => 构建调试-外部中断.md} | 0 doc/构建调试-外部中断v2.md | 166 ++++++++++++++++++ 2 files changed, 166 insertions(+) rename doc/{构建调试-S态外部中断.md => 构建调试-外部中断.md} (100%) create mode 100644 doc/构建调试-外部中断v2.md diff --git a/doc/构建调试-S态外部中断.md b/doc/构建调试-外部中断.md similarity index 100% rename from doc/构建调试-S态外部中断.md rename to doc/构建调试-外部中断.md diff --git a/doc/构建调试-外部中断v2.md b/doc/构建调试-外部中断v2.md new file mode 100644 index 0000000..28eb36e --- /dev/null +++ b/doc/构建调试-外部中断v2.md @@ -0,0 +1,166 @@ +# 外部中断的解决方案尝试 +AtomHeartCoder + +## DMA 控制器 +在刘一鸣同学的努力下,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 轮询方式,这样外部中断次数就正常了。至于原本的串口中断为何也很多,我就不得而知了。 + +## 一个很奇怪的问题 +也是 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`等都有,但寻指相关的寄存器未见出错 +2. 有时 S 态也会捕捉到 U 态的访存异常 + + `sepc` 对应的值在汇编文件中可以看到是正常的访存指令,但 `stval` 的值不太正常 +3. 我用刘一鸣同学的板子试了几次,也存在问题,但表现有所差别 + + RustSBI 报 panic 的概率小了很多,可以运行更久 + + S 态也会捕捉到用户进程的访存异常 + + 除了板子新一些,其他条件相同,包括板子型号、SD 卡、宿主机等 + +我们怀疑有以下原因,但都不能明确: +1. K210 板子自身的问题 + + 理由: + + 用户程序在 QEMU 上没有问题 + + 内陷 S 态前后,取指相关的寄存器没有问题,可能不是内核问题 + + 疑点: + + 如果是硬件问题,为何只会在 U 态下出错? + +2. K210 版 RustSBI 的问题 + + 理由: + + 如果板子硬件没问题,是否是硬件参数配置不合适? + + 外部中断处理时,M 态下执行了 S 态代码,有不稳定因素 + + 疑点: + + 这个报错在先前就已经存在 + +3. K210 版内核的问题 + + 理由: + + 内核直接支撑着用户程序的进行,是离用户程序最近的一层,最可能出错 + + 疑点: + + 我跟踪过内陷前后的情况,用户现场的保存应该没问题。 + +4. 用户栈空间的问题 + + 理由: + + 栈出错可能导致 `pc`、`ra`、`sp` 等寄存器出错 + + 疑点 + + 用户程序在 QEMU 上没有问题 + +凡事先从自己身上找原因。最近的移植工作涉及较多改动,这个过程中我们还没有能很好地排查内核代码中可能存在的问题,现在也只能慢慢 DEBUG 了。