For a Microkernel, a Big Lock Is Fine
摘要
众所周知,高端的可伸缩性需要细粒度的锁定,对于Linux这样的系统,即使在中等核心数的情况下,大的锁也会降低性能。然而,我们认为大锁对于设计用于运行在紧密耦合内核(共享缓存)上的微内核来说可能足够细粒度,就像设计良好的微内核的典型短系统调用一样,在实际负载下锁争用仍然很低。
1.介绍
为了同步访问共享内核状态,最简单的方法是使用大内核锁(BKL),该锁在内核进入时使用,直到内核退出才释放。由于争用,即使在中等处理器数量的情况下,BKL的性能也很差。为了实现高端的可伸缩性,最小化争用是必要的,这使得大锁不适合使用。
然而,有些场景本质上并不处理高核心计数。例如,为嵌入式系统设计的soc目前只具有低到中等的核心计数(最多8个),而为高端可伸缩性设计的优化对于这样的处理器来说可能不是最优的。此外,对于只提供基本机制而不提供一般系统服务的微内核,高端可伸缩性最好通过无共享的多内核设计来实现,从而避免内核中的所有锁定,以支持内核间消息传递。
对于共享缓存的少量内核来说,多内核不是最优的,因为所需的内核间通信比通过紧密耦合的内核之间的共享缓存通信要昂贵得多。seL4微内核是一个通用平台,目前主要部署在核心数较低或中等的系统上,因此我们倾向于聚类多内核方法:共享一个缓存的内核集群共享所有内核状态,而跨集群则不共享任何内核状态。
这种系统的最佳锁定策略并不明显:seL4的设计使得所有常用的系统调用(消息传递IPC和中断处理)都非常短,这导致的争用比Linux等单片内核上低得多。由于共享缓存处理器集群不太可能扩展到大的核心数,大锁可能是最好的设计。
我们研究在共享缓存的内核上运行的微内核的锁粒度权衡(无论是作为多核集群设计的一个集群,还是因为整个芯片是紧密耦合的)。我们发现,在这种情况下,BKL对于至少8核集群仍然具有竞争力,这意味着它仍然是微内核的首选设计,与单片操作系统形成鲜明对比。
2. 这个Big Lock有什么吸引力
有人可能会问,我们为什么要费事弄这个大锁呢?为了比较它与细粒度锁定的性能,我们显然需要实现后者,所以为什么不保留它,不再担心可伸缩性呢?
支持大锁的主要问题有两个:开销和正确性。
2.1 锁的开销
每个锁都会带来开销,如图1所示。在这里,我们测量了在x86和ARM架构上,应用于seL4内核的不同锁策略的单核(热缓存)来回IPC开销(参见4.1节了解平台的详细信息)。“BKL”和“fine”分别指应用单锁和细粒度锁,而“RTM”指使用硬件事务内存进行同步。“None”与“BKL”相同,但已编译了锁代码。我们将在后面讨论锁实现的细节(第3节)。
图中显示,一个锁的开销在x86上是65个周期,即10%,在ARM上是124个周期,即20%。细粒度锁定的开销是单核性能的三倍(每次系统调用总共需要4个锁),x86的开销约为30%,ARM的开销约为60%。细粒度锁定的额外成本显然非常重要,而IPC成本对于基于微内核的系统的性能至关重要。目前尚不清楚减少的争用是否使这种开销有价值。
RTM锁也是如此:它本身就更昂贵。由于硬件支持的事务在cache-line的粒度上进行保护,与BKL相比,RTM也应该减少争用,问题是这是否会超过更高的基线成本。
2.2 正确性
细粒度锁定将并发性引入内核,而并发性是出了名的难以把握。即使是seL4的小型9 kLOC代码库(其中只有大约1500 LOC是对可伸缩性至关重要的IPC或中断处理代码),我们也知道很难获得正确的优化细粒度锁定实现。事实上,即使经过几个月的工作,我们仍然不相信我们的方法是100%正确的。
seL4是为安全或安全关键用途而设计的高保证系统,因此正确性是首要考虑的问题。该内核已经全面正式验证了功能正确性和安全性,但这仅适用于单核版本。验证单锁版本是可行的,但是我们目前的验证方法不能处理细粒度锁定产生的并发执行代码。我们的验证团队估计,将验证扩展到带有细粒度锁定的内核版本将远远超过验证单核版本的成本。我们的验证团队估计,将验证扩展到带有细粒度锁定的内核版本将远远超过验证单核版本的成本。
3. 锁定seL4状态
3.1大内核锁
BKL是对现有的seL4多核设计的自然的、最小的扩展,因为它很容易实现,并且在很大程度上保留了内核中没有并发性的假设。内核的进入和退出代码保存用户状态并将其恢复到每个内核的内核堆栈,并设置安全的内核执行,这些代码保留在BKL之外,而内核的其余部分则受到BKL的保护。
这种设计是不完全充分的-以下不变式,在验证中使用,不再适用于多核心内核,即使当BKL被持有:
除了当前正在执行的线程的TCB和页表外,所有其他TCB和页表都是静态的,可以修改或删除。
在其他内核上执行的用户级代码隐式地依赖于正在运行的线程的TCB和页表,通过内核入口代码转换到内核模式,从而竞争BKL。因此不变式不再成立。我们通过修改内核来解决这个问题,以确保远程内核不依赖于任何正在删除的TCB或页表。我们的原型目前采用了一种na¨ıve方法,即使用IPI让远程内核进入内核空闲循环(它有一个永久的TCB和页表)。
BKL设计部分由现有的事件驱动代码库驱动,由于在微内核中大多数系统调用的持续时间很短,因此是一个有效的设计选择;在任何其他类型的系统上,它都会导致较差的可伸缩性。
唯一需要的其他更改是(i)启用TLB关闭和(ii)引入每核空闲线程。为了最小化核间缓存行迁移,我们还在中引入了每核调度程序队列和当前线程指针,尽管访问是由BKL序列化的。
为了减少争用(并启用事务性内存的使用,参见3.3.1节),我们在BKL发布后,通过移动上下文切换相关的硬件操作,进一步减少锁定代码的数量。
我们的BKL实现使用一个CLH锁[Craig1993]。CLH锁是一种可伸缩的基于队列的锁,在等待时在本地缓存线上旋转。我们也试验了票锁,但它们是不可扩展的,不支持提供性能好处
3.2 细粒度的锁
为了将粗粒度的BKL与更复杂但更可伸缩的细粒度锁进行比较,我们首先用一个大的读取器锁(Corbet)替换BKL。该锁允许所有读取器内核并行执行,因为它们只访问本地状态以获得读锁。现在暴露给并发写的数据结构使用单独的细粒度锁进行保护。
IPC会改变tcb、端点和(可能的)调度器队列的状态(这取决于在IPC期间是否应用了避免队列更新的优化)。我们在这些数据结构中添加票据锁(即细粒度的锁),以便同步在读取器锁中捕捉IPC。典型的同步IPC现在包括内核读取器锁、两个TCB锁和一个端点锁。==IPC期间的锁争用现在仅限于IPC涉及共享目标或端点的情况,或者与内核写入锁的一般争用。在独立核心上执行IPC的独立活动不会导致锁争用。==我们通过在锁定tcb之前识别所涉及的tcb(通过读取器锁定提供的内存安全使得这成为可能),然后按照它们的内存地址顺序锁定它们,从而避免了锁定tcb导致的死锁。
在我们目前的原型中,我们有选择地使用写锁,以避免为了同步对性能不关键的路径的并发性而进行的重大代码更改。典型的操作系统功能(如网络)是在用户级实现的,因此大多数微内核系统调用的使用频率远低于IPC或中断传递。因此,选择写入锁来同步不太频繁的系统调用既注重实效,又不影响业绩。
这种设计至少保留了在写入锁中解除内核对象的分配。这样做的好处是在持有读取器锁的同时保留了现有的内存安全,不过对象本身的内容将暴露于并发性以提高可伸缩性。
3.3 硬件事物内存
3.3.1 体系结构支持
从Haswell微架构开始,英特尔处理器的特点是一个被称为英特尔TSX的受限事务内存(RTM)实现。我们使用它来实现RTM锁。
TSX提供了3条新指令:XBEGIN、XEND和XABORT。在XBEGIN和XEND指令之间成功执行的代码看起来已经完成原子性,因此称为事务性区域。如果在事务区域执行期间存在任何内存冲突,事务将中止并跳转到XBEGIN指定的指令。程序可以通过发出XABORT指令显式终止事务。
TSX利用现有的缓存一致性协议,来识别CPU上不同核写入和读取的缓存线路集。这有两个重要的原因:内存冲突是在cache-line粒度上捕获的,事务必须符合硬件限制的L1缓存大小。后一种结果表明,将一个完整的单片内核包装到RTM事务中可能是不可行的,因为它不太可能适合L1。
由于TSX的实现,RTM锁在逻辑上锁住了一组动态的L1缓存线,这是一种非常极端的细粒度锁,它应该会大大减少争用(假设内核数据结构布局合理)。
注意,即使事务足够小并且没有内存冲突,RTM事务也不能保证会完成。各种各样的(特定于硬件实现且通常未指定的)场景都可能导致异常中止。我们工作中特别感兴趣的是特定寄存器上的某些交互,这些交互会触发中止,但在执行操作系统代码时显然是不可避免的。
由于事务不能保证进程,开发人员必须确保存在同步的回退方法,以确保在重复中止的情况下进程。我们使用一种常见的实现技术,在重复中止的情况下,对代码片段返回常规锁。为了避免事务保护的片段和锁保护的片段之间的竞争,我们的事务在进入RTM代码片段时测试该锁,以确保该锁是空闲的,并且处于事务尝试的读集中。竞争线程对锁状态的更改将触发所需的中止,并允许片段通过锁进行同步。
3.3.2 RTM锁实现
TSX扩展,再加上内核的小尺寸,允许我们在没有并发控制的情况下乐观地执行大部分代码。这是通过seL4的两阶段系统调用结构实现的,其中第一阶段验证执行的前置条件,第二阶段保证执行不会失败。对于这个设计来说,基于事件的内核设计也很重要,它避免了阻塞。我们几乎用事务原语括起了整个内核。
除了3.1节中描述的更改之外,我们还需要在事务之后移动任何特定于tsx的触发中止的CPU操作。
许多这样的操作在seL4中不会发生,因为大多数中止操作都是设备驱动程序的典型操作,而设备驱动程序是seL4中的用户级程序。剩下的问题操作是:
- 上下文切换触发的页表寄存器(CR3)加载和段寄存器加载;
- IPI触发内核间通知
- 用户级设备驱动程序的中断管理,它包括在返回到用户级处理程序之前屏蔽和确认中断。
这里的关键观点是,将这些操作移到事物之外是安全的,因为两阶段内核确保了需要这些操作的系统调用在进入执行阶段后能够成功,并且这些操作是内核的本地操作,因此不会暴露于来自其他内核的并发访问。。
4.评价
4.1 平台
4.1.1 x86平台
作为x86平台,我们使用台式机戴尔Optiplex 9020,它具有Q87 Express芯片组和英特尔酷睿i7-4770处理器。这是一个时钟速率为3.4 GHz的四核处理器,每个处理器有两个硬件线程,总共有8个硬件线程。它也是Haswell微架构的代表,因此支持Intel的TSX,我们使用TSX来实现RTM锁(见3.3节)。
处理器有三级高速缓存。每个核都有专用的L1指令和一个数据缓存,每个缓存的大小是32 KiB,并且有8路关联。每个内核还有一个私有的、非包容性的8路256 KiB L2缓存。所有内核之间共享16路8级MiB L3缓存。
该平台还包括16 GB的主内存和82574L千兆以太网控制器。
4.1.2 ARM平台
我们的ARM平台是Sabre Lite,它基于飞思卡尔i.MX 6Q SoC,具有四核ARM Cortex-A9 MPCore处理器。
这些核心以1 GHz的时钟速率运行,并拥有私有的、分割的L1缓存,每个4路关联,大小为32 KiB。核心共享一个统一的、16路联动的、1 MiB的L2缓存,这是最后一级缓存。典型的L1接入时间为1-2个周期,L2接入时间为8个周期[ARM 2010]。该平台有1gib的主内存,我们测量的访问时间为51个周期。
该平台还配备了千兆以太网控制器,但由于内部总线吞吐量的限制,理论上的最大性能被限制在470mb /s(发送和接收的总和)[Freescale 2013]。
MPCore架构的特点是在私有L1数据缓存和共享L2缓存之间有一个snoop控制单元(SCU)。SCU实现了MESI缓存一致性的变化,通过以下的适应。
- SCU复制L1数据缓存的标签位,在不访问远程缓存的情况下,可以检查远程缓存。
- 干净的数据直接从L1复制到L1 (ARM称之为直接数据干预)。
- 脏数据(即MESI中的修改状态)直接从一个核的L1迁移到另一个核的L1,而不需要首先将数据写回共享的L2(称为迁移线)。
4.2 微基准测试
在基于微内核的系统中,IPC性能是整体系统性能的关键因素,因此优化IPC性能在L4社区中有很长的历史。最佳情况下IPC性能的传统基准是“乒乓”:单个核上的一对线程除了相互发送消息之外什么也不做。这允许我们评估我们的锁实现的基本成本,即纯粹的获取和释放成本,而没有任何争用。
我们将单核乒乓扩展到多核(包括超线程)。具体来说,我们在每个硬件线程上运行一个ping pong的副本,所有硬件线程完全独立地执行,且不同步。
这个基准测试会在内核上产生极端的争用(几乎为零的用户级执行时间)。但是,没有一个内核数据结构是竞争的,因为每个硬件线程的一对软件线程在它们的系统调用期间访问互不相连的内核对象(tcb和IPC端点)。因此,在最大化BKL上的争用的同时,可以期望细粒度锁定和RTM能够完美地扩展。
由于缺乏对共享数据的并发访问,我们可以运行这个基准测试,以获得一个理论上的、完美的、零开销的细粒度锁的最佳性能:我们使用一个(不安全的!)没有任何锁定的内核实现。
4.3 Macro-benchmark:复述
多核乒乓提出了一个不现实的情况,没有用户级的工作,这是BKL最糟糕的情况。为了评估BKL的可伸缩性,以及细粒度方案开销的重要性,我们寻找“现实的最坏情况”场景,即在现实条件下产生尽可能高的系统调用率的基准。
通常的嵌入式系统基准测试都不会在微内核上产生显著的系统调用负载,因此我们使用服务器风格的基准测试。注意,基准测试的性质与本练习完全无关,需要考虑的是内核条目的速率和分布。相关的操作是IPC和中断处理,因为所有其他微内核操作都要处理相对较少的资源管理。
实际上,单片系统中系统调用的seL4等价于向服务器进程发送IPC消息并等待应答(即每个单片操作系统系统调用两个微内核IPC)。类似地,一个中断,在一个单片操作系统中导致一个内核条目,为基于微内核的系统产生两个,因为中断被内核转换成一个通知给驱动程序(一个内核条目),驱动程序通过另一个系统调用向内核确认。
为了加强我们的内核,我们使用了一个简单的服务器场景,包括Redis键值存储[Redis]。它从网络接收客户端请求,使用(usr-level)以太网驱动程序和lwIP TCP/IP堆栈[lwIP]的端口作为usermode进程运行。
在单核上运行时,setup由三个进程组成:driver, network stack和Redis。
对于多核设置,核心0有相同的配置,而所有其他核心(或硬件线程)运行他们自己的副本lwIP和Redis。所有中断都转到核心0上的以太网驱动程序,它根据端口号将传入数据包解复用到网络堆栈。
注意,我们将Redis作为volatile实例运行(我们禁用了文件系统访问),因为我们的原型缺乏文件系统支持。
我们评估性能使用雅虎!云服务基准测试(YCSB) [Cooper等人2010],运行在一对专用的负载发生器机器上,在负载发生器和被测机器之间有一个专用的千兆以太网。在每个负载生成器上,我们在目标机器上为每个Redis实例运行一个YCSB基准测试实例。所有YCSB基准测试实例同时启动,并进行了调优,以执行一些操作,这些操作将导致大约30秒的运行时间。我们还将recordcount增加到32000,以完全填满原型的内存限制。
YSCB由几个工作负载组成。我们展示了Cooper等人[2010]给出的工作量A的结果。这个工作负载是一个更新频繁的工作负载(50/50读和写),它使用zipfian分发来选择存储中的记录。
我们还包括了一个基于linux的Redis配置来进行比较。我们使用Linux 3.13.0,在运行时使用/sys/devices/system/cpuX/online配置为使用1到8个内核,每个内核有一个Redis实例。
5 结果
5.1 微基准测试
我们从16次乒乓球基准测试的1秒运行中收集结果,并计算平均值和标准差。
5.2 单核IPC微基准测试
单核乒乓球的结果如图1所示(前面已经讨论过了),显示了无争用锁定成本:在x86上,BKL的开销约为10%,细粒度锁定和事务的开销约为30%,而在ARM上,这一数字约为两倍。
较高的同步成本与ARM处理器的部分存储顺序内存模型有关。它需要内存屏障(dmb指令)来保持内存访问顺序。根据我们的经验,屏障的成本从6个循环到19个循环取决于微架构状态。我们的BKL实现在这个基准测试中执行6个障碍,而对于细粒度锁定,则需要16个障碍。这些障碍解释了大部分的开销。
正如第2节中提到的,细粒度锁定的巨大成本提供了尽可能长时间坚持使用BKL的动机,即使验证可处理性不是问题。
5.1.2 多核IPC微基准测试
我们在两秒钟的热身时间内运行多核乒乓球,然后在一秒钟的间隔内采样总IPC,以获得每秒的总IPC往返吞吐量。我们重复每个基准16次,并报告图2中的平均值和标准偏差(作为误差条)。
在图中,x86的core-counts > 4对应于超线程的使用:Cores= 4+i意味着i个核启用了两个硬件线程,而其余的只使用一个硬件线程。
请注意,对于单核情况,这些吞吐量数字略小于图1的倒数,因为后者是纯粹的最佳内核时间,而吞吐量数字包含最少的用户代码。
这里的“none”情况对应于理论上的最佳情况(使用不安全的无锁实现)。正如预期的那样,这种情况在两种架构上都可以很好地扩展,尽管超线程比实际内核增加的吞吐量要少一些。
细粒度的锁也可以很好地扩展,因为在这种情况下锁是不满足的。由于锁的开销,吞吐量有所降低(与基线相比)。
在x86上,我们观察到RTM的行为与细粒度的锁定相同。这也是我们所期望的:正如Section 3.3.1中解释的,RTM在逻辑上是细粒度锁定的极端情况,根据图1,基线锁开销与细粒度锁定相同。
BKL变量跨所有可用的硬件线程序列化IPC路径,并且几乎没有可用的并行性来利用可用的硬件。它的性能在3核之后趋于稳定。
在ARM上,我们看到了类似的行为,除了细粒度锁定的更高开销是很容易看到的,而且在稳定之前,BKL变体的性能超过了细粒度锁定,达到3个内核。
请记住,这个基准测试度量的是在用户级别没有完成工作的病态情况,它代表了BKL不现实的最坏情况。
5.2 Macrobenchmark
我们在x86平台上运行Redis基准测试。对于每个内核变体和内核数量的组合,我们运行YCSB三次并报告平均值和标准差。我们检测内核在空闲循环中记录空闲时间,以获取每次运行的CPU利用率。
图3显示了不同核数的平均吞吐量。标准偏差显示为(除了Linux几乎不可区分的)错误条。
该图显示吞吐量独立于内核变量。进一步的研究表明,整个系统的吞吐量受到网络带宽的限制,并且所有核都有显著的空闲时间。
为了比较类似吞吐量的效率,我们在图4中显示了标准化的“Xput”值,我们将其定义为吞吐量除以所有核心的平均利用率。这表示在固定处理成本下的吞吐量,因此是在不同负载的处理器上比较的正确值。
我们可以看到,所有的seL4变体的性能都非常相似。BLK内核比其他seL4变体的内核略有下降(同样,仅仅在错误条之外),但并没有显示明显争用的性能悬崖。结果表明,对于8路并行性(甚至更大的并行性),锁的选择基本上与性能无关。
6.相关工作
编写并行和可伸缩的代码是一个几乎和计算本身一样古老的主题。Cantrill和Bonwick[2008]为并发软件提供了一些历史背景和动机,以及解决编写高性能和正确的并发软件的困难的智慧之言。我们遵循他们的建议,避免并行处理复杂的软件(例如,分割BKL),因为我们的数据表明这是没有根据的。
最近的补充工作评估了各种同步原语(David et al. 2013)在多核处理器上的可伸缩性。作者强调,可伸缩性是硬件的一个功能,当访问被限制在具有统一内存访问的单个套接字时,可伸缩性最好——这正是我们感兴趣的领域。
TxLinux [Rossbach et al. 2007]使用硬件事务性内存来实现cxspinlocks,这是一种合作自旋锁和事务的组合,能够支持设备I/O和嵌套。小型微内核既不需要,也不需要,因为I/O是在用户级,而且可以设计成避免复杂的、嵌套的、细粒度的锁。
使用Intel TSX的Linux补丁已经可用[Kleen]。据我们所知,没有任何性能数据被公布。删除现有的细粒度锁定并不能降低内核的复杂性。我们省略了整个微内核,在保持简单性的同时提供了良好的性能。
7.结论
我们的初步结果表明,对于同步访问微内核中的共享状态,大内核锁仍然是一种很有吸引力的方法,至少对于硬件共享缓存和中等内核计数来说是这样。在我们现实的(但困难的)基准测试中,更复杂的锁定方案无法展示性能优势。
虽然这个结果与可伸缩性的传统说法不一致,但它从根本上来说并不令人惊讶,也不局限于内核代码。只要满足两个条件,一个大的锁可以很好地扩展:(1)锁获取和保持时间很短(即内核间缓存延迟和临界区很小),(2)相互锁时间(即并发执行阶段)比较长。更令人惊讶的是,这些条件可以在OS内核中得到满足:在实际负载下,单个内核无法满足这些条件,但设计良好的微内核可以满足这些条件。
我们打算通过在ARM上运行宏基准来进一步探索这个领域,也可以在核心数更高的密集平台上运行宏基准。我们可能需要限制集群的大小,并将其转移到集群的-多内核设计,甚至在大型共享缓存多处理器上,这将是一个值得探索的有趣权衡。