Inter-process communication in a safe kernel论文翻译

Inter-process communication in a safe kernel

摘要:

传统的操作系统遵循单片设计,在共享地址空间中执行所有内核子系统,从而以牺牲隔离和安全为代价获得良好的性能。微内核通过将操作系统组件分离到单独的地址空间来改进这种设计,但由于交换地址空间的成本很高,过去一直非常昂贵。RedLeaf是一个新的操作系统,它依赖于Rust编程语言的安全性,而不是硬件机制的隔离。RedLeaf在相同的硬件地址空间中运行所有的操作系统内核子系统,并通过语言安全和特殊通信原语的组合来实现隔离。其结果是通信开销在几十个周期的数量级上,与常规函数调用相当。然而,即使在安全的内核中,跨域通信也需要谨慎的设计选择,以在域崩溃的情况下保持隔离。本文描述了这些设计选择,并介绍了共享堆和远程引用(RRef)的后续概念,这些概念建立在Rust的安全模型之上,提供零拷贝通信,即使在子系统崩溃时也能确保安全性和隔离。

1.介绍

背景:大多数现代操作系统都是作为单个内核实现的。单片内核具有丰富的特性,包括设备驱动程序,用户级程序驻留在单独的虚拟地址空间中。尽管它们具有良好的性能,但是单片内核的容错性很差,单个设备驱动程序的恐慌可能导致整个系统崩溃。更糟糕的是,单片内核中任何部分的漏洞都可能导致机器的完全接管。微内核是对单片内核的响应,具有更好的安全性和故障隔离,通过将尽可能多的操作系统功能移出内核来实现。这种模块化极大地提高了容错性和安全性,但代价是隔离子系统之间昂贵的地址空间交叉。现代的微内核,比如L4系列改进了进程间通信(IPC)性能,并在生产中被广泛采用,但仍然承受着巨大的开销。最先进的seL4微内核在3.4GHz x86 CPU[10]上引入了最小的1260个周期的开销,这对于每秒需要数百万个域交换的现代I/O密集型工作负载来说仍然是无法接受的。随着最近低成本内存安全编程语言的出现,现在通过依赖内存安全保证而不是硬件隔离机制来提高域交换的性能有了空间。

RUST:Rust是一种新的系统编程语言,专注于通过子结构类型系统[13]实现编译时内存安全。这意味着常规的Rust指针有一个静态定义的生命周期,这可以防止一系列内存bug,如free后使用、悬空指针和double-free。此外,在安全的Rust(该语言的严格子集)中,指针强制转换和原始指针解除引用在编译时是被禁止的。通过强制执行安全的Rust保证,可以静态地确保坏行动者的程序不会操纵外部内存,从而消除对虚拟地址空间的需要。

Redleaf:RedLeaf[12]是Rust中从头开始编写的新奇微内核。RedLeaf依靠Rust的内存安全机制,而不是硬件机制来进行内存隔离。程序被组织成域,这些域共享一个地址空间,但在其他方面是隔离的。这种设计消除了困扰微内核的昂贵的地址空间跨越,实现了令人难以置信的跨域通信性能,而不牺牲安全性或隔离性。

隔离:虽然Rust在每个域的基础上实施内存安全,但它不足以确保跨域通信期间的完全隔离。当域出现故障时,内核将其从内存中卸载,以释放资源。如果这种情况发生在跨域调用的中间,就会违反Rust的内存安全保证,因为任何进入死域的外部指针现在都是悬空的。为了实现完全的域隔离,我们引入了以下不变量:域不共享堆,域不公开来自各自堆的指针。有了这些保证,域可以安全地从内存中卸载,而无需任何外部悬空指针。对于跨域通信,域将交换来自单个共享堆的指针。为了实现这一点,我们引入了远程引用(RRef s)机制,即对共享内存的引用。RRef使域能够以零复制的方式创建和交换共享堆对象,同时在面临域崩溃时保持安全性。

本文描述了远程引用和共享堆的实现,以及RedLeaf的零复制和容错IPC背后的机制。

2.RedLeaf Architecture

:RedLeaf微内核处理一组狭窄的任务,并将其余的任务委托给用户级域(图1)。微内核执行调度和域加载,并提供一个接口来分配共享内存和处理中断。设备驱动程序、文件系统和所有其他程序都作为运行在内核之上的用户级域实现。

虚拟内存:Safe Rust执行与RedLeaf相关的内存安全的几个方面。值得注意的是,原始指针,大致相当于普通C指针,不能解引用或转换为安全指针。因此,安全的Rust程序只能访问指向分配或系统调用结果的内存——它们不能从头开始创建指针。RedLeaf在不受信任的域中实施安全Rust,隔离每个域的堆,这保证了域不会访问外部内存。这减少了对硬件虚拟地址空间的隔离需求,因此所有域都可以在相同的地址空间中运行。

进程间通信:我们将进程间通信(IPC)定义为任意两个域之间的通信。与消息传递体系结构(典型的微内核)或特权系统调用(典型的单片内核)相反,RedLeaf IPC接口公开为一组功能[14],作为Rust特征对象实现。因为所有域都位于相同的地址空间中,通信可以是零拷贝的。IPC调用中的参数可以是堆栈上按值传递的小值,也可以是按引用传递的大对象。如果没有仔细的设计,通过引用传递的对象可能成为攻击的载体。

攻击模型:RedLeaf依赖Rust的安全性来实现跨域隔离。IPC可以使Rust安全性失效,打破允许攻击的隔离。由于每个域都有自己的堆这一不变条件,通过引用共享对象允许将外部指针指向域的堆。如果一个域从内存中卸载,而另一个域引用它的堆,那么这个引用就变成了一个悬浮指针,破坏了Rust的内存安全模型。这可以被武器化:一个域可以在IPC呼叫中故意恐慌,以摧毁另一个域,打破隔离状态。我们的解决方案不是限制IPC,而是围绕一个共享堆,它确保域共享的内存始终有效。

共享堆:共享堆是内存的一个特殊区域,用于跟踪所有权。当域从共享堆请求内存时,它将自己标记为指针的所有者。当域与另一个域共享这个指针时,指针的所有者发生了变化,这由共享堆记录下来。通过这种方式,每个域都是完全隔离的,并且RedLeaf内核可以安全地卸载一个域并释放该域拥有的任何共享堆内存。

远程引用:共享堆提供了供IPC安全使用的内存。为了正确使用共享堆,我们引入了远程引用(RRef)。RRef在共享堆上分配内存并跟踪域所有权。RRef静态地保证指向共享堆上的有效内存,执行Rust的内存安全保证。这允许域通过IPC与RRef的参数安全地通信,即使在其中一个域恐慌的情况下。

代理:为了处理故障和跟踪RRef域所有权,我们引入了代理。代理是透明的域,用于插入每对域之间的通信。对于每个跨域调用,代理记录RRef所有权和调用堆栈信息。如果域在IPC调用过程中发生恐慌,执行将使用调用堆栈信息返回到代理,隔离故障。该代理由RedLeaf IDL生成,并引入了较小的开销,以确保整个域隔离。

3 实现

远程引用(RRef)在一个可信的箱子中实现,暴露类型RRef, RRefArray,和RRefDeque。将来还可以在这些类型的基础上构建更多的数据结构。本节将介绍RRef板条箱的实现细节,以及为其提供动力的共享堆组件。

3.1 共享堆

RRef是指向共享堆的特殊指针。它们引用供IPC安全使用的共享内存,并根据它们所在的域更新内存的所有权。共享堆公开了分配和释放指向内存的原始指针的方法。解引用原始指针需要不安全的代码[7],将其限制在受信任的RedLeaf组件中使用。RRef板条箱是这些受信任的组件之一,它构建在共享堆功能之上。

清单1显示了共享堆功能的接口。alloc(layout:drop_fn:)方法返回三个指针,用于管理共享堆内存的状态。此状态由RRef类型抽象,该类型在跨域期间由代理域管理。共享堆维护一个包含关于每个分配及其反构造函数方法的信息的注册表。记录在分配或重新分配时从注册表中插入或删除,并作为内核在域恐慌时清理共享内存的真实来源。

3.2 RRef

3.2.1 Rust pointer types

RRef指针类型被设计成尽可能的rust原生类型,并模仿标准库中的现有类型。与C不同,Rust中的指针类型在类型级别上是不同的。在C语言中,指针可以引用堆、堆栈、内存映射值,也可以什么都不引用(null)。在《Rust》中,每一个案例都有不同的类型。

Rust最原始的指针类型是Box,它指向堆上的一个值。new(10u64)在堆上创建一个指向64位无符号整数的指针。更复杂的指针类型构建在Box之上。记住这一点,基本RRef类型是模仿Box的。RRef::new(10u64)在语义上与Box::new(10u64)非常相似。但是,RRef::new(10u64)不是在域的堆上分配,而是在共享堆上分配。就像Box,更复杂的类型如RRefArray和RRefDeque都是建立在RRef基础类型之上的。

使用RRef与使用标准库指针非常相似。比较清单2a(常规Rust引用)和清单2b(远程引用)中显示的块设备驱动程序接口。通过引用传递的data参数被包装在RRef中,以强制使用共享堆。mutable borrow (&mut)被转换为move-in-move-out语义,原因将在第3.2.6节中介绍。在调用点上,解引用和操作RRef的语法本质上与Box类型相同(清单3)。

3.2.2 结构

RRef是指向共享堆上值的三个指针的组合。这些指针所用的语法∗mut是一个原始指针[7]。原始指针是Rust中不安全的子集的一部分,并且提供很少的内存保证。使用RRef,这些指针总是直接来自可信的共享堆,因此我们可以假定它们是有效的。

value_pointer包含实际的对象。类型T是泛型的,受限于RRefable(更多见3.2.5节),这意味着实际的类型、布局和大小是在编译时解析的。RRef::new([1usize,2,3])将请求内存来存储三个整数的数组,并将数组复制到共享堆(这是RRef生命周期中的唯一副本)。

domain_id_pointer跟踪域的当前所有者。RRef有一个所有者,除了只读借阅(章节3.2.6),如果当前所有者死亡,RRef会自动释放。当RRef从一个域传递到另一个域时,它的所有者会在保护每个域的可信代理中发生更改。

最后,borrow_count_pointer计算有多少个域借用了这个对象。此信息用于允许不可变借用而不泄漏内存;第3.2.6节将进行更详细的介绍。

3.2.3 初始化

到目前为止,RRef crate 中最复杂和不安全的部分是RRef::new初始化方法。new从共享堆请求内存,并执行RRefable对象的字节复制(更多信息见3.2.5节)。这种方法对于正确使用是至关重要的,因为它执行的原始内存操作可能会破坏Rust的内存保证。

清单5显示了完整的初始化代码。在第11行,我们从共享堆请求内存并获得三个原始指针。RedLeaf控制每个域的HEAP全局初始化,因此我们认为它是可信的。因此,我们可以假设它返回的任何指针都是有效的,并且具有预期的布局。Rust能够在编译时计算泛型类型的布局(大小和对齐方式),见第4行。>确保泛型类型不包含引用,所以简单地分配字节并将它们转换为T是安全的(第14行)。

RRef实现的其余部分封装了对三个原始指针的操作,以简化使用共享堆内存的工作。

3.2.4 非关联化

Rust具有去引用的内在特征,Deref[5]和DerefMut[6]。这些特性使几个层次的语法糖成为可能,这对智能指针特别有用。首先,它们允许星号解引用操作符(∗ptr),该操作符从智能指针中提取可变或不可变引用。其次,对于任何实现Deref<Target=T>的类型,编译器隐式实现T类型的方法。这意味着,例如,RRef::new(10u64).checked_add(1)是有效的代码,它自动解引用RRef到&u64,并调用相应的checked_add方法。

清单6显示了RRef的DerefMut trait的实现。代码看似简单,解引用原始指针并创建对指针值的常规可变引用。在幕后,Rust还插入了将&mut链接到&mut self的生命周期[3]。这确保了被解引用的&mut不会比RRef容器活得长。而且,这允许编译器强制RRef的Rust模型为“一个可变引用或多个不可变引用”[4]。

3.2.5 RRefable

RRef确保T的内存位于共享堆上。但是,如果T是一个数据结构,在共享堆之外引用另一个内存呢?如果该引用指向死域堆上的内存,则在IPC期间该引用可能成为一个悬空指针。安全Rust中的指针保证是有效的,因此这会破坏Rust的安全性,从而成为攻击向量。出于这个原因,我们限制RRef只包含“copy”类型或对共享堆上内存的其他引用。遵循这些规则,RRef, RRef<[1,2,3]>, RRef<RRef>都是有效的,而RRef<&str>应该无法编译。

我们可以在RedLeaf IDL中执行这一点。然而,使用Rust丰富的特征类型系统,我们可以在编译时完成大部分验证工作。在这种情况下,RRef中的T受RRefable特性的限制。

RRefable特征定义如清单7所示。作为自动特性[8],RRefable被隐式地实现为符合定义的每个类型。RRefable不是枚举RRef可以包含的所有可能的类型,而是为所有引用类型提供了否定的实现。Rust编译器递归地检查自动特征定义,因此即使Box不直接匹配∗mut、∗const T、&T或&mut,它仍然不是可重构的,因为它在i的某处包含了一个引用

3.2.6 Borrowing

前面的清单2b显示了一个典型块设备驱动程序的接口。与非rref代码(清单2a)相比,主要的区别是read(block:data:)方法,该方法接受并返回数据缓冲区(move-in-move-out),而不是可变地借用它。移入移出比较麻烦,但不幸的是,IPC不支持可变借用。考虑这样一个场景:块设备驱动程序域部分地写入缓冲区,然后出现故障。缓冲区处于损坏状态,不应返回到调用方域。这就阻止了它们在IPC中使用。另一方面,不可变的借款不构成这种威胁。

为RRef支持不可变借用的动机有两个。首先,不可变借用优雅地表示一个值是只读的,而移入移出值更改所有权,因此可以进行修改。此外,更重要的是,如果一个域将一个远程引用移动到另一个域,这将导致恐慌,那么RRef将永久消失。在不可变借用的情况下,该引用应该仍然有效,因为它从未被修改过。

我们用一个简单的计数器来跟踪不可变的借用期间的所有权。借计数器的目的是在域恐慌的情况下清理远程引用。一个代理位于每个域交叉之间,并管理借位计数器。当下降到另一个域时,它增加计数器,并在返回时(成功或不成功)减少计数器。RRef只有在借位计数器为零且其所有者域已死时才被释放。

考虑图2中的场景。当R被域B借用时,计数器增加到1。如果在这一点上域A恐慌,内核将检查共享堆注册表,发现R的所有者恐慌,但它有一个非零的借计数,因此仍在使用。一旦R返回到域B的代理,它的借位计数器就减少到零。由于所有者域(A)现在已死,并且借位计数器为零,该引用将被释放。

如果域B或C在R返回域A前恐慌,中间代理处理解除和安全地返回引用到域A,因为引用是不变地借来的,Rust保证它从来没有修改,因此仍进一步使用有效。

3.3 RRefArray

远程引用数组RRef<[T;它很快就变得笨拙起来。要将一个元素从数组中移出,它必须被包装在一个Option中以表示空槽。此外,元素还必须封装在自己的RRef中,因为当它从数组中删除时,它的所有者可以更改。要管理这种所有权,必须由RedLeaf IDL[1]生成适当的访问器。RRefArray简化了这个用例,并作为进一步的数据结构的基础,如RRefDeque(第3.4节)。

清单8显示了RRefArray主要是RRef<[Option<RRef>的包装;N] >。可以取出并重新插入元素,并跟踪它们的所有权。当一个元素被插入到数组中时,它的所有者变为0,表示它属于另一个RRef(这避免了double-free)。由于此代码位于受信任的RRef框中,因此允许它使用特权方法来更新所有权,否则就必须由IDL生成所有权。

3.4 RRefDeque

RRefDeque建立在RRefArray之上,提供了一个deque(双端队列)数据结构。deque数据结构支持队列头部和尾部的push和pop操作,并且包含在Rust的标准库中,如VecDeque[9]。RRefDeque的目标是成为VecDeque的替代品。RRefDeque的主要差异在于其易出错的插入操作。这源于这样一个事实,即所有共享堆分配都需要具有固定的大小,因为共享堆依赖于编译时类型布局信息。将来,共享堆还可以支持动态大小的数据结构,但目前受到Rust语言支持[2]的限制。因此,RRefDeque由固定大小的RRefArray支持,可能会在插入期间耗尽内存并返回错误。另一方面,当它接近容量时,VecDeque请求更多的内存,从而允许可靠的插入操作。

清单9包含了RRefDeque背后的源代码。RRefDeque建立在RRef和RRefArray的基础上,使其实现变得简单,不需要对域所有权进行任何操作。这说明RRef是一个可伸缩的抽象,可以支持大多数IPC需求。

RedLeaf中RRefDeque的主要用例是ixgbe网络驱动程序中的submit_and_poll操作(清单10)。Submit_and_poll向网络驱动程序发送一个包队列,网络驱动程序处理包并将它们移动到“收集”队列。如第3.2.6节所述,包队列的可变借用会更干净,但这是被禁止的。

4 性能

我们在开放的CloudLab [11] c220g2服务器上运行基准测试,将RedLeaf的IPC与当前的技术水平进行比较,该服务器配置了两个运行在2.60 GHz的Intel E5-2660 v3 10核Haswell cpu。表1显示了这些基准测试的结果,其中RedLeaf比seL4快一个数量级。RedLeaf的IPC性能与几个常规函数调用相当。唯一的开销是在域边界跟踪所有权,这是相对便宜的。这是几乎完全依赖于编程语言的安全和隔离的结果。

5.总结

事实证明,用语言安全取代硬件隔离方法可以显著提高性能,而不牺牲跨域通信的灵活性。Rust对设计智能指针类型的支持允许远程引用的行为与常规指针类似(如清单3所示),还增加了隔离的好处。依靠RedLeaf IDL在代理中生成粘合代码使其成为确保隔离的可伸缩方法。Rust允许新一波微内核开发,它可以实现细粒度的隔离,而不受硬件开销的影响。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值