这是一篇 2019 年的关于微内核 IPC 性能优化的文章。

摘要

微内核有很多引人注目的 features,例如 安全性、容错性、模块化,以及可定制性,这些特性近期在学术界和工业界又再次掀起了一股研究热潮(including seL4, QNX and Google’s Fuchsia OS)。

Google’s Fuchsia’s kernel (called Zircon)

但是 IPC(进程间通信)作为微内核的 阿喀琉斯之踵,仍然是导致微内核 OS 总体性能较低的主要因素之一。同时 IPC 在宏内核中也扮演者很重要的角色,例如 Android Linux,其中的移动端程序会经常和用户态服务通过 IPC 通信。所以优化 IPC 自然是一个很重要的课题。

之前学界对 IPC 在软件层面的优化都绕不开 Kernel,因为 IPC 在这方面的主要开销就是 域切换(domain switch)和消息复制/重映射(message copying/remapping);在硬件层面的优化方法主要是 给内存和能力打 tag,为了隔离而替换页表。但这类修改通常需要对现有的软件栈进行相当多的修改来适应新的 OS 原语。

这篇文章主要是提出一种硬件协同的操作系统原语——也就是 XPC(Cross Process Call),提供一种高速安全的 IPC 的实现。

XPC 的主要特征是:

  • 允许在 XPC caller 和 callee 间直接切换,中途不需要陷入 Kernel;
  • 允许消息在不同进程的调用链中传递,无需 copy;

XPC 还有个优点是,这个原语本身是与传统的 “基于地址的隔离机制” 相兼容的,能比较轻松地整合到现有的微内核或者宏内核中。

所以笔者就就介绍了基于 Rocket RISC-V 核心、搭载 RPGA 的板子上实现了 XPC 的原型,并且移植到两个主流的微内核系统 seL4 和 Ziron 上、一个宏内核的 Android Binder 上以便进行评估测试、GEM5 模拟器上来验证通用性。

结果发现 XPC 能显著提升 IPC 调用的性能,Android Binder 上提升 54.2 倍,对于在微内核上跑的真实应用(sqlite/ HTTP Server)也有相当大的速度提升。

介绍

1. 问题分析

  • (微内核特征)微内核已经被详尽地研究多年了:

    • 最小化在特权级中执行的功能,把一些 paging、fs、设备驱动之类的组件放到用户态中,这样能实现 细粒度的隔离、更高的可扩展性、安全和容错性;

      这种基于微内核的操作系统已经被广泛应用到诸如移动基站、飞行器和交通工具中;

  • (微内核问题)但当前设计基于微内核的 OS 仍然面临安全和性能之间权衡的问题:

    • 更细粒度的隔离会提升系统的安全性和容错性,但是会导致 IPC 次数增多;
  • (宏内核也面临同样的问题)对于宏内核的 OS 也存在 IPC 性能不佳的问题,针对这个问题,Android 向 Linux Kernel 中引入了 Binder 以及 Anonymous Shared Memory 来缓解这个问题,但这个开销仍然很大。

    像 Android 基于宏内核的 Linux 构建,会给移动应用程序提供一些用户态的服务,然后应用程序会频繁地调用这些服务,像是利用窗口管理器在屏幕上绘制组件,等等,开销比较大。

可以分析出来,IPC 的主要两个性能瓶颈在于 domain switch 以及 message copy:

  • 因为 caller 和 callee 分别位于用户态的两个进程,二者要发生调用的话就需要通过中断进入内核,然后切换内存地址空间,包括 Context saving/restoring、功能和权限的检查以及其他的 IPC 逻辑,一来一回的开销就不小。

  • 此外,两个进程间因为存在虚拟内存的隔离机制,两者间传递消息就需要一些特殊手段,常见的方法是类似于 mmap 一样,分配一个共享内存,在这段共享内存中放置消息就能实现近乎 0 copy 的消息传递。

    但需要注意的是,如果 caller 和 callee 同时持有这块共享内存的话,可能会引起 TOCTTOU(Time Of Check To Time Of Use)攻击。

    TOCTTOU Details

    这个问题可以通过重新映射页表的权限信息(所有权转移)来缓解,但更改页表又会涉及 Kernel 的操作,不仅无法避免用户态和内核态的切换,而且会间接造成 TLB 击穿;

总结下来就是,在之前众多软件解决方案中,进程切换陷入内核的开销无法避免,而且消息传递要么需要多次 copy,要么会导致 TLB 击穿;在之前的硬件解决方案中,14 年的论文提出 CODOMs(Code-centric Memory Domains)的系统架构,充分利用标记内存,而非更改页表的方式达到隔离效果,起到减少 domain switch 次数、提升消息传递效率的效果。

但这样的一个新的硬件层面的解决方案可能需要现有的使用多套内存地址的 kernel 的实现进行比较大的变动。

所以这篇文章就想提出一种新型的硬件协同的 OS 原语 XPC,实现更高效更安全的 IPC 调用,完成以下目标:

  • (减小开销 1)让 IPC 在 caller 和 callee 间直接切换,不需要频繁陷入 kernel;
  • (减小开销 2)以更安全的 0 copy 的方式在 caller 和 callee 间传递数据;
  • (软件改动小)更轻松的集成到现有的 kernel 中,而无需很大的改动;
  • (硬件改动小)最小的硬件改动;

2. 构思方案

本文要实现的原语主要分为 3 个部分:

  • 创建硬件层面可感知的抽象 x-entry,定义新能力 xcall-capx-entry 中包含一个 ID,xcall-cap 是一个用于访问控制的新能力;

    这个能力由 Kernel 管理(flexibility),并交由硬件来校验(efficiency);

  • 创建一组新指令,包括 xcallxret,来让用户态的代码直接 switch 到另一个进程,而无需内核的参与;

  • 创建一套新的内存地址空间映射机制,称为 relay-seg(relay memory segment,中继内存段),用来为 caller 和 callee 间做 0-copy 消息传递提供空间;

    这个映射关系将存放在一个新的寄存器中,用来标识存放消息所对应的虚拟内存和物理内存的基地址以及范围;

    • 这套机制能够控制共享内存的所有权转移,不仅规避了 domain switch 及其引起的 TLB 击穿带来的性能问题,而且缓解了 TOCTTOU 风险。
    • 与此同时,relay-seg 本身也可以通过调用链传递,称为 handover,也能进一步降低 copy 的次数,提升性能;

    三点好处:0-copy、page-free 的所有权转移、实现 handover;

此外,XPC 会选用同步的 IPC 实现方式。尽管异步的 IPC 有更高的吞吐量,但同步 IPC 可以达到更低的时延,并且在 POSIX API 下更容易实现它的语义。

即便 Google 的 Zircon 用的是异步 IPC,但它也是用异步 IPC 模拟文件系统接口的同步语义。这就造成了每次 IPC 比较大的时延。

不仅如此,XPC 克服了传统同步 IPC 的两个限制:

  • 相对低下的数据传输效率(XPC 用 relay-seg 解决);

  • 对多线程应用不友好的编程模型(XPC 提供 migrating thread model 模式的编程接口,1994 Bryan Ford);

    参见 Notes on Thread Models in Mach 3.0

XPC 的移植情况:

  • Rocket RISC-V core on FPGA board(for evaluation);
  • 2 Micro-kernel Implementation(seL4 & Zircon);
  • 1 Monolithic kernel Implementation(Android Binder);

Measured: micro-benchmark & real-world applications;

3. 论文贡献

总的来说,这篇文章作出了如下的成果:

  • 详细分析了 IPC 的性能开销,并且对比了当今常见的几种优化方式;
  • 提出了一种新的 OS 原语,不借助 kernel trapping,沿着调用链的 0-copy 消息传递;
  • Implementation……
  • An evaluation on micro-benchmark & real-world platform;

动机

  • IPC 的性能仍然很重要:

    • 使用前沿 microkernel(seL4)分析性能,在运行 sqlite3 的 seL4 的 RISC-V 板子上测试 YCSB benchmark,发现在这个 workload 下有 18%-39% 的时间都在 IPC;
    • 在消息量很短的情况下,性能瓶颈位于 domain switch;随着 IPC 消息量增大,性能瓶颈趋向于 message transfer;

    这个性能测试激励笔者设计 XPC 时兼顾快速的 domain switch 和高效的消息传递两个因素;

  • 拆开 IPC,详细分析其中各个步骤的性能瓶颈,同样使用 mirco-kernel seL4 & RISC-V board with FPGA;

    • fast path(聚焦):自然是不存在其他中断和调度的情况,只有 trap(syscall)、IPC 逻辑、Process Switch、Restore、Message Transfer;

      • Trap & Restore:

        • 过程:syscall -> context switch -> […] -> restore callee’s context -> callee user space(domain switch 的两大重要开销);

        一般情况下,这个 domain switch 的开销不可避免(caller & callee 互不信任);

        设想:但是某些情况下 caller 和 callee 可以自定义 calling conventions,让它们自己管理 context 就能在 isolation 和 performance 间找到最佳平衡;

      • IPC Logic: CHECKING

        seL4 使用 capabilities 管理 kernel 资源,包括 IPC:

        kernel 找到 caller 的 capability & 检查有效性

        -> 然后检查以下条件,来确定是否发生调度:

        • caller / callee 有不同优先级?
        • caller / callee 不在一个核上?
        • message 大小大于寄存器(32 bytes)但小于 IPC buffer size(120 bytes);

        以上满足一项,就调度到其他进程(slow path)

        设想:这些检测步骤可以由硬件完成,通过并行化来缓解时延问题。此外,这步启发我们将这个 checking 逻辑分为 control plane(软件 -> flexibility)和 data plane(硬件 -> efficiency),回想 xcall-cap 能力

      • Process Switch:

        • 4 步过程:dequeue callee & block caller -> add reply_cap into callee thread -> kernel 传送小于一个寄存器大小的消息 -> switch to callee’s user space address;
        • 存在内存访问,cache miss / TLB miss 会影响本部分性能;
      • Message Transfer:

        • 一个寄存器大小以内的,上面 process switch 时传递(short path);
        • 大于一个寄存器大小、小于 IPC buffer size 的,进入 slow path(interrupt & schedule)来传输数据;
        • 大于 IPC buffer size 的,seL4 会使用位于用户态的 shared memory 来减少 copy;

        但是最后一种情况使用 shared memory 确实不安全:多线程的 caller 可以观察 callee 的操作,并且能通过修改共享内存信息,从而影响 callee 行为;并且在大部分实现中,数据还是要先 copy 到共享内存中去。所以大数据量下 message transfer 仍然是开销大头;

    • slow path:会考虑到现实情况下,可能存在 OS 的其他中断以及进程调度;

基于上述分析,观察到两点:

  1. 一个快速的 IPC 需要不经过 Kernel 的参与,但现在还没实现;
  2. 在传输消息时,安全、快速的 0-copy 机制对于 IPC 性能是很重要的,尤其是大数据量的情况;

根据这两点,笔者设计了 XPC 的总体框架:

设计

总体概览

XPC Engine 提供 x-entry & xcall-cap 两个抽象。规则如下:

  • 一个进程可以创建多个 x-entries;所有的 entries 都会存放在 x-entry-table 中,并且每个 entry 有一个 ID(就是 table index);
  • table 本身存放在全局的 memory 中,有一个寄存器 x-entry-table-reg 存放表基地址,x-entry-table-size 控制表长;
  • 一个 caller 需要 xcall-cap 这个能力来 invoke 一个 x-entryxcall-cap 描述了对每个 x-entry 能否 invoke 的能力;
  • xcall #regxret 控制进行 XPC 调用的两条指令,#reg 是 kernel 提供的具体 x-entry 的 index,对应 Kernel 管理的 xcall-cap-reg 内的能力(里面可以 check);

XPC 进行轻量的信息 copy 的 relay-seg 规则如下:

  • relay memory segment 位于一段连续的物理内存上,不由页表管理,只有 seg-reg 寄存器承担 VM 翻译工作(里面存放虚拟内存和物理内存基地址,以及区域长度和权限信息,相当于全能版 PTE);
  • OS Kernel 会确保这片区域不会和分配的普通页表 overlap;

这样修改区域权限的时候就能绕开页表,避免 TLB 击穿;

XPC Engine

执行 xcall #reg 时,XPC Engine 完成以下任务:

  1. 读取 #reg index,按 xcall-cap-reg(可以是 general purpose register)指向的存放 bitmap 的内存区域校验 caller 的能力(细节见下文);

  2. x-entry-table 读取对应的 x-entry,检查这个 entry 是否有效;

  3. 向 link stack 表(kernel space)中推入一条记录,保存返回时的必要信息。其中 link stack 在每个线程中都有;

    类似普通 call / ret 指令需要操作 %rsp 来在栈上保存 return address、类似 system call 会用 %rcx / %r11 保存 rflags 和 $I_{next}$,这里新的 xcall / xret 就需要一个返回时必要信息,这里称 linkage record;

  4. CPU 切换 Page Table(CR3),flush TLB,设置 PC 为 callee 进程的 procedure 入口地址。最后还会把 caller 的 xcall-cap-reg 放到寄存器中(RISC-V 中用的是 t0),让 callee 知道关于 caller 的信息;

    这相当于 “精简版” 的 context switch。但这样会有问题吗?细节见下文;

虽然没有真正涉及到 kernel(domain switch),但如果出现 exception 还是会 report 给 kernel 来决定;

执行 xret 时,XPC Engine 通过弹出 link stack 中的一条记录,检验 valid 并完成一次简单的 switch 来回到 caller 所在进程;

那么 xcall-cap 是如何管理、xcall #reg 指令是如何校验 caller 的能力的呢?原来:

  • 每个线程就有一块内存空间,起始地址存放在 xcall-cap-reg 中,也是由 Kernel 管理;这块内存空间就是一片 bitmap,第 i bit 是 1 就表示该线程有能力 invoke ID 为 i(也就是在 xcall-table 中 index=i)的 x-entry

就和之前说的一样,这里由 Kernel 管理,由硬件校验;

还有一点是 link stack,虽然说 xcall 完成了一次简单的 context switch,但毕竟只改了 Page Table、TLB、PC,还有进程很多其他信息像文件描述符表、CPU 寄存器等信息没有恢复,也没保存在 link stack 中。

有些 per-thread 的 寄存器(例如 xcall-cap-reg)会在 xcall 的时候交给硬件修改;

这是因为 XPC engine 想把这部分恢复工作交给 XPC 库/应用来放到 user space 来做。对不同架构,如果有些信息在 user space 恢复不了,还可以扩展这个表,让它满足其他架构的 switch 需求。这就是前面提到的 caller 调用 callee 中 “共同约定管理 context 能优化掉 Kernel 参与 switch 的过程”。

这里还有一个严重的问题。在 xcall 从 caller 向 callee 切换的时候,可能在期间发生一次 page fault 中断进入 Kernel,因为 Kernel 对这个过程无感知,所以可能会用 caller 的 page table / VMA 等信息来处理 callee 的 page fault。解决方案借鉴了 migrating thread,将由 kernel 管理的线程状态划分为两类:

  • scheduling states:关于内核调度的信息,包括 kernel stack、优先级、time slice;
  • runtime states:内核用来服务这个线程的信息,例如地址空间(VM 分配)、capabilities 等等;

这里可以让 xcall-cap-reg 作为一个 runtime states 让 kernel 区分;当 trap 到 kernel 时,让 kernel 通过 xcall-cap-reg 找到当前线程的 runtime-states,从而解决问题;

笔者还提到 xcall 向 link stack(每个线程中,位于主存)写数据时有时延,可以采用非阻塞式地异步写,可以优化 16 cycles;

笔者表示,除了写 link stack 会有较高的延迟,需要优化以外,第二步从 x-entry-table 中取 entry 也耗时所以单独给这个表做缓存。不过基于两个事实:

  • IPC 的时间局部性很强;
  • IPC 是可预测的;

笔者提出使用软件层面的 cache,预测要进行的 IPC 并把需要的 entry 先 prefetch 一下,又能提升 12 cycles;

关于 Relay Memory Segment

  • seg-reg 的地位:我们知道,relay memory segment 已经脱离 page table 的管理,相当于用 seg-reg 做了一个独立、持久的 TLB;不过在地址翻译过程中,seg-reg 的优先级高于 TLB;

  • seg-mask 寄存器规定的新功能:目前 seg-reg 指定了 relay memory segment 的全部范围,但实际上在某些场景下,我们只希望传递其中一片区域的信息,但 user space 的 app 是不能改 seg-reg 寄存器的,所以可以使用 seg-mask 来规定范围:

    这样在 xcall 填入 link stack 时,把原来的 seg-regseg-mask 信息保存下,再把 seg-regseg-mask 的交集更新到 seg-reg 中。这样后面的 calling chain 就只能看到这片内存;

  • seg-list 支持服务端创建多个 relay memory segment 区域:每个进程管理一个 seg-list,其基地址使用 seg-list-reg 存放(seg-listseg-list-reg 都由 OS Kernel 管理);执行 swapseg #reg 即可切换到 #reg 索引的 entry 中(atomic),更新新的 seg-reg

回顾一下之前遇到的问题,relay memory segment 如何更改区域的 ownership?

实际上,OS Kernel 会确保每片 relay memory segment 只会被一个 CPU core 访问(也就是说同一时间只会有一个线程拥有访问这个区域的能力),并且所有权会随着 calling chain 传递,解决 ownership 转移、减小 TOCTTOU 风险的同时,避免直接修改页表导致 TLB 击穿。

还有在 xret 时重要的安全逻辑:返回前检查 callee 本身的 seg-reg 和调用时的状态是一致的(通过检查 link stack 上的 seg-reg & seg-mask 与当前 seg-reg 指定范围是否匹配),如果不一致,则说明有恶意的 callee 把不恢复原先的 seg-reg,而是 swap 到 seg-list 中。如果 check 失败,则会向 Kernel 抛出异常。

总结一下每个表现在所处的位置:

  • x-entry-table:全局内存(Kernel space);
  • xcall-cap-bitmap:per-thread memory,被 Kernel 管理,由 xcall-cap-reg 指向;
  • link-stack:per-thread memory,只被 Kernel 访问,由 link-reg 指向;
  • relay-segment-list:per-process memory,被 Kernel 管理,由 seg-list-reg 指向;
  • relay memory segment: continuous physical memory,可被服务端创建多个,由 seg-reg / seg-list-entry 指向;
  • seg-reg 寄存器:同时有虚拟地址和物理地址,随着 calling chain 传递,每个线程自己可以通过修改 seg-mask 来改变传给 callee 的内存范围;

前 4 个就是 Kernel 帮忙管理,后一个需要 Kernel 帮忙 check no overlap;

中途在出现 context switch 时,会 save/restore per-thread objects;

思考:各个寄存器放的是物理地址还是虚拟地址?

实现

RocketChip (Microkernel)

  • XPC Engine -> a unit of a RocketChip core;
  • 新寄存器 -> CSRs(Control & Status Registers),通过 csrr / csrw 指令访问;
  • 新指令 xcall / xret / swapseg 会在 Execution Stage 向 XPC Engine 发送(不涉及内核);
  • 新增 5 类 exceptions;
  • 默认实现不含 x-entry-table 的 cache,为了最小化硬件改动;如果改动,则使用 1 entry 作为缓存,使用软件管理,在 prefetch 时调用 xcall -#reg

权限转移(Capability):XPC 这套架构提供一套 “赋予能力” 的功能,这个就是 grant-cap;这个功能也由 Kernel 管理。一个线程创建 x-entry 时就拥有对这个 x-entryxcall-cap 以及赋予这个能力的 grant-cap,现在这个线程可以给指定线程赋予 xcall-cap 的能力;

单次调用 C-Stack: XPC 支持一个 server 的 x-entry 同时被多个 clients invoke;XPC library 会提供一种 per-invocation XPC context,其中包括了一个 execution stack 和 local data,用来支持多个 clients 同时进行跨进程调用;

每创建一个 x-entry 就能针对这个 entry 设定最大的 context 数,XPC library 会提前创建这些 context 并向 x-entry 绑定一个 “跳板”;当 clients invoke 这个 x-entry 后,XPC library 就按此跳板恢复 C-stack 以及 local data,并在 return 前释放资源;如果没有空闲的 context,则抛出错误或者等待空闲的 context 出现;

也可能出现 Dos 攻击,假设一个恶意的 client 频繁 invoke 某个 x-entry,耗尽了这个 entry 的 context。

对于这个问题,XPC 允许 server 指定策略限制 clients 访问 entry,又或者引入信用系统;

调用链的异常终止与断开策略:假设 xcall 的调用链 A -> B -> C 中,B 因为某些原因被 Kernel kill 了;如果此时 C xret 了,则可能返回到错误的进程。为了解决这种情况,在调用链中途的进程被 kill 后需要引发一个 exception 让 Kernel 处理。

解决方法是,在一个进程结束后,让 Kernel 扫描一遍 link stack,找到死去的进程并将 valid 位置为 0,这样在 C xret 时校验 valid 会发现问题并抛出 exception,Kernel 就可以将无效的 stack entry pop 出去,程序控制流能够返回 A 进程中;

另一种减少 scan table 的方式是,在 B 死去后,直接将 top level page table 清零,这样在 C xret 时会触发 page fault,Kernel 会介入并回收 B 的资源(为什么?参见 “seg-reg handover” 的讨论);

Android Binder

Android Binder: driver + framework (C++ middleware) + API (Android interface definition language)

  • binder transaction -> cross-process method invoke;

  • twofold-copy & transaction buffer -> transfer data;

    anonymous shared memory (ashmem) -> bulk memory transfer;

其中,原先的 binder transaction 过程如下:

  1. client 准备 method code(代表 remote method),在调用时携带集中好的数据(Android 中的 parcels);
  2. client binder 对象调用 transact(),通过 AB driver 复制 user space 中的 transaction buffer 到 kernel 中(copy_from_user),再切到 server side 从 kernel 中复制出来(copy_to_user)(两次复制,两次 domain switch,称 twofold copy);
  3. Binder Framework 的 server side 收到请求并触发预先准备的 onTransact()
  4. 最终控制流回到 server,server 可以通过 AB driver 回复;

XPC 对此的优化方式是:

  • 不修改 IPC interfaces(transact/onTransact)确保应用兼容性;
  • extend binder driver 来定义管理 xcall-capx-entry table 的能力;
  • 当应用调用 binder interface 的 addService 注册服务时,修改后的 framework 会向 binder driver 触发 ioctl 指令来添加一个 x-entry
  • 对于使用 getService 准备调用对应服务的 client 而言,framework 会 set-xcap 来保证 client 有权访问对应的 x-entry
  • 上面都是准备工作。在 client 真正调用 remote method 的时候,使用 xcall / xret 指令,并且用 relay memory segment 实现 parcels 来完成 data transfer。期间注意 kernel 还是需要管理 per-thread XPC registers;

这样期间的 domain switch 以及 memory copying 都被消除了;

此外,Android 的 anonymous shared memory 子系统向 user space 提供一套基于文件共享内存的接口。虽然类似匿名内存,但是借助共享 file descriptor 来向其他进程共享映射方案的。进程既可以使用 mmap 也可以用 file I/O;

和其他 share memory 来在进程间共享内存的方法一样,ashmem 需要多进行一次额外的 copy 来规避 TOCTTOU 风险,这会造成性能损失。于是 XPC 优化如下:

  • ashmem 分配:更改 framework 在分配 ashmem 时用 driver 分配一段 relay memory segment;
  • ashmem 映射:map 动作会将位于物理内存上的 relay memory segment 分配到虚拟内存上,设置 seg-reg 寄存器;
  • ashmem 所有权转移:仅通过 framework 调用 xcall 是传递 seg-reg 来完成;

不过一般情况下同一时间只有一个活跃的 relay memory segment。如果一个应用需要同时访问多个 relay memory segment,可以借助 page fault(隐式)或 使用 swapseg(显式)切换 relay memory segment;

seg-reg handover

在 calling chain 传递 seg-reg 想法是好的,但实际上还会遇到一些问题:

  • 如果需要一部分一部分传递 relay memory segment 的内容,应该怎么办?
  • 我们在上面讨论 “调用链异常终止” 的情况时,只是采取措施不至于返回到错误的进程,但死去的进程的资源(例如它所创建的所有 relay segment memory,还不能被其他 relay memory 或页表使用)。应该在什么时机释放它们?
  • 传递过程中可能出现追加信息的情况(例如网络栈会追加头部包数据),如果这个时候超过 relay memory segment 大小怎么办?

第一个问题很简单,就利用 seg-mask 像滑动窗口一样向 callee 传递其中一段内存即可;

第二个问题,在触发异常进入 Kernel 后,Kernel 除了 pop invalid link stack entry 或者将 dead process 的首级页表清零以外,还可以扫描 seg-list,恢复 caller 原来的 relay segment,同时释放掉这个进程分配的其他 relay segments;

第三个问题,需要 Message Size Negotiation;如果存在一个调用链 A -> B -> [C | D](B 可能调用 C 也可能调用 D),那么:

其中 $S_{\text{all}}(X)$ 表示 $X$ 需要的所有空间,$S_{\text{self}}(X)$ 表示 $X$ 在自己的 logic 中需要追加的内容所占空间;

测试评估

优化对比:

  • partial context
  • tagged TLB
  • Nonblock link stack(write)
  • XPC Engine cache(1 entry prefetch)

Microbenchmark

  • One-way call
  • Multi-core IPC:因为采用了 migrating thread model,跨核调用的性能并没有明显下降;

OS Services

  • file system
  • network

Android Binder

limitations: 仅支持同步 IPC(异步 IPC 像 death notification 还尚未实现),并且没有实现 split thread state 来应对 Kernel 的 trap,而是充分利用在 RISC-V 中的 “machine mode” 来捕获 xcall 和 xret 期间发生的异常。

MACHINE MODE ?

Machine Mode (M-mode) in RISC-V is the highest privilege level in the architecture, primarily used for low-level system control and management. Here are some key points about M-mode:

  1. Privilege Level: M-mode has full access to all resources and can execute any instruction without restrictions. It is designed for the operating system kernel and hardware abstraction layers.
  2. Bootstrapping: When a RISC-V system powers on or resets, it starts execution in M-mode. This allows it to initialize hardware components and set up lower privilege modes (User Mode and Supervisor Mode).
  3. Control Registers: M-mode can configure and control various system registers and hardware features, such as interrupts, timers, and memory management.
  4. Exception Handling: M-mode is responsible for handling exceptions and interrupts. When a higher privilege level (e.g., Supervisor Mode) generates an exception, control can be transferred to M-mode for processing.
  5. Delegation: M-mode can delegate certain exceptions and interrupts to Supervisor Mode, allowing for a structured and layered approach to handling system events.
  6. Memory Protection: M-mode can configure the memory protection settings for lower privilege levels, ensuring safe execution of applications.

Overall, M-mode provides the foundational control necessary for managing the RISC-V system, enabling the efficient execution of higher-level software components.

硬件开销优化

资源 utilization 可以进一步优化,例如使用 Verilog 替代 RocketChip 中的 Chisel;

讨论

安全性分析

XPC 认证和识别

一个 caller 如果没有对应的 xcall-cap,则不能直接通过 xcall ID 来触发一次 XPC;这个 xcall-cap 一般是需要向注册服务的 server(具有对应的 grant-cap 能力)申请,类似 08 年的一篇文章介绍的 CuriOS 中 name server 一样;

其次,一个 callee 可以通过 xcall-cap-reg 指向的 bitmap 来确定 caller 的身份,而这个寄存器在 xcall 时会被 XPC Engine 放在 general purpose register(RISC-V 中是 t0)中,无法被伪造。

针对 TOCTTOU 的防护

XPC 机制中,一个 relay segment 可以通过 reg-seg 在进程进程间传递,同一时刻最多只有一个线程拥有这片空间。并且 Kernel 会保证不会有与 page table 规定区域 overlap 的情况出现。

调用链故障隔离

在 XPC 调用链 A -> B -> C 上,任意一个进程 crash 后,总是能回到上级 caller 中继续执行。这主要是 XPC Engine 定义的机制:某进程死亡后 Kernel 会扫描一遍 link stack entry,invalid 掉无效的 process(或者进行 top level page table 清零的操作),在 xret 触发 exception 时再 pop 掉;同时扫描 seg-list 释放归属于无效进程的空间、恢复原先 caller 的 seg-reg、relay segment list 等等。

注意:不能在进程刚结束就释放 relay segment lists,而是在调用链返回出错时才释放。因为我们需要死亡进程在调用时的 seg-reg 数据,以通过 xret 的检验。

除此以外,XPC 还可以支持超时机制,但是实际使用时经常设置为 0 或者 $+\infty$,经常会出现没有用的情况。

DoS Attacks 防御

一种 DoS Attacks 的方式是,请求创建大量的 relay-segment,占用连续的物理内存空间,造成比较多的 external fragments;但 XPC Engine 会把 relay-seg 放在 process 的 private address space 上,因此这样本身既不会影响 Kernel 也不会影响其他进程;

PRIVATE ADDRESS SPACE ?

因为在创建 relay segment 的时候选取的是连续的物理地址,类似 Android Binder Ashmem 创建和映射的过程,会把这个物理地址映射到当前进程的私有地址空间,然后再更改 seg-reg / 推入 seg-list,因此说使用的是 process 的private address space;

另一种 DoS Attacks 的方式是,频繁调用 callee,消耗 x-entry 对应的 pre-invocation C-Stack(context & data);这个通过 credit system 解决,callee 可以在接受调用前检查 caller 的信用分,如果过低就不分配 XPC context 给它;

Timing Attacks 防御

首先 XPC Engine cache 比较少,发生的可能性小;其次可以像 tagged-TLB 一样,通过 tag cache entry 实现 thread-private 的形式阻隔一个线程感知其他线程的 cache,进一步减小 timing attack 的可能。

进一步优化

  • scalable xcall-cap:将 bitmap 换成 radix tree,但是会增大内存开销,使得 IPC 性能下降;
  • Relay Page Table:提高 relay segment 的空间利用率,可以把 segment 换成类似二层页表的结构,但是 ownership transfer 就比较难做,而且只支持以一页为粒度的内存控制;

其他相关工作

We revisit previous IPC optimizations in this section.

Domain Switch

  • protected procedure call (LRPC) / migrating thread(software,trap

​ 这种方法的好处是什么?

Eliminates the scheduling latency and mitigates IPC logic overhead !

  • CODOMs、CHERI(hardware):

    The switch can be done directly at unprivileged level without trapping to the kernel, which is a huge advantage against software optimizations. Meanwhile, multiple domains can share one address space (single address space), which can further reduce the overhead of TLB miss after domain switch. However, these systems usually require non-trivial changes to existing micro-kernels which are designed for address space based isolation mechanism.

    (不仅硬件上需要大改,软件上也不支持现有的基于内存地址空间隔离的 micro-kernels);

Message Copying

  • LRPC(software):reduce twofold copy(caller -> kernel -> callee) to one(caller -> shared memory);

    TOCTTOU 发生机制:在 callee 检查完成 shared memory 中的 messsage 有效性和安全性之后准备执行时,caller 通过精巧的时间掌控(例如利用 trap)在期间更改了 shared memory 的内容为恶意数据或代码;

    但是如果再从 shared memory 中 copy 一次,那就丧失了节省一次 copy 的优势了;

  • remapping page ownership(software): kernel involved & TLB shootdown;

    而且 remapping page 粒度在 page 级别,很难在 calling chain 中 handover(对 shared memory 也是);

  • temporary mapping(software):kernel 找到 callee 中 unmapping area,把它和 caller 的 communication window(一种位于 caller address space 中,但只能被 Kernel 访问的空间)map 到一起;当 caller 向 Kernel 请求发送把消息 copy 到 communication window 并 send 之后,caller 本身是无法继续访问这块空间,杜绝了 TOCTTOU 的风险,但是:1 次 copy、remapping + TLB shootdown、kernel involved 仍然会带来不可忽略的开销;

  • CODOMs(hardware):hybrid memory granting mechanism(permission list + capability registers),但是同样存在 ownership 的问题(哪个线程有这个寄存器的值就能访问)。不过由于使用 single address space,因此 tagged memory 来实现隔离,并且能降低 TLB shootdown 带来的开销;