参考比赛官网去年"内核赛要求"部分
- 启动和系统初始化
- 内存管理
- 进程管理和中断异常机制
- 系统调用
- 文件系统
- 命令解释程序
我个人选择做一些更改:
- 启动和系统初始化
- 物理内存管理
- 虚拟内存管理
- 进程管理
- 中断异常机制
- 系统调用
- 文件系统
命令解释程序我将其归为用户程序部分
entry.S -> start.c -> main.c
- cpu启动顺序
risc-v下hart(类似于x86体系下的cpu)可以认为是在同一时间同时启动。 而x86中首先由主处理器于
0x7c00
处启动, 运行一段汇编代码并设置好栈(以运行C语言代码), 随后便进入到main.c
之中, 此时内核运行0x10_0000
。 在main.c
之中首先于startothers()
将用于其他cpu启动的汇编代码移动到0x7000
, 随后通过IPI启动其他核。 xv6-riscv中, 由于risc-v的体系结构特性则去掉了通过ipi启动的方式。 甚至由于设计问题, 还没有开启ipi功能(即hart的软件中断)。
设置了每个hart单独使用的stack。
此处留下几个问题:
- 检查了xv6-riscv的页表映射, 发现是由0x8000_0000 -> 0x8000_0000。由于x86中xv6也是放在虚存的这个位置, 怀疑是为了和旧版本兼容(但也不排除就是为了避免S态和M态的栈问题), 这导致从entry.S开始整个内核始终在使用同一个栈, 而这对大部分risc-v内核而言是不现实的, 这导致M态和S态的栈位于同一位置, 从而避免了由S态trap入M态时的栈位置转化。
- 我们之后讨论到底我们改使用几个栈, 以及是否像我之前尝试的那样使用多个栈空间(临时空间 + 多个临时压栈空间)
- 但相同的是运行时栈, M态和S态用于维护C语言程序正常运行的栈是同一个, 但是当转跳到S态后, xv6-riscv没有再在M态设计使用C语言(见kernelvec.S中的timervec), 由此我们必须得讨论中断/异常设计对栈设计时的影响。
- 我们确实可以讨论的一个话题是, 我们是否有能力去减少M态初始化时的运行时栈。例如: xv6-riscv在main.c中设计了一个公用的started参数, 当0号hart(也就是hart id为0, 也就是mhartid寄存器为0的hart)将started置1后才开始运行。由于要一直判断started是否为0, 此处其他核的循环是普通的死循环, 没有用到wfi指令的特性(由于ISA规范, wfi的最简单实现可能是nop, 如果这样并不能提升死循环的功耗)。我们可以在从hart(借用x86主从CPU的概念)设置栈之前开启mie下的软中断位(mstatus中仍然关中断, 一是由于这样不会影响软中断对此hart的唤醒, 二是避免引发中断陷入我们还未初始化的mtvec), 我们可以采取类似xv6的方法在main.c中使用ipi去唤醒从hart。关于这条我们有两点要补充。
- risc-v体系下没有绝对的主从处理器之分(在软件上, 硬件设计上例如qemu模拟的那块芯片就是0号和其他不一样), 但我们会使用0号处理器来主导整个启动进程, 这是因为在手册中0号处理器是绝对要求实现的。
- 即使要与xv6的过程类似, 我们也不能将发送ipi唤醒的过程放在main.c中(只是考虑ipi唤醒启动, 毕竟ipi在设计上还有通知其他处理器刷新页表的软件作用, PS. 不一定要用ipi来实现提醒其他处理器刷新页表), main.c已经位于S态, 考虑到日后别人对我们系统的移植可能基于SBI, 在这样的情况下我们应该让所有hart几乎同一时间进入S态。又得讨论核的唤醒顺序问题, 但我们可以在进入S态之后再次设计一套wfi等待循环(这次的目的不再是节约运行时栈, 而是1.考虑到0号在主导启动, 所以0号可能是最后进入S态的, 因为它要一个一个确保唤醒其他hart后才自己进入S态。2.减少普通死循环的时间, 尽管如果我们按照xv6-riscv的思路移植又回遇到0号要唤醒其他所以会在唤醒其他hart之后才运行
userinit()
, 而其他核又要等待userinit()
运行完之后才开始初始化, 相当于其他核又要等0号, 所以我们总要有普通死循环去等, 我们只是减少了这个的时间,所以引出以下问题) - 是否能通过我们的代码设计使6中的普通死循环时间尽量的短, 或者有没有方法避免出现这种等待?
- 使用这种唤醒的机制会不会导致启动时间的增加?
start.c流程:
设置转跳到S态的地址为
main()
->
禁止页表 ->
向S态代理所有的中断和异常 ->
使能S态二级中断(代指sie中的位) ->
使能S态物理内存访问 ->
计时器初始化(实际上做了更多的事) ->
将tp寄存器设置为hart id 转跳 -> main.ctimerinit()(1)
设置下一次时钟中断 ->
设置了M态中断处理的特殊栈(2), 并用mscratch保存 -> 设置了M态的中断处理转跳地址(3) ->
使能M态全局中断(指mstatus中的mie位) ->
使能M态计时器中断的二级中断注:
(1) 对于xv6-riscv的设计而言, 这些确实能称为计时器的初始化流程, 但我个人不认同这种设计。(2) xv6-riscv中mscratch保存的地址并不能称为栈, 它就是一个固定的位置, 并且不能向栈一样处理嵌套的情况 (只能处理一次的数据)。 但由于risc-v没有专门的push / pop栈的指令, 使得在代码实现上感觉在压栈 (risc-v的压出栈操作本质就是向内存读写的指令)。 但像openSBI是切切实实在mscratch中保存了一个额外的地址, 其作为S态陷入M态时的栈。
(3) 虽说是中断的转跳地址, 但其实处理的只有转发 M态定时器中断(MTIP)。 在此我们提前讨论的原因是: xv6-riscv转发使用的 是SSIP(S态软件中断)。
为什么转发的不是STIP(S态定时器中断)
下面我们讨论三种中断在M态和S态中设置(set)与清除的条件:
MEIP:
设置:PLIC设备
清除:PLIC设备注意, PLIC有两个动作来处理(注意用词不是清除)一个pending:
claim
与complete
claim
可以清除当前mip中的MEIP并获取当前外部中断中最高优先级的中断,claim
可以在任何时候读取, 无论是否屏蔽一、二级中断, 无论pending位是否已经被清除。
只有当complete
动作完成, 才会有后续动作。(如果只claim
但不complete
会导致PLIC无法引发后续中断。)SEIP:
设置:根据手册, SEIP在M模式下可以软件的设置(S态下read-only); 在实践中在S态的外部中断也会直接设置SEIP位; 我们担心一个情况: 我们在M态中处理一些事物的时候如果遇到外部中断, 如果这个中断设置了MEIP位, 会不会导致回到S态后我们漏掉这个外部中断?
我和rustSBI的作者交流过, 这一块尚且模糊。另外qemu6.1.1中暂时无法在M态软的地设置mip.SEIP, 经和同好讨论, 暂且认为是qemu的issue, 已经报了issue, 在等待qemu回复。
我暂时的设计是:
// 设计的几个个前提:
// 1.能软件地在M态中设置mip.SEIP (特权手册支持, 等qemu回复)
// 2.claim掉PLIC后, 不会影响mip.SEIP
// (特权手册有说过"signal from the external interrupt controller"
// 不会影响mip.SEIP的读写, 如果我阅读理解没有错的话, 所以应该是满足的)
// 3.sip.SEIP能被或能再次被PLIC的claim操作清除
// (实践中的sip.SEIP一定能被claim清除, 但不清除我在设计中的二次清除能否完成)
// 4.1. 外部中断在M态发生就出发M态外部中断、在S态发生就出发S态外部中断;
// 4.2. 在任何时候发生都只引发M态外部中断。
// (从FU5400的手册中更像前者, 不过这两种情况我们都能应对)
#
// 假设在M态遇到了外部中断 / 只有M态外部中断
// now in M mode:
claim_PLIC() // clear the mip.MEIP pending
set_bit(mip.SEIP) // 转发中断(条件1)
mret(S-Mode) // 回S态
// now in S mode:
claim_PLIC() // clear the sip.SEIP pending(条件1)
handle() // 处理
清除:如上设计
其他中断处理见backup中SBI笔记
与xv6不同xv6-riscv在进行物理内存回收前先进行了输出的初始化。个人非常不认同这样的处理。
consoleinit()
与printfinit()
初始化锁 ->
uartinit ->
初始化uart参数 ->
初始化uart的锁
在devsw中登记consolewrite / read
初始化锁 ->
指定之后printf()
都使用锁
panic()
会取消在printf()
中使用锁printf()
使用的是consputc()
流程:
knit()本质调用freerange(), freerange()不断调用kfree()
- 最明显的是在此时xv6-riscv没有使用页表, 而xv6中此时已经做了内存的平移映射。
这引发了两个问题:
- 由于此时xv6-riscv使用的是物理内存,
kfree()
能够将全部将所有的空间一次性回收。如果我们此时开临时页表就可能会遇到只能访问到2GiB的问题, 可能要两次回收。- 难以言说xv6-riscv中回收的内存参数到底是物理内存还是虚拟内存**(即使xv6-riscv中参数名显示是物理地址)**。之前也提到过, 从后面虚拟内存管理中可以看到, 做内核映射的部分是原地映射的。但xv6中很明确,
kfree()
与kalloc()
的参数/返回值都是以平移后的虚拟内存为参数的。
- 在xv6中, 早期的两次内存回收(将所有内存都回收)时是不使用锁的, 这是由
kmem.use_lock
控制的。xv6-riscv中取消了这个变量, 即使在早期回收内存时(此时不可能有其他CPU发生临界区冲突)也使用了锁。但是从整体来看这是划算的, 消除了需要使用锁的时候对kmem.use_lock
的判断。 - 在xv6中只有在
kfree()
才在回首时写入脏数据, 而在xv6-riscv中kfree()
与kalloc()
都有写入脏数据的过程。我没有仔细思考这会不会保护kalloc()
中的链表数据。(xv6与xv6-riscv都用了回收的内存块中的一部分作为链表的管理数据。)