S081 论文拾遗

虚拟机、RCU、微内核、meltdown

微内核 #

摘要 #

The Performance of μ-Kernel-Based Systems 原文发布于 1997 年

本文介绍 L4 这个微内核 kernel,并验证 L4 是否克服了初代微内核的限制

微内核只提供最基本的 内存、线程与进程间通信 功能,其余功能通过接入模块实现

L4 是第二代微内核

背景 #

九十年代,pure-微内核架构性能过差,逐渐式微

人们对微内核做出了两种尝试:

  1. 向下细化抽象,建立类似硬件架构的接口
  2. 向上提升抽象,为拓展 kernel 提供更好的接口

但那不是本文的工作,本文还是专注于 pure 微内核架构,介绍 L4 kernel,从三个方面进行性能评估:

  1. L4 上运行 Linux,评估性能
  2. L4 上实现管道扩展
  3. 将 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 通信,需要有两个线程分别调用 sendrecv

如果是异步通信,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 模式提供的服务:

  1. 访问 EPT 提供的安全隔离内存
  2. 调用原生 OS 的接口
  3. 访问特权模式保护的硬件

相比传统的 VMM,Dune更加明确自己的工作场景,更加轻量化,硬件又提供了直接访问特权硬件的方式,还加速了中断传递与地址翻译,这样一来,Dune 进程大大减少了 Trap 到内核的次数,性能自然得到提升

而 not-root 模式本身就不被硬件信任,Dune 的安全性也得到保障

Dune 被实现为 加载 module 的形式,使用自己的 dunelib 库做接口,转换与下层抽象的交互,因此可以与常规的进程并存

采用 Dune 模式,我们有以下应用:

  1. sandbox,用于运行风险程序
  2. 特权分离,在 Dune env 下对不同线程进行细化权限配置,加强控制性
  3. GC,利用硬件对 Trap 次数的优化,加速常规扫描与访存

Meltdown #

meltdown 漏洞从用户态代码,获取任意一块物理 bit 上的内容

  • 构造想要读取的 物理地址,诱骗 CPU 在分支预测时读取该地址的内容

一般的内核,会将 用户与内核态内存 映射到同一个页表,减少切换成本,只是通过设置权限位 PTE_U 来隔离访问

Intel CPU 在预测时不会检查该权限,因此会直接读取,在分支预测失败后,CPU 意识到不该执行这条指令,于是回退状态

但是读取的信息已经存放在 Cache 里了

  • 我们只需要重新读出两个分支的数据,比较 CPU 时间,更短的那个说明就在 cache 里

借助该方式,我们通过 CPU 时间推理出了想要攻击的信息

解决方案:

  1. 隔离 user 与 kernel 页表映射
  2. 优化 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 提供的抽象:

  1. 将不同空间抽象为 domain,在 domain 间做访问限制
  2. 对每个 domain 建立隔离的 heap,并在全局提供一个共享 heap
  3. 中间代理,为跨 domain 的调用提供接口封装

将 domain 与所有权模型结合,我们就实现了零开销的拷贝操作

通过 domain 抽象,我们完成在设计层面上的隔离空间,为了贯彻这种隔离的理念,redleaf 实现为一个微内核,并在其之上实现 rv6 OS 进行性能评估

这里需要解释 fault 隔离 的含义:当一个模块发生故障时,只需要清理它所在的 module 而不影响其他部分的运行,反例是宏内核,一旦出错就需要重启整个系统

redleaf 采用两种自定义概念:

  1. RRef 类型:同时包含元数据与数据本身,在 domain 间进行所有权转移
  2. proxy 接口:对接口进行严格的 trait 定义,采用静态语言进行正确性检查