本文内容已用一种抽象的方式做成了视频,喜欢看视频的同学可以在B站上搜索“泊浮目”观看相应的内容。
《 Microkernel Goes General: Performance and Compatibility in the HongMeng Production Microkernel》论文中提到的一些性能优化的思路和方法是很有学习价值的,结合论文里提到的点,我做了个视频。为了方便大家观看,我梳理了这个文字版本。
读前提示:Linux是个通用操作系统,鸿蒙是特殊领域专用系统。在专属领域中,鸿蒙肯定会比Linux发挥得好。作为开发者,我们需要知道软件工程中的trade off,才不会被一些标题党带着走。
大家都知道Linux是个成熟的操作系统,那为什么还要重新造个鸿蒙出来呢?主要有两个原因:
- 在嵌入式、安全要求极高的场景(比如军工),需要利用微内核来构筑内核。减少内核中的功能,将不重要的程序隔离出内核,这样会让内核更安全、可靠。
- Linux是面向通用发展的,在一些特定领域(除了上述提到的,还有汽车、移动设备)去做领域适配并不合适。比如这个补丁前后花了10年才正式合入Linux。
那么什么是微内核呢?简单来说就是内核只保留最基本的能力,比如进程调度、虚拟机内存、中断等,把一些应用放到了用户空间,比如驱动程序、文件系统等。这样系统服务与系统服务之间是隔离的,单个服务出现故障或者完全攻击,也不会导致整个操作系统挂掉,提高了操作系统的稳定性和可靠性。
论文中举了一个例子:Linux的代码中,驱动和文件系系统有3000多w行,占到了代码库的80%,过去4年的1000个通用漏洞披露中90%来自于这部分的代码。
但是微内核会有性能问题。以文件系统为例,和硬件设备交互就需要频繁切换到内核态,这样会带来性能损耗。那鸿蒙怎么解决这类问题呢?我们就来解读解读论文里提到的优化点。
中文可以翻译成 同步RPC样式的IPC快速通道 吧。
这个章节里提到了IPC(进程间)的通信在鸿蒙的场景中很频繁。在过程中很容易消耗大量的内存,为啥消耗内存?举个最简单的例子,AB之间进程之间通信,中间是不是有个通道,要不要耗内存?进程之间通信的时候,不能把主进程做的事阻塞住吧,那是不是要起线程去做,是不是要一些栈内存?
那鸿蒙是怎么优化呢?针对上述的栈,鸿蒙搞了个栈池,进程服务能在里面复用就复用,不够的时候就扩,当然它也会记录每个进程扩的量,扩多了肯定不给扩了,不然内存爆了。
可能有同学好奇,什么叫Fastpath,我们以RPC FastPath为例,看下常见的优化:
减少上下文切换
- 直接调用:对于一些高频调用的服务,可以将远程调用转换为本地直接调用,避免网络传输和上下文切换的开销。
- 内联调用:在某些情况下,可以直接内联远程服务的逻辑到调用方,减少远程调用的开销。
缓存机制
- 结果缓存:对于重复调用且结果不变的服务,可以缓存结果,避免重复调用。
- 状态缓存:缓存一些状态信息,减少每次调用时的状态查询开销。
零拷贝技术
- 共享内存:使用共享内存来传递数据,避免数据从一个进程复制到另一个进程。
- 直接I/O:使用直接I/O(如DMA,Direct Memory Access)技术,减少数据在用户态和内核态之间的拷贝。
协议优化
- 紧凑的消息格式:使用紧凑的消息格式(如Protocol Buffers、FlatBuffers)来减少消息体积。
- 压缩算法:对消息进行压缩,减少传输的数据量。
多路复用
- 连接复用:保持长连接,避免频繁建立和关闭连接的开销。
- 批量处理:将多个RPC请求打包成一个批次进行处理,减少网络往返次数。
异步处理
- 异步IO:使用异步IO机制,减少等待时间。
- 事件驱动:采用事件驱动模型,提高并发处理能力。
- 预连接机制:预先建立好连接,减少每次调用时的握手时间和开销。
智能路由
- 负载均衡:通过负载均衡器智能分配请求,避免单一节点成为瓶颈。
- 就近接入:根据地理位置或网络拓扑选择最优的接入点。
硬件加速
- NIC卸载:使用支持卸载功能的网卡(NIC),减轻CPU负担。
- FPGA/ASIC加速:使用专用硬件(如FPGA、ASIC)来加速数据处理。
第二个是关于进程隔离级别的,有点像在传统的内核态和用户态之间加了一个中间态。
这一层适用于性能有要求而且已经验证的操作系统服务,当然这一层如果被攻击了,整个系统是要G的。
所以论文里提到,如果出现了新的攻击,鸿蒙是可以快速把IC1的应用退到IC2的。这也算是微内核带来的一个好处吧。
- IC0:IC0是核心部分,任何被攻破的IC0服务都可以任意读取和修改其他服务的内存。因此,将服务放置在IC0级别应当经过仔细验证,以避免核心内核被破坏。
IC1:适用于性能关键且已验证的操作系统服务。鸿蒙将内核地址空间划分为不同的域,并为每个服务分配一个独特的域(IC0/核心内核也驻留在一个独特的域中)。鸿蒙一些机制(ARM watchpoint and Intel PKS) 来防止跨域内存访问。此外,由于IC1服务在内核空间运行,它们可以执行特权指令。为了避免这一风险,鸿蒙采用二进制扫描和轻量级的方法来防止未经授权的特权指令执行。
- 具体实现是IC1服务之间(或到IC0)的IPC会进入核心内核中的一个gate,gate执行最小的上下文切换(仅切换指令和栈指针,不包括地址空间切换和调度),并配置硬件以切换域(只需要几个CPU周期)。这样的门无法被绕过,因为域切换需要特权指令。因此,IPC的开销被显著减少。如图4所示,与用户空间服务(IC2IC2)相比,它减少了IC1服务之间IPC延迟的50%。
- IC2:这个没什么特殊的。IC2适用于非性能关键的服务或包含第三方代码的服务(例如,Linux驱动程序),通过地址空间和特权隔离来强制实施
第三个叫Flexible Composition,则是参考宏内核的优化方式。将紧密耦合的OS服务合并,以减少高性能需求场景中的IPC频率(如下图左侧中间黄色部分,File System和Mem Mgr就合并了)。
我翻译成为基于地址令牌的访问控制。
这里面提到呢,一般微内核会将内核对象隐藏在内核后,通过权限控制访问。如果要对内核对象进行访问,就会涉及到内核态用户态的切换而带来的权限问题。而正是由于微内核最小化原则,某些内核对象(例如页表)的需要频繁地由内核之外的操作系统服务进行更新。
那鸿蒙是怎么优化的呢?每个内核对象被放置在HM的唯一物理页面上,然后通过权限配置只读、读写来映射给操作系统服务,避免总是经过内核降低性能。
这是一个关于Page fault的一个优化,那什么是Page fault?这里我们做一个简单的计算机基础知识回顾。
Page fault是指当程序尝试访问其虚拟地址空间中的一个页面,而这个页面并没有加载到物理内存(RAM)中时所发生的情况。当发生 page fault 时,操作系统会介入处理,并将所需的页面从磁盘上的交换文件或其它存储介质加载到物理内存中。
这个其实涉及到了计算机实现虚拟内存的关键机制之一,它们允许操作系统使用比实际物理内存更大的地址空间。在实际应用中呢,swap分区就是用来这个事情的。
那在鸿蒙的场景中呢。page fault会有一定的性能问题。主要原因在于从内核到分页器的额外往返通信。具体来说,在抛出页面错误异常后,内核会向分页器发出一次进程间通信(前面说到过这是一个OS服务),分页器检查地址并分配新的页面,然后返回内核更新页面表,最后回到应用程序。
总得看下来,异常处理是在微内核里做的,但是决策是在内核之外做的。那么鸿蒙做了一个改进,它在内核中保留了一个默认的页面错误处理机制——鸿蒙在内核中有段匿名内存的地址范围以及预先分配的物理页面(这段内存都是高性能程序要求的区域),那对于这部分内存,内核知道它可以立即映射到物理内存上,而无需进一步的决策过程。后面异步同步给内存管理器这个操作记录就行了。
如果页面错误发生在非性能关键区域(即不在内存管理器提供的匿名内存地址范围内),或者预先分配的页面已经被用尽,那么内核将不得不发送一个IPC请求给内存管理器来获取新的物理页面。这种情况下,内存管理器将根据其策略来分配新的物理页面,并通知内核进行相应的页面映射。
那总得来说呢,鸿蒙里面提到的性能优化手段还是比较精彩的。大多数基于work load来做出优化的,思路值得学习。
说完如何解决微内核的性能问题。我们继续讲解生态问题。
大家都知道写个操作系统比较容易,网上教程一搜一大把,但是如何让操作系统上的生态建立起来是个问题。
这里面做了个Linux ABI的兼容层,该层将所有Linux系统调用重定向到适当的OS服务。也就是说,其他Linux系统的二进制程序,理论上是可以直接兼容的。
而驱动这块,出于安全性的考虑是放在IC2的。但论文里面提到了一个有意思的方法,根据安全和性能要求,切分了驱动中的数据平面和控制平面。
对于驱动呢,鸿蒙会在IC1中另起一个程序,方便做隔离,两个程序之间是通过IPC来通信的,这两个程序分别来做控制平面和数据平面分离:控制平面负责管理、配置,并在较高层次上做出决策的部分;「数据平面则处理实际数据传输和处理任务的部分;前者对安全性要求高、性能要求低,后者反之。HM把设备驱动中「数据平面」(也就是IO操作)的部分,委托给IC1空间中的twin driver来完成,而控制平面操作留在LDC中。
总得来说,鸿蒙操作系统的诞生还是没有摆脱那句经典的话语——计算机世界总是在通用和专用之间摇摆。
当通用软件没法满足需求,而需求越来越多的时候呢,则会出现对应的专用软件。