1 问题背景
如今的移动设备面临着巨大的工作量,这些工作量来自于一些非交互应用,类似于图片渲染等后台进程。同时,移动设备为了能够达到较高的能效, SoC 的架构也在不断地发生演变,现在越来越多的 SoC 使用硬件异构和非一致性的方式,从而在能效和性能之间取得平衡。这样的一个趋势是由于微任务以及架构的能量消耗的特性所决定的,下文将一一讲述。
对于微任务来说,它具有以下几个特点:
- 对性能要求不是很高。如果没有等待的用户,微任务不会直接影响用用户的体验。
- IO 密集性。微任务需要通过频繁的 IO 操作与外部世界交换信息。在 IO 操作期间,处理器空闲是经常出现的。
- 需要内核以及用户空间的执行。微任务不仅会执行用户程序的逻辑,而且会调用多种系统服务,如设备驱动和页面分配。
- 频繁的调度执行。
- 高度影响电池的续航。有研究表明,后台的邮件下载使电池的续航减少 10 分钟。
对于微任务的能量消耗的特性,有以下三个:
- 供电状态的进入和退出对电池的消耗很高。强大的处理器核心在大多数时间内都是非活跃的状态,周期性的微任务不可避免的时不时的唤醒它们。
- 高昂的空闲能量消耗。强大的处理器核心经常有短暂的空闲,这些空闲期间对能量的消耗很高。
- 过剩的性能。
此外,对于架构来说,一致性的异构 SoC 的全局缓存一致性是高能效的一个瓶颈,原因如下:1)统一的硬件一致性机制限制了架构的非对称性;2)一致性互联自身也会消耗大量能量;3)在相同的一致性域中的核可以会受类似的热约束。对于非一致性的异构 SoC 来说,硬件一致性只在域内存在而不在域间存在。
综合上述的原因,现在移动平台的 SoC 逐渐呈现出异构和非一致性的特点。但是,这样的趋势也给维护一致性以及编程带来了新的挑战,尤其是编程。为了能够充分利用异构架构的特点,需要将一些服务或者操作交给弱域来执行;但是,要和现有的软件适配,最好的解决方式还是复用现有的操作系统服务。在已有的研究中,大多都采用了非常激进的方式,例如将操作系统服务固定在一个域内,或者是将操作系统服务分布在多个域内。这两种做法都会遇到前文微任务特点中所描述的问题,而且不能达到预期的性能要求。
当在多个一致性域中运行复用的操作系统服务时,操作系统的状态也会分布在多个域中。软件必须显示地同步这些操作系统的状态。这会对软件开发和操作系统工程带来极大的麻烦,因为对于绝大多数的应用程序以及应用程序的开发者来说,他们都认为操作系统提供的独立的系统镜像,而且操作系统的软件会认为有一致性的系统状态。此外,同步状态也会对性能造成一定的影响。
2 解决方案
基于 1 节中所提到的背景和问题,作者提出了 K2 移动操作系统。不同于前文提到的完全不共享(只在一个域内提供操作系统服务)和完全共享(分解操作系统服务在多个域中),K2 在两者中取了平衡,尽可能地共享操作系统服务,而且可以做到以下几点:
- 保留现有的编程模型,并且提供了一个为人熟悉的抽象—— NightWatch 线程,用来分发用户工作流到异构域中。
- 复用内核服务,也复用内核资源。
- 通过缓解域间争用来维持现在的操作系统的性能:它创建核心操作系统服务的独立实例并适当协调它们。这样的不对称性原则,有利于提高强域的性能。
下文介绍 K2 的具体实现。
2.1 share-most OS model
为了达到上述提到的几个目的,作者提出了 share-most
操作系统模型。该模型透明地维护拓展服务的状态一致性,协调分离的主要服务,并且阻止单个进程的多个域的并发执行。
- 透明地为拓展服务共享状态:这些服务的使用频率较小,是操作系统代码的主要部分而且迭代很快。这些特征导致了很难人工地将他们转移到多个域上,域间冲突的概率也很小。所以这些服务更应该由操作系统来维护他们的状态一致性。
- 独立实例化主要服务:对操作系统的主要服务,为每一个域创建独立的实例,实例之间不共享内部的状态。这些服务会被频繁的调用,例如内存管理,在域间共享很容易发生冲突,从而影响性能。独立实例化主要服务这个方面引出了两个问题:什么时候以及如何协调两个相同服务的独立实例?怎么最小化对现有的内核服务代码的修改?
- 避免单个进程在多个域中并发执行:单个进程在多个域中并发执行,很容易造成冲突。为了解决这个问题,K2 做法是推迟微任务的执行。这种做法之所以可行是因为强域处理器并不会长时间保持饱和状态,而且推迟的也是相同进程的微任务。
2.2 操作系统架构
操作系统的架构如上图所示。操作系统微用户层的应用程序提供了两种线程:一种常规线程,另一种 NightWatch
线程。这使得应用程序的开发者需要自己对他们的应用程序进行粗粒度的分解,将性能要求高的部分划给常规线程,而将轻量的任务交个 NightWatch
线程。常规线程会把执行交给强域,而 NightWatch
线程会将执行交给弱域来执行。
在 K2 操作系统层面,从上图中可以看出,它将内核服务分成了 3 种,分别是影子服务、私有服务以及独立服务。三种服务的区分方式如下:
- 影子服务:管理平台资源、低到中等的性能影响,被称之为影子服务。
- 私有服务:对于那种处理器定制化的以及管理特定的平台资源服务,我们将其视为私有服务。对于有复杂的执行过程并且极少使用全局操作的服务,我们也将其视为私有服务。
- 独立服务:具有很高的性能影响的服务,我们将其视为独立服务。
[!NOTE]
独立和私有在中文层面理解感觉有点类似,但是这里的描述是有区别的。独立表示每个域都有一份这样的服务,这些相同服务之间独立。私有就是表示这个服务是私有的,其他域没有这个服务。
为了构建 K2,作者使用了两套编译,分别编译成 ARM 和 Tbumb-2 架构。为了桥接处理器之前的异构差异,作者使用了自动化脚本。编译后的两个内核,共享的内核对象都有相同的加载地址以及共享函数的指针。编译时静态地将 blx
重写成未定义的指令,因为对于 Cortex-M3,执行 Cortex-A9 会使处理器陷入不可恢复的状态,而未定义的指令是可以恢复的。
3 K2 组件
在 K2 操作系统中,实现了 3 个主要组件,分别是内存管理、中断管理以及调度。其中,内存管理是最为重要的。
3.1 内存管理
K2 为每个内核都创建了统一的内存空间,这些空间都采用直接映射的方法。为了管理多个内核的虚拟内存,K2 规定了以下约束:
- 内核之间共享的内存对象在两个内核中必须具有相同的虚拟地址。
- 对于每个内核,线性映射的假设适用于整个直接映射内存。
- 最大化连续的物理内存。
K2 的内存地址分布如下图所示:
从图中可以看出,内核直接映射部分,前面一段是本地内存空间,后面一段是全局内存空间。而在本地内存空间段,前一部分是给影子内核的,后一部分是给主内核的。原因是 K2 会避免在主内核中有内存空洞。
在末尾处还有内存的暂时的映射。这部分主要用于下述几点:1)访问 IO 设备;2)访问额外的内存页面。当今的移动平台 64 位机逐渐兴起,需要访问的页面越来越多。
3.2 气球驱动
内存分配以及释放是被频繁调用的一个模块,为了避免频繁内核之间的通信,采用了 share-most
模型的独立实例化服务。使用气球驱动成熟的内存管理机制来动态共享整个内存池的空闲页面以及最小化物理内存碎片。K2改进了虚拟机的气球驱动程序的想法,并基于 Linux 的连续内存分配框架实现,用于控制各个内核可用的连续物理内存量。内存管理如下图所示:
气球驱动其实是一个伪设备驱动,主要任务是占用连续的物理内存,以使内存不受本地页面分配器的影响。气球驱动有两个原语操作,充气和放气。
- 充气:气球驱动从内核中分配一个页面块,将页面从页面块中驱逐,并将所有权转移给 K2。
- 放气:从本地页面分配器中释放一个页面块,并将所有权从 K2 转移至本地内核。
K2 提供了元级别管理器来决定什么时候拿取和提供页面块。管理器在每个内核中都放置有探针,通过硬件信息来感知内存压力。当内存压力增大的时候,管理器就会释放页面块来缓解内存压力;当整个系统处于消极地释放页面块的时候,管理器就会进行充气操作,主动来回收页面块。
K2 通过以下 3 点来优化内存管理模块:1)减少域间通信;2)最大化连续的内存空间;3)增加成功回收页面的机会。
3.3 软件一致性
K2 通过分布式共享内存(DSM)为影子服务提供透明的序列一致性。为此,K2 制定一套简单的标准的两状态协议,对于每一个共享页面,都有两个状态之一,Invalid
或者是 Valid
。当页面为有效的时候,内核可以执行读写操作;而当是无效状态的时候,内核就必须发送 GetExclusive
的信息给其他内核,来获取页面的所有权。其他内核收到 GetExclusive
的消息之后,就会使其本地的缓存的数据都置为失效状态,然后回复一个 PutExclusive
的消息。之后,前者就可以访问内存了。这个过程的流程图如下:
K2 DSM检测对共享内存的访问如下:当页面从有效状态转变为无效状态时,DSM会将相应的页面表条目修改为无效,并处理随后对页面的访问触发的页面错误。错误处理对于进行内存访问的操作系统代码是透明的。
为了保证性能,作者直接使用了 OMAP4 上的硬件邮箱完成一致性通信。每条信息都是 32 个比特,构成方式如下图所示。由于消息具有序列号,
为了避免一致性交流出现死锁,通信必须和请求者同步:这是因为无法休眠的中断处理程序可以访问共享状态并从而启动通信。因此,当请求者内核发送 GetExclusive
消息时,它会旋转等待,直到目标内核发送回 PutExclusive
消息。出于同样的原因,处理 GetExclusive
也必须避免休眠,例如,它必须使用原子内存分配。更重要的是,处理 GetExclusive
必须避免访问共享状态,这可能会引发新的页面错误,从而导致内核之间的无限请求循环。
为了优化内存占用,K2 允许非共享的区域可以以更大的粒度进行映射(1MB 或者是 16 MB),这两个大小也是 ARM 架构所支持的。
3.4 中断管理
K2 要求中断处理只能被一个内核所处理,为了提高性能和能效,K2 以以下的方式在内核之间协调中断处理:
- 如果强域处于非活跃的状态,则不将其唤醒,交给弱域处理中断。
- 如果强域是活跃的状态,则将中断交给强域处理。
3.5 NightWatch 线程
NightWatch 线程固定在弱域上;创建后,NightWatch 线程将进入影子内核运行队列进行执行。执行遵循以下原则:当且仅当同一个进程的所有常规线程暂停时,才会调度 NightWatch 线程。那么,当 NightWatch 在执行的时候,来了一个常规线程呢?这个时候就需要抢占,抢占的流程图如下:
对于恢复 NightWatch 的执行,主内核会发送 ResumeNW
信号,收到消息后,影子内核会从给定进程的所有 NightWatch 线程中删除标志,并在未来的调度中考虑它们。
参考文献
K2: A Mobile Operating System for Heterogeneous Coherence Domains