-
Notifications
You must be signed in to change notification settings - Fork 210
Development Log
2021春季学期计划:
- New OS BOOK! 完善zcore tutorial(wrj, cx, xly, wyf, lfy, xsp,zyr等 每周讨论交流一次)
- ASYNC is future! 尝试进一步实现异步(io-uring, async zcore, ixy nic drv)(lfy等)
- APP is just the part of OS! 尝试比较直接(没有syscall,直接对接OS)把std放入zcore中,这样OS的功能就能无限扩充,且还有安全性保证。 (zyr等)
- zCore is in EVERY CPU! 完成对riscv,arm等的porting (xly,cx,wyf等)
- zCore is Linux & Zircon! 完善对linux, zircon syscall的支持
- zCore is APP! 进一步完善用户态的arch,在riscv/arm linux上,支持用户态的zcore。
感兴趣一起开发的朋友欢迎与我们联系。
实现了 sys_pc_firmware_tables
,同时更新 rboot 获取 SMBIOS 的地址。
之后用户程序就开始用 in/out 指令访问 IO Port,引起了 General Protection Fault。我直接在 rflags 中加入了 IOPL,允许用户态访问全部 IO Port。后续还需要进一步设置 TSS 中的 IO Port Bitmap,并实现 sys_ioports_{require, release}
以正确控制访问权限。
更新了 bitmap-allocator
,把 log2
改成了 Rust 内置函数实现,顺便修复了之前逆序分配物理帧的行为,改成了顺序分配。结果发现 zCore 跑不起来了,问题出在杰哥之前在 rCore 里写的自动扩容内核堆上,它依赖了物理帧逆序分配的“特性”。于是我干脆把它删掉了……
同时 PQL 实现了 sys_channel_write_etc
,通过了全部测试,并且能够运行 microbenchmark 了!我俩各自在 Linux 的 QEMU-KVM 上面跑了一发 Vmo benchmark,并和官方 Zircon 做了对比。不出意外,很多确实比官方慢,但也有一些异常地快。我怀疑是 zCore 的计时还有问题,因为目前在 QEMU 中读不到 CPU 主频,直接假设为 3 GHz,所以跑出来的结果并不准确。这方面还有待进一步分析。
合并了同学们的进展,并更新了 master。
重构了一下 Paged VMO 代码,将主要的 commit 操作改成了递归写法,并刷了一些测试。
Zircon libos 能够运行 shell 了!
之前 libos 卡在一个神秘 ud2
导致的 SIGILL 上面。经过一路侦查,发现它位于 vdso 中的一个复合操作中: zx_vmar_unmap_handle_close_thread_exit
。这个 syscall 是单独用汇编写的,所以之前 syscall -> function-call 的补丁漏掉了这种情况,导致它直接执行 syscall 指令进入 host 然后返回了错误(这个问题在 Linux musl libc 中也多次出现)。由于 syscall 并没有结束当前线程,不符合预期结果,所以 vdso 之后就执行 ud2
主动挂掉了。
一开始的解决方法就是把这里的 syscall 也改成 function-call,不过紧接着又出现了新的问题:SIGFAULT!而且是发生在 Rust 库的 stack_overflow
处理函数中。这说明用户程序发生了一次 SIGFAULT,然后被 Rust stdlib 捕获处理后又发生了一次(由于 fs 没有切换回来,因此访问 TLS 会挂掉)。经查发现用户程序 SIGFAULT 的位置跟刚才一样。这时才意识到这个复合 syscall 另有玄机。
之所以要把三个操作合一用汇编写,原因是第一个 unmap 操作用于把当前线程栈清理掉,而一旦成功后栈就不存在了,只能访问寄存器,所以这波操作无法在 C 中实现。注意!此时是可以执行 syscall
指令的,因为它不会访问栈。但是!我们之前把它改成了函数调用 call
指令,这时就会 push rip 到栈上,于是就炸了。经过一番思考,感觉在没有栈的情况下很难用其它方法跳到内核,还不如直接在内核里特殊处理,新增一个 syscall 来完成复合操作,用户态只需把它当成一个 syscall 处理就行了。
经过上述修改,在 Linux 上 libos 已经没有其它异常了,在 macOS 上还时不时会有神秘错误(比如 SIGBUS)。之后实现了一下 HAL-Unix 中的读函数:开一个线程来阻塞读 stdin(模拟内核中断处理函数),把数据通过“管道”转发到用户线程。这样 libos 中就可以跑 shell 了!
实现了 kcounter VMO,能够在用户态运行 kcounter 程序打印出计数信息了。
将预编译镜像升级到 20200413,包含 tests 和 microbenchmarks。添加了自动编译脚本。
重大进展!能够运行 shell 了!
这里有一些 hack:
- 在启动 shell 的同时还有一些驱动正在初始化,需要实现
sys_iommu_create
,我们在这里暂时让线程休眠,这样就不会打扰 shell 的运行了。 - shell 调用
sys_debug_read
时每次尝试读入一个字节,如果某次读到空,那么就会在某个 port 上等待,系统就卡住了。解决办法是暂时把sys_debug_read
实现为阻塞的,直接在内核里挂起等待,直到发生键盘或串口中断时再唤醒。
修复了 trap 6 的问题,原因是 debug_read
给了错误的输出。
之后就能看到出现 sys_iommu_create
了。
实现了简单的 kcounter。能够对部分内核对象的创建和销毁进行计数,并在 panic 时候打印出来。
在 LOG 中打印当前 tid+pid。这个需要 executor 提供 Task Local Storage 支持,目前仅在 libos 上有效。
之前把编译好的镜像丢给其他同学远程测试,发现 VirtualBox 和 NUC 上都跑不起来。今天修复了相关 bug。
VirtualBox 上的现象是 TripleFault,观察寄存器发现 rsp 指向了页表的物理地址,于是猜测是爆栈了。 这里顺便修复了 rboot 的一个坑:之前没有为内核设置单独的栈,还沿用 UEFI 环境的栈,一旦爆栈就有覆盖其它关键数据的危险。于是修复了这个问题,为内核设置了独立的栈。 改好后依然 TripleFault,虽然给了 1M 的空间,还是爆栈了。这说明出现了神秘 bug 导致无穷递归。
进一步查看 rip 定位到程序出错位置在 Logger 相关函数中,说明很可能是 log 过程中发生了 panic,而 panic 时又要 log,导致了无穷递归。在 log 的过程中,最可疑的操作就是获取当前时间了。把它去掉再跑,果然顺利打出了 panic 信息,提示发生了除零错误。于是问题找到了:当前时间的计算方法是 TSC / 处理器频率,后者需要去读 CPUID,而在 VirtualBox 中读到的数是 0。
这一现象通过查看 VirtualBox 的 log 也能证实:(处理器频率在 Leaf 16)
00:00:01.256962 Unknown CPUID Leaves
00:00:01.256962 Leaf/sub-leaf eax ebx ecx edx
00:00:01.256963 Gst: 00000014/0001 00000000 00000000 00000000 00000000
00:00:01.256964 Hst: 02490002 003f3fff 00000000 00000000
00:00:01.256965 Gst: 00000014/0002 00000000 00000000 00000000 00000000
00:00:01.256965 Hst: 00000000 00000000 00000000 00000000
00:00:01.256966 Gst: 00000015/0000 00000000 00000000 00000000 00000000
00:00:01.256967 Hst: 00000002 000000d8 00000000 00000000
00:00:01.256968 Gst: 00000016/0000 00000000 00000000 00000000 00000000
00:00:01.256968 Hst: 00000a28 00001194 00000064 00000000
解决办法是判断一下如果读到 0 就当它频率是 3GHz 好了。。到底该怎么获得频率留到以后解决。
修复了这个问题以后,VirtualBox 上就能顺利跑进用户程序了。不过接下来在解压 zbi 的时候出现了一个神秘错误:IO_DATA_INTEGRITY
,这个问题以前从来没有出现过。根据文档描述,这个错误的原因是数据校验失败。最后发现,是我在创建磁盘镜像时,不知从哪儿复制的命令限制了镜像大小为 48MB,于是就悄无声息地把 zbi 的一部分数据丢了。。去掉大小限制后问题解决,VirtualBox 上能跑出跟 QEMU 一样的结果了。
接下来我把同样的镜像放到了 NUC 上,结果在 rboot 阶段读文件时 UEFI 返回了奇怪的错误 INVALID_PARAMETER
,查阅文档发现那里面根本没提到 read 函数可能发生这种错误???于是我换了个 UEFI 环境,放到另一台 PC 机上跑,就没这问题。。真是醉了。
不过还有一个线索是,另一位同学在他的 NUC 上也跑出过这个错误,并且把烧好的 U 盘挂载到 VirtualBox 上也是同样的现象。这说明,可能是磁盘镜像的问题:我们在此之前都是用 Fuchsia 脚本的 fx mkzedboot
命令烧写的 U 盘,它会创建一个 64MB 的 EFI 分区,然后我们把 zCore 生成的文件复制到 EFI 分区里覆盖掉原有文件。比较令人担心的是,它这个分区太小了,也就刚好能放得下 zCore 那一套东西。这样想着,我就给它重新分区把 EFI 改大了,然后。。。错误就消失了,屏幕上出现了熟悉的画面,跟 QEMU 的结果一样。
这都是些什么奇怪问题啊!果然不同的平台总能给你带来不同的惊喜。我太难了。。。
接下来,我尝试开启 Global Page,放到 NUC 上跑没出现问题。对比关闭这个特性,简单测了下时间:从进入用户态开始计时,一直跑到 panic,时间是 1.150s,波动在 1ms 左右,是否开启 Global Page 未见显著区别。然后我统计了一下,这个过程中发生了 496 次任务切换(上下文切换),9999 次进入用户态并返回。而切换页表只会发生在任务切换中,显然这个数目还不够多,达不到影响性能的程度。
最后,这两天还解决了两次浮点异常的问题。
第一次的问题是用户态 0x13 异常(SIMD Floating-Point Exception)。这个问题会出现在 HAX/KVM 加速的 QEMU 和真机上,但在默认未开启加速的 QEMU 中不会出现。并且,在 libos 那边也出现了类似的 floating point exception。经过 QEMU/GDB 反汇编定位代码,发现它们是同一个位置的异常。然后,我去查阅了一下 x86 浮点异常的相关文档,发现是否抛出异常是由 MXCSR 寄存器控制的,它里面有一些 mask 位表示屏蔽对应的异常。重点是,文档里说这些位的初值都是 1!!!也就是说整个寄存器上电时的初值是非零的!!然而在我们的实现中,用户程序浮点寄存器的初始化是通过对一片全 0 区域做 FXRSTOR
完成的。鬼知道你的初始状态不是全 0 啊,这很反直觉啊好不好!?最后,我给 trapframe
库打了个补丁,修改了浮点寄存器结构的默认值。问题就解决了。
第二次,是今天晚上我尝试跑 CoreTests,跑到 FPUTest 时发生了 0x10 异常(x87 Floating-Point Exception)。其实我们之前跑过这个测试,只不过是用未加速的 QEMU,没有发生异常,直接失败就过去了。而开了硬件虚拟化加速后就会发生异常。(有了上一次的经验,我们早就接受了这个事实:QEMU 模拟的都是假的,浮点也不例外)接下来,同样的套路:定位指令,发现程序挂在了一条古老的 x87 指令上;查文档,RTFM,发现哇塞原来 x87 跟 SIMD 是两套独立系统,x87 有一套自己的控制和状态寄存器,指令异常由一个叫 x87 FPU Status Word
的寄存器控制,这里面也有一堆 mask 位,它们的初值也不是零!!好吧。然后我在 zCore 里面快速修复了一下它的初值,就没有异常了,不过 FPUTest 还是没过,可能计算结果还是有些偏差。不管了,我学不动了。
这次,我不打算再打补丁了,而是计划直接把浮点寄存器移出 trapframe
。因为根据调研,主流 OS 自己都是不用浮点寄存器的,平时这些值就挂在 CPU 上面作为内核线程的状态,等到上下文切换的时候再去保存恢复。另外根据今晚的统计数据,500 次上下文切换对应 10000x2 次用户态切换,显然应该把它们放到上下文切换里。不过,这个修改又是一项大工程,以后有空再说吧。
升级工具链到 nightly-2020-04-06。修复 CI,更新了 master。
在内核中添加了一些 hack,使得可以向 userboot 启动的下一个程序传递参数。于是就可以灵活地配置 standalone-test
要运行的测试。用这种办法手动将所有测试跑了一遍,填满了单元测试状态列表。此外,还发现 libos 中也可以跑测试,但在运行线程相关测试时会 SIGILL,原因是 vdso 中仍残留了 syscall 指令没有改成函数调用,不知道哪里漏了。