20201111-交流 #
内核页表隔离 #
每个进程两个地址空间:用户和内核。用户地址空间只保留少部分内核空间数据,每次系统调用都从用户地址空间切到内核地址空间。防止熔断漏洞。
结论:目前暂时不这样实现
与 IPC 的统一 #
IO 核的运行可看做另一个进程,IO 核处理用户进程的 IO 请求相当于 IPC。这种设计可与传统的 IPC 统一,即使用 ring buffer,在两个用户进程之间进行 IPC。
20201101-交流 #
目前进展:基本完成普通核部分的内核功能,有简单的 C 用户程序和库。(step 3)
分工 #
jyk:完成 IO 核部分的内核功能:创建共享内存、调度 IO 协程、执行 IO。
zyr:制定系统调用接口和共享内存数据格式,用 C 编写用户态 IO 测试程序。对应上文提到的三种实现方式:3 异步 read、4b 同步写法的异步 read、4c 同步写法的异步 read。
lfy:编写 rust 用户库,支持运行 rust 用户程序,编写利用 rust async 创建多个协程的用户程序。然后利用 rust async 将 C 的 IO 测试程序改成 rust。对应上文提到的实现方式:4a 同步写法的异步 read。
20201014-讨论的实现方案 #
利用 io_uring/virtio 的思想,基于共享内存实现的高效异步系统调用接口。
假设系统有两个核,分别为:
- 普通核:类似传统的 OS,有用户态和内核态,负责运行用户进程、调度用户进程、处理普通系统调用。
- IO 核:只有内核态,负责运行内核协程、调度内核协程、处理异步调用。实现了一套类似 io_uring 的机制,内核协程会不断 poll 共享内存中的请求队列,处理完毕后放入完成队列。使用 Rust async 实现。
前期简化 #
有且仅有两个核;没有用户线程;单用户进程;没有抢占;
用户程序的实现 #
- 通过系统调用,创建用户进程
- 通过系统调用,在 IO 核同时创建 io_uring 共享内存,和对应的内核协程(需要 IPI)。
- 异步 read:向请求队列写入 read 操作和所需的参数,立即返回。直到完成队列显示已完成,并得到返回结果。(可能需要通知机制?)
- 同步写法的异步 read(这部分是用户库的内容吗?是):
- 使用 Rust async 创建用户协程,poll 函数里检查对应的 IO 操作是否在完成队列中
- read 后轮询是否完成,用 yield 去别的进程执行一会儿
- read 后 wait 进入睡眠,直到完成时才被唤醒(通知机制?)
请求队列与完成队列的设计 #
TODO
使用共享内存在用户进程与内核协程间共享。
是否要和 io_uring 完全一样?
是否还是两队列形式?更好的设计?
最小原型系统 #
预计实现的普通系统调用:(有用户/内核切换)
- spawn:创建用户进程
- setup_io:创建共享内存和内核协程,返回共享内存地址
- yield:主动放弃当前进程的执行,进行进程调度
- wait:主动放弃当前进程的执行,进行进程调度,直到 IO 操作完成时返回
预计实现的异步调用:(只需写共享内存,无用户/内核切换)
- open
- read
- write
- close
测试环境:
- RISCV64,QEMU/K210
- memory fs/SD 卡
实现步骤 #
方案一:从头开始造
- RISCV64 QEMU 多核启动
- 普通核实现内核的基本功能(内存管理、进程管理、memory FS)
- 支持运行一个 hello_world 用户进程
- IO 核实现共享内存创建
- 内核协程创建与调度
- 实现 open/close/read/write
- 完善系统调用
- 编写性能测试程序,真机测试
方案二:将 rCore_Tutorial 改造成这种架构
其他问题 #
IO 核如何访问不同用户进程的IO数据?好像还是要切页表?
(第一版无需考虑)
完成时如何通知用户程序?
是否可在普通核开中断?
是否可以支持乱序完成?(似乎 io_uring 就是乱序的,可以通过参数强制顺序执行)
#
用户线程与地址空间相关的问题 #
普通核 #
内核态有一个全局调度器,负责调度和执行用户线程(Thread)。每个线程都有独立的地址空间(可以多个线程共享同一地址空间),通过系统调用进入内核态时不发生地址空间切换,只有调度器进行线程切换时才进行地址空间切换。因此线程地址空间需要同时映射内核与用户的代码与数据。
注:目前的实现线程==进程,是最基本的调度单位。以后可通过将共享地址空间的线程组成一个“线程组”,来表示进程的概念。在具体实现中,用户线程也被封装为了一个 rust future,通过 executor 进行调度。
IO 核 #
只有一个地址空间,即内核地址空间,只映射内核的代码和数据,且不会在之后进行切换。也运行一个全局调度器,复制调度和执行用于 IO 的协程。当用户程序创建 IO 共享内存时,会同时在 IO 核的内核地址空间,和普通核对应的用户线程地址空间的用户数据段增加映射。
关键问题 #
IO 核如何访问不同用户进程的IO buffer而不用频繁切页表?
目前想到的解决方法:(这几种方法可以一起用:共享内存放在特定区域,以优化地址转换;加一个描述缓冲区的数据结构,提高灵活性)
- 创建专门的共享内存,存放 IO buffer。缺点:IO buffer 需要放在特定的地方,降低了灵活性,增加编程复杂性。
- 内核要访问用户数据时,手动进行用户虚拟地址到物理地址的转换,然后直接访问物理地址获取数据。缺点:降低安全性?对于不连续的页面每隔 4K 需要一次地址转换,开销大。适合 IO buffer 小到几个页面范围内的情况。
- 对方案 2 的优化:将 IO buffer 映射连续的物理地址,只需进行一次地址转换;使用大页,每隔 2M 进行一次地址转换;将用户虚拟地址到物理地址的映射结果缓存到专门的区域,以减少每次地址转换的开销(相当于4级页表的4次访存变为1次)。