S081 论文拾遗
虚拟机、RCU、微内核、meltdown
微内核 #
摘要 #
The Performance of μ-Kernel-Based Systems 原文发布于 1997 年
本文介绍 L4 这个微内核 kernel,并验证 L4 是否克服了初代微内核的限制
微内核只提供最基本的 内存、线程与进程间通信 功能,其余功能通过接入模块实现
L4 是第二代微内核
背景 #
九十年代,pure-微内核架构性能过差,逐渐式微
人们对微内核做出了两种尝试:
- 向下细化抽象,建立类似硬件架构的接口
- 向上提升抽象,为拓展 kernel 提供更好的接口
但那不是本文的工作,本文还是专注于 pure 微内核架构,介绍 L4 kernel,从三个方面进行性能评估:
- L4 上运行 Linux,评估性能
- L4 上实现管道扩展
- 将 L4 移植到不同的硬件平台
Design #
L4 只关注 线程 与 用户空间 两个概念,线程间通信引出 IPC 设计
L4 对上层 OS 的服务基于一个想法:对地址进行递归映射,为 guest OS 提供资源使用权
这里需要阐明一个事实,L4 作为宿主,其上层的 OS server 应该运行在 user-level
这些 server 调用 L4 提供的 grant,map,unmap 接口,维护用户进程的内存映射,因此又称为 pager
L4 Linux #
我们将 Linux 作为一个用户态进程,运行在 L4 上
运行在 Linux 上的应用不知道自己在 L4 上,因此我们需要重写系统调用的库,将其重定向到 L4 层
IPC #
原先的 Linux 宏内核可以直接与硬件交互,但是在这里,模块之间只能通过 L4 提供的 IPC 进行通信服务
这么做提高了不同功能间的隔离性,但是 频繁的 IPC 通信显然会影响性能
要进行 IPC 通信,需要有两个线程分别调用 send 与 recv
如果是异步通信,sender 请求时会陷入 L4 kernel,reciever 回复时又会陷入,一来一回增加了许多开销
因此 L4 采用同步通信
但这样还是不够,IPC 基于 page 映射提供的空间共享服务,以此实现通信,同时还要在 kernel 间反复 trap,如果我们针对这种情况做出特殊优化呢?
第一,我们可以减少 L4 内核态的切换,不要 trap 后,回到用户态再等待回复,而是在内核态直接切换上下文,让出 CPU,收到回复后再返回 user-level
第二,我们为 message 与 reply 指定空间映射,通过 L4 pager 提供的服务,进行 IPC 通信,这样一来就不需要在两个不同的 user address space 间进行 copy
这种设计其实就是 RPC 的想法
硬件交互 #
原先,Linux 直接与设备打交道,捕获信号、异常,并trap到内核线程
在 L4 就不可以了,因为现在中断信号与 Linux 分离开
L4 采用的做法是,将这些信号转换为 IPC 的 message,设置若干个 signal thread,循环等待 message,提醒 linux kernel 进行处理
调度功能可以直接调用 L4 的接口
一个值得注意的设计失误是,起初 Linux kernel 与其他进程的地位是平等的,导致 L4 切换时不能区分开,会频繁刷新 TLB
解决方案是将 Linux kernel 映射到共享内存中,这样就避免频繁切换页表了
虚拟机 #
Dune: Safe User-level Access to Privileged CPU Features
虚拟机的想法是,让不同的操作系统运行在同一台物理机器上,由 VMM 作为中间层管理资源
比较直观的做法是,由软件对硬件寄存器进行模拟,但这么做增加大量额外的系统调用,性能极差
最后流行的选择,还是让 OS 代码直接运行在 CPU 上,但是 VMM 会向上层 OS 屏蔽底层细节
VMM 的核心理念可以概括为 Trap-Emulate,检查到 OS 想要与逻辑上的”底层硬件“交互时,就触发 trap 到 VMM,由 VMM 掩盖一切,模拟交互结果后,将期望的结果返回给 OS
- VMM 为 OS 提供逻辑上的双层页表,OS 的物理地址其实是 VMM 为其分配的
- VMM 也会为 OS 提供设备模拟,这样 OS 还是以为自己在与真实设备交互
人们普遍认可虚拟机的流行后,分别从 OS 与 硬件 上对其进行针对设计
- OS 从设计层面意识到虚拟机的存在,这样 VMM 与 OS 直接便可以达成协议共识,比如二者可以针对一个约定好的虚拟设备接口开发驱动
- 硬件为 VMM 提供优化,为每个 OS 加入独立的寄存器空间,并且为双层页表提供物理支持
这样一来,hardware-OS 的两层结构实际变成了三层:HW-VMM-OS,原先的用户-内核态设计,也变成了三层权限
- 第一层是直接操作物理硬件的权限
- 而第二层则是对运行环境 env 的隔离,不同的 env 之间不会相互干扰
- 最后一层才是 user-level,运行用户程序
其中,隔离 env 的理念具有普适性,而优化后的 VT-x 硬件又天然支持三层架构
为何不利用这种硬件,在原生 OS 上实现 env 层隔离呢?这就是 Dune 的设计动机
Dune #
借助内核态的设计,我们可以提供大量的安全性服务,然而这会增加开发的复杂度,此外,借助虚拟机来实现沙箱隔离的做法也是可行的,但毕竟需要原生的 OS 去做适配,相性不好
因此我们产生 Dune 的想法,利用硬件提供的三种特权模式,将这种隔离功能与原生的进程模型结合起来,兼备性能与安全性
Dune 模式提供的服务:
- 访问 EPT 提供的安全隔离内存
- 调用原生 OS 的接口
- 访问特权模式保护的硬件
相比传统的 VMM,Dune更加明确自己的工作场景,更加轻量化,硬件又提供了直接访问特权硬件的方式,还加速了中断传递与地址翻译,这样一来,Dune 进程大大减少了 Trap 到内核的次数,性能自然得到提升
而 not-root 模式本身就不被硬件信任,Dune 的安全性也得到保障
Dune 被实现为 加载 module 的形式,使用自己的 dunelib 库做接口,转换与下层抽象的交互,因此可以与常规的进程并存
采用 Dune 模式,我们有以下应用:
- sandbox,用于运行风险程序
- 特权分离,在 Dune env 下对不同线程进行细化权限配置,加强控制性
- GC,利用硬件对 Trap 次数的优化,加速常规扫描与访存
Meltdown #
meltdown 漏洞从用户态代码,获取任意一块物理 bit 上的内容
- 构造想要读取的 物理地址,诱骗 CPU 在分支预测时读取该地址的内容
一般的内核,会将 用户与内核态内存 映射到同一个页表,减少切换成本,只是通过设置权限位 PTE_U 来隔离访问
Intel CPU 在预测时不会检查该权限,因此会直接读取,在分支预测失败后,CPU 意识到不该执行这条指令,于是回退状态
但是读取的信息已经存放在 Cache 里了
- 我们只需要重新读出两个分支的数据,比较 CPU 时间,更短的那个说明就在 cache 里
借助该方式,我们通过 CPU 时间推理出了想要攻击的信息
解决方案:
- 隔离 user 与 kernel 页表映射
- 优化 CPU 分支预测,检查权限,或者干脆取消映射
我觉得还可以从 cache 入手,但是读入读出都会存在 side information,根源应该是不能暴露任何分支预测的执行信息,仅依靠 cache 还是不可以
RCU #
面对并发的读写场景,我们需要用锁进行控制,然而大量的读操作并不互斥,因此引出读写锁 rw_lock 的设计
然而,既然有锁,就必然会有 CPU 空转,假设 n 个 CPU 同时进行并发 read,对于同一个读锁,我们需要维护 ref_count 引用计数,这同样是一个原子场景,产生互斥,这样浪费的 CPU 周期是 $n^2$ 级别的
能否实现无锁的读写结构呢?这就是 RCU 的想法
RCU 有限制,必须不强制原地写入
RCU 的做法是,在写入时,拷贝一个临时的副本,在副本上做写入,完成时,原子的替换指针,使得后续的读操作都会连接到这个副本上
而之前的读操作依旧会读取旧的快照,不会读到写入一半的数据
当写入结束时,需要等待一段时间,将快照从 cache 中驱逐,便可安全释放内存
rust kernel #
传统的 OS 利用页表与特权机制,实现空间隔离,这需要硬件作为支持
而旧的 C 语言实现,存在大量安全隐患,产生从语言层面解决问题的需求,因此目前有大量的项目选择用高级语言重写 OS
而本文选择利用 Rust 的现代语言特性(所有权、移动语义),提供一种新的抽象体系,仅从软件层次实现 OS 的隔离性,这就是 redleaf 的设计动机
redleaf 提供的抽象:
- 将不同空间抽象为 domain,在 domain 间做访问限制
- 对每个 domain 建立隔离的 heap,并在全局提供一个共享 heap
- 中间代理,为跨 domain 的调用提供接口封装
将 domain 与所有权模型结合,我们就实现了零开销的拷贝操作
通过 domain 抽象,我们完成在设计层面上的隔离空间,为了贯彻这种隔离的理念,redleaf 实现为一个微内核,并在其之上实现 rv6 OS 进行性能评估
这里需要解释 fault 隔离 的含义:当一个模块发生故障时,只需要清理它所在的 module 而不影响其他部分的运行,反例是宏内核,一旦出错就需要重启整个系统
redleaf 采用两种自定义概念:
- RRef 类型:同时包含元数据与数据本身,在 domain 间进行所有权转移
- proxy 接口:对接口进行严格的 trait 定义,采用静态语言进行正确性检查