zCore 的用户态运行支持
libos 版 zCore(简称uzCore) 的开发与裸机版 zCore (简称bzCore)同步进行,两个版本的 zCore 共用除了HAL 层之外的所有代码。为了支持 uzCore 的正常运行,zCore 在地址空间划分方面对 Zircon /Linux的原有设计进行了一定的修改,并为此对 Fuchsia 的源码进行了简单的修改、重新编译;另外,uzCore 需要的硬件相关层(HAL)将完全由宿主 OS 提供支持,一个合理的 HAL 层接口划分也是为支持 uzCore做出的重要考虑。
HAL 层接口设计
HAL 层的设计是在bzCore 和 uzCore 的开发过程中逐渐演进形成的,在开发过程中将硬件实现相关的接口,比如页表、物理内存分配等进行封装,暴露给上层的内核对象层使用。在 kernel-hal 模块中,给出空的弱链接实现,由 bzCore 或 uzCore 的开发者对相应的接口进行相应的实现,并用设定函数链接名称的方式,替换掉预设的弱链接的空函数。在整个开发过程中,不断对 HAL 层提出需求并实现,目前形成了第一版 HAL 层接口,在设计上能够满足现有的内核对象实现所需要的功能。
对内核对象层而言,所依赖的硬件环境不再是真实硬件环境中能够看到的物理内存、CPU、MMU 等,而是 HAL 暴露给上层的一整套接口。这一点从设计上来说,是 zCore 与 Zircon 存在差异的一点。Zircon 将 x86_64 、ARM64 的硬件架构进行底层封装,但是没有给出一套统一的硬件 API 供上层的内核对象直接使用,在部分内核对象的实现中,仍然需要通过宏等手段对代码进行条件编译,从而支持同时面向两套硬件架构进行开发。而在 zCore 的内核对象层实现中,可以完全不考虑底层硬件接口的实现,使一套内核对象的模块代码可以同时在 bzCore和 uzCore 上运行,之后如果 zCore 进一步支持 RISC-V 64 架构(已初步实现),只需要新增一套 HAL的实现,无需修改上层代码。下面将列出目前的uzCore的HAL层,即kernel-hal-unix的接口。
HAL接口名称 功能描述
- 线程相关
- hal_thread_spawn Thread::spawn创建一个新线程并加入调度
- hal_thread_set_tid Thread::set_tid 设定当前线程的 id
- hal_thread_get_tid Thread::get_tid 获取当前线程的 id
- future
- yield_now暂时让出 CPU,回到async runtime中
- sleep_until 休眠直到定时到达
- YieldFuture 放弃执行的future
- SleepFuture 睡眠且等待被唤醒的future
- SerialFuture 通过serial_read获得字符的future
- 上下文切换相关
- VectorRegs x86相关
- hal_context_run context_run 进入“用户态”运行
- 用户指针相关
- UserPtr 对用户指针的操作:读/写/解引用/访问数组/访问字符串
- IoVec 非连续buffer集合(Vec结构):读/写
- 页表相关
- hal_pt_currentPageTable::current 获取当前页表
- hal_pt_newPageTable::new 新建一个页表
- hal_pt_map PageTable::map 将一个物理页帧映射到一个虚拟地址中
- hal_pt_unmap PageTable::unmap 解映射某个虚拟地址
- hal_pt_protect PageTable::protect 修改vaddr对应的页表项的flags
- hal_pt_query PageTable::query 查询某个虚拟地址对应的页表项状态
- hal_pt_table_phys PageTable::table_phys 获取对应页表的根目录表物理地址
- hal_pt_activate PageTable::activate 激活当前页表
- PageTable::map_many 同时映射多个物理页帧到连续虚拟内存空间
- PageTable::map_cont 同时映射连续的多个物理页帧到虚拟内存空间
- hal_pt_unmap_cont PageTable::unmap_cont 解映射某个虚拟地址开始的一片范围
- MMUFlags 页表项的属性位
- 物理页帧相关
- hal_frame_alloc PhysFrame::alloc 分配一个物理页帧
- hal_frame_alloc_contiguous PhysFrame::alloc_contiguous_base 分配一块连续的物理内存
- PhysFrame::addr 返回物理页帧对应的物理地址
- PhysFrame::alloc_contiguous 分配一块连续的物理内存
- PhysFrame::zero_frame_addr 返回零页的物理地址(一个特殊页,内容永远为全0)
- PhysFrame::drop Drop trait 回收该物理页帧
- hal_pmem_read pmem_read 读取某特定物理页帧的内容到缓冲区
- hal_pmem_write pmem_write 将缓冲区中的内容写入某特定物理页帧
- hal_frame_copy frame_copy 复制物理页帧的内容
- hal_frame_zero frame_zero_in_range 物理页帧清零
- hal_frame_flush frame_flush将物理页帧的数据从 Cache 刷回内存
- 基本I/O外设
- hal_serial_read serial_read 字符串输入
- hal_serial_write serial_write 字符串输出
- hal_timer_now timer_now 获取当前时间
- hal_timer_set timer_set 设置一个时钟,当到达deadline时,会调用 callback 函数
- hal_timer_set_next timer_set_next 设置下一个时钟
- hal_timer_tick timer_tick当时钟中断产生时会调用的时钟函数,触发所有已到时间的 callback
- 中断处理
- hal_irq_handle handle 中断处理例程
- hal_ioapic_set_handle set_ioapic_handle x86相关,对高级中断控制器设置处理例程
- hal_irq_add_handle add_handle 对某中断添加中断处理例程
- hal_ioapic_reset_handle reset_ioapic_handle 重置级中断控制器并设置处理例程
- hal_irq_remove_handle remove_handle 移除某中断的中断处理例程
- hal_irq_allocate_block allocate_block 给某中断分配连续区域
- hal_irq_free_block free_block 给某中断释放连续区域
- hal_irq_overwrite_handler overwrite_handler 覆盖某中断的中断处理例程
- hal_irq_enable enable 使能某中断
- hal_irq_disable disable 屏蔽某中断
- hal_irq_maxinstr maxinstr x86相关,获得IOAPIC的maxinstr???
- hal_irq_configure configure 对某中断进行配置???
- hal_irq_isvalid is_valid 查询某中断是否有效
- 硬件平台相关
- hal_vdso_constants vdso_constants 得到平台相关常量参数
- struct VdsoConstants 平台相关常量:
- hal_vdso_constants vdso_constants 得到平台相关常量参数
max_num_cpus features dcache_line_size ticks_per_second ticks_to_mono_numerator ticks_to_mono_denominator physmem version_string_len version_string
* fetch_fault_vaddr fetch_fault_vaddr 取得出错的地址 ???好像缺了hal_*
* fetch_trap_num fetch_trap_num 取得中断号
* hal_pc_firmware_tables pc_firmware_tables x86相关,取得`acpi_rsdp` 和 `smbios` 的物理地址
* hal_acpi_table get_acpi_table 得到acpi table
* hal_outpd outpd x86相关,对IO Port进行写访问
* hal_inpd inpd x86相关,对IO Port进行读访问
* hal_apic_local_id apic_local_id 得到本地(local) APIC ID
* fill_random 产生随机数,并写入到buffer中
在上述“线程相关”的列表中,列出了 HAL 层的部分接口设计,覆盖线程调度方面。在线程调度方面,Thread 结构体相关的接口主要用于将一个线程加入调度等基本操作。在 zCore 的相关实现中,线程调度的各接口使用 naive-executor 给出的接口以及 trapframe 给出的接口来进行实现,二者都是我们为裸机环境的协程调度与上下文切换所封装的 Rust 库。uzCore 中,线程调度的相关接口依赖于 Rust 的用户态协程支持以及 uzCore 开发者实现的用户态上下文切换。
在内存管理方面,HAL 层将内存管理分为页表操作与物理页帧管理两方面,并以此设计接口。在 zCore 实现中,物理页帧的分配与回收由于需要设计物理页帧分配器,且可分配范围大小与内核刚启动时的内存探测密切相关,我们将其直接在总控模块 zCore 中进行实现。而在 uzCore 中,页表对应操作依赖 mmap 进行模拟,物理页帧的相关操作则直接使用用户态物理内存分配器进行模拟。
在 Zircon 的设计中,内存的初始状态应该设置为全 0,为了在内核对象层满足该要求,我们为 HAL 层设计了零页接口,要求 HAL 层保留一个内容为全 0 的物理页帧,供上层使用。上层负责保证该零页内容不被修改。
修改 VDSO
VDSO 是由内核提供、并只读映射到用户态的动态链接库,以函数接口形式提供系统调用接口。原始的 VDSO 中将会最终使用 syscall 指令从用户态进入内核态。但在 uzCore 环境下,内核和用户程序都运行在用户态,因此需要将 syscall 指令修改为函数调用,也就是将 sysall 指令修改为 call 指令。为此我们修改了 VDSO 汇编代码,将其中的 syscall 替换为 call,提供给 uzCore 使用。在 uzCore 内核初始化环节中,向其中填入 call 指令要跳转的目标地址,重定向到内核中处理 syscall 的特定函数,从而实现模拟系统调用的效果。
调整地址空间范围
在 uzCore 中,使用 mmap 来模拟页表,所有进程共用一个 64 位地址空间。因此,从地址空间范围这一角度来说,运行在 uzCore 上的用户程序所在的用户进程地址空间无法像 Zircon 要求的一样大。对于这一点,我们在为每一个用户进程设置地址空间时,手动进行分配,规定每一个用户进程地址空间的大小为 0x100_0000_0000,从 0x2_0000_0000 开始依次排布。0x0 开始至 0x2_0000_0000 规定为 uzCore 内核所在地址空间,不用于 mmap。图 3.3给出了 uzCore 在运行时若干个用户进程的地址空间分布。
与 uzCore 兼容,zCore 对于用户进程的地址空间划分也遵循同样的设计,但在裸机环境下,一定程度上摆脱了限制,能够将不同用户地址空间分隔在不同的页表中。如图 3.4所示,zCore 中将三个用户进程的地址空间在不同的页表中映射,但是为了兼容 uzCore 的运行,每一个用户进程地址空间中用户程序能够真正访问到的部分都仅有 0x100_0000_0000 大小。
LibOS源代码分析记录
zCore on riscv64的LibOS支持
- LibOS unix模式的入口在linux-loader main.rs:main()
初始化包括kernel_hal_unix,Host文件系统,其中载入elf应用程序的过程与zcore bare模式一样;
重点工作应该在kernel_hal_unix中的内核态与用户态互相切换的处理。
kernel_hal_unix初始化主要包括了,构建Segmentation Fault时SIGSEGV信号的处理函数,当代码尝试使用fs寄存器时会触发信号;
- 为什么要注册这个信号处理函数呢?
根据wrj的说明:由于 macOS 用户程序无法修改 fs 寄存器,当运行相关指令时会访问非法内存地址触发Segmentation Fault。故实现段错误信号处理函数,并在其中动态修改用户程序指令,将 fs 改为 gs
kernel_hal_unix还构造了进入用户态所需的run_fncall() -> syscall_fn_return();
而用户程序需要调用syscall_fn_entry()来返回内核态;
Linux-x86_64平台运行时,用户态和内核态之间的切换运用了 fs base 寄存器;
- Linux 和 macOS 下如何分别通过系统调用设置 fsbase / gsbase 。
这个转换过程调用到了trapframe库,x86_64和aarch64有对应实现,而riscv则需要自己手动实现;
- 关于fs寄存器
查找了下,fs寄存器一般会用于寻址TLS,每个线程有它自己的fs base地址;
fs寄存器被glibc定义为存放tls信息,结构体tcbhead_t就是用来描述tls;
进入用户态前,将内核栈指针保存在内核 glibc 的 TLS 区域中。
可参考一个运行时程序的代码转换工具:https://github.com/DynamoRIO/dynamorio/issues/1568#issuecomment-239819506
- LibOS内核态与用户态的切换
Linux x86_64中,fs寄存器是用户态程序无法设置的,只能通过系统调用进行设置;
例如clone系统调用,通过arch_prctl来设置fs寄存器;指向的struct pthread,glibc中,其中的首个结构是tcbhead_t
计算tls结构体偏移:
经过试验,x86_64平台,int型:4节,指针类型:8节,无符号长整型:8节;
riscv64平台,int型: 4节,指针类型:8节,无符号长整型:8节;
计算tls偏移量时,注意下,在musl中,aarch64和riscv64架构有#define TLS_ABOVE_TP,而x86_64无此定义
- 关于Linux user mode (UML)
"No, UML works only on x86 and x86_64."
https://sourceforge.net/p/user-mode-linux/mailman/message/32782012/