一、概述
本文以物联网操作系统面临的碎片化问题为主题,从CPU、外设、组件与接口四个方面出发,阐述物联网操作系统面临的挑战以及一些设计理念。以总分1.0进行度量,我认为整个物联网系统的碎片化指数如下:
从根源上来说,物联网系统的碎片化来自应用需求。这些需求的维度包括:成本、功能、性能、启动速度、功耗、实时性、安全性等等,甚至还包括编程语言与接口。有些需求是相互冲突的,比如成本低与功能强。因此我把应用的碎片化指数定义为最高的1.0。
形形色色的物联网应用需求无法用单一硬件架构满足,这导致了底层硬件的碎片化。其中尤以外设最为严重,我认为它的碎片化指数为0.9。通过操作系统的抽象与封装,在一定程度上可以降低硬件碎片化带来的影响,比如,CPU种类繁多,但经软件封装后可以提供较为一致的接口,所以上图中CPU核的碎片化指数为0.4,但CPU抽象层的碎片指数只有0.2。
为了满足应用在不同维度上的需求,物联网操作系统从接口到组件到内核都出现了较为严重的碎片化。同时,操作系统自身版本的演进也进一步带来了碎片化,不同的用户可能在使用不同的版本,甚至有的用户还在按自己的需求独立演进。
本文也将对比PC硬件平台/桌面操作系统,分析它们在面对碎片化问题时采取的策略。
二、CPU与操作系统内核
2.1 CPU的碎片化分析
市场上存在五花八门的CPU,它们各具特色。CPU通过数字电路实现,实现的依据是CPU的架构设计文档,主要是指令集架构(ISA,Instruction Set Architecture),分为两种:
(1)精简指令集(RISC ,Reduced Instruction Set Computing)。它只提供最基本的指令,通过组合多条指令实现复杂功能。该架构的CPU实现简单,功耗低。
(2)复杂指令集(CISC,Complex Instruction Set Computer),提供尽可能强大的指令。该架构的CPU实现复杂,性能高、功耗也高。
在物联网领域绝大部分都使用精简指令集架构的CPU,比如ARM、MIPS、RISC-V、CSKY,且ARM架构处于绝对垄断低位。同时,在IP核授权的产业合作模式下,一种CPU架构被应用到大量种类的物联网芯片上。
每种CPU架构都主要包含如下三个部分:
(1)指令集与寄存器。指令集包含了跳转、运算、位操作等指令,不同的架构提供了不同的指令。同时为了减少指令占用的内存空间,一些CPU架构设计了更紧凑的指令,比如ARM的thumb指令,RISC-V的压缩指令。精简指令集架构的一个显著特点是,CPU只能操作寄存器中的数据,不能直接操作内存中的数据。它提供了load和store指令,用于从内存加载数据到寄存器或把寄存器数据写到内存,即所谓的load/store架构。相比复杂指令集CPU,精简指令集CPU的寄存器个数更多,通常为16个或32个。
(2)异常与中断。异常是由指令触发的同步事件,包括无效指令、访问非法地址、系统调用等。中断由外设触发,用于处理异步事件。精简指令集架构的CPU通常把中断看作异步异常,中断和异常按相同的流程处理。CPU架构的异常与中断部分包括异常向量表、异常使能与禁用、异常进入与返回、优先级配置等。
(3)浮点。硬件提供的浮点计算能力相比用软件实现效率更高。CPU架构的浮点部分包括浮点指令、浮点通用寄存器、浮点控制寄存器等。
虽然物联网芯片基本上都使用精简指令集架构的CPU,但每种架构都有自己的指令编码、寻址方式、异常流程,有自己特有的状态寄存器、控制寄存器,也设计了特有的ABI规范。如果开发者需要面对这些繁杂的细节,不仅开发门槛大大提高,开发的周期也会拉长。C/C++语言及其编译器已经从语言级别实现了一层抽象,但这显然还不够。
2.2 CPU抽象层与内核
CPU在运行时先从内存读取一条指令,然后解析指令并执行相应的操作。这个过程以指令为单位不断重复。当中断发生时,跳转到中断入口处,处理完中断后再返回被中断打断的位置。也就是说,从CPU核的角度看,只存在两条执行流:一条是主执行流,一条是中断执行流,且两者分时运行。
传统上的裸机编程就是按CPU的这种运行特点来的,整个应用程序主要包含一个大while循环和一个中断处理入口,分别对应CPU的主执行流和中断执行流。这种编程模式把所有应用的逻辑都放在一个大while循环里面,这不适合开发复杂应用,且应用逻辑只能按顺序执行,对系统的实时性也有影响。
CPU在运行时,它的状态寄存器、程序执行位置寄存器PC(Program Counter Register)、栈位置寄存器SP(Stack Pointer Register),以及一些通用寄存器和浮点寄存器构成了它的执行快照,称为CPU上下文。我们若能把CPU执行流的上下文保存下来,那么可以用该上下文恢复执行流。
基于此,操作系统研发人员发明了软件并发机制——多线程。其原理是,在系统中创建多个线程,每个线程是一个独立的执行流,通过任务切换分时共享CPU。当前线程在CPU上运行时,把CPU上下文保存起来,该上下文也是当前线程的上下文,作为线程私有数据保存在线程中,然后把另一个线程的上下文恢复到CPU上,以恢复另一个线程的运行。这样当前线程就把CPU让渡给另外一个线程了,以实现多线程并发运行。下图是线程1把CPU让给线程2的一个示意图:
有了多线程机制后,编程模式有了根本性的改变。应用可以拆解为多个逻辑单元,每个逻辑单元由一个线程来执行。同时,可以创建一些系统后台任务,比如软件定时器任务,为应用提供一些系统服务。
多线程只是软件层面构建出来的一个概念,底层CPU在某个时刻仍然只能执行一个线程,这就涉及到一个问题:让哪个线程占用CPU?为了解决这个问题引入了调度算法。在物联网操作系统上一般采用基于优先级的时间片轮询调度算法。应用逻辑拆分为多个线程后,还涉及到一个问题,多个线程之间如何协调?比如两个线程要同时访问一段内存数据时,如何保证数据的完整性。为了解决这个问题引入了同步与互斥机制。更进一步,为了方便地在线程之间传递数据,增加了通信机制。整个内核架构如下图所示:
CPU抽象层实现了CPU能力的抽象,主要包括CPU初始化、异常进入与退出、开关中断、Cache、MMU、任务初始上下文构建、任务上下文保存与恢复,以及提供等接口。把不同架构CPU在指令、寄存器、异常、浮点等等上的差异隐藏起来了。基于该抽象层,内核实现了线程、调度、同步与互斥,以及通信功能。上层应用通过内核提供的接口使用CPU能力。
从本质上来还说,不管是哪种架构的CPU,都是为了提供计算能力。因此都可以抽象出相同的接口。包括多核架构的CPU也可以抽象出和单核相同的接口,只不过在多核处理器上同一个时刻可以运行多个线程。
基于线程模型的软件抽象无疑是成功的,它不仅隐藏了硬件实现的细节,还为硬件赋能:每个线程不仅仅只是一个简单的执行流,它还有生命周期与状态。比如在AliOS Things物联网操作系统上支持就绪、阻塞、睡眠、挂起、删除五个线程状态。应用程序可基于线程能力更方便地实现应用逻辑。AliOS Things上的线程状态流转如下图所示:
嵌入式操作系统都采用类似的方法抽象CPU能力为上层提供统一接口,这有效解决了CPU碎片化问题。有些嵌入式操作系统,像FreeRTOS和uC/OS,它们主要就是提供了CPU部分的抽象。
三、外设与板级支持包(BSP)
在物联网系统上,外设是碎片化的重灾区。这部分将对比PC平台,分析物联网系统外设碎片化的原因与挑战。
3.1 PC平台的外设与总线
PC机也会连接五花八门的外设,比如不同性能的显卡、声卡,不同功能的打印机,以及不同协议的通信设备。所以,PC机上的桌面操作系统也会遇到碎片化的问题。但不同硬件配置的PC机,只需安装同一个操作系统,最多额外再安装几个设备驱动即可,并不需要为其定制操作系统。这是如何做到的呢?
PC平台上的外设主要通过PCIE和USB总线进行连接。下面以PCIE总线为例进行阐述。PCIE总线拓扑示意图如下:
设备挂载在PCIE总线上,每条总线有一个编号。一条总线上最多可以挂载32个设备,挂载位置从0编码到31。每个设备最多可以有8个功能,从0编码到7。总线可以通过PCIE桥扩展出新的总线,所以整个PCIE总线体系是树形结构。
在系统初始化时将遍历总线,发现并识别设备类型,这个过程称为设备枚举。该操作以深度搜索算法遍历整个总线,依次组合Bus/Device/Function三个编号,尝试从该总线位置读取厂商号vendor ID。若读到的值为0xFFFF,说明该位置无设备,否则该位置存在设备,读到的值为设备的厂商号,进一步可从设备的配置头读取设备号Device ID。系统根据vendor ID/Device ID可以确定设备的类型,并加载指定驱动。
概言之,PCIE总线具备找到外设并判断外设类型的能力。基于该能力,操作系统可以动态加载合适的驱动。
3.2 虚拟文件系统(VFS)
基于设备总线构建的设备框架实现了外设的自动识别与驱动的自动加载,可以有效应对硬件平台外设碎片化带来的问题。然而,每种外设提供的能力都不同,这将导致提供给上层应用的接口出现碎片化,如下图所示:
Linux上提出了“一切皆文件”的设计理念,由虚拟文件系统(vfs)抽象外设能力向上层提供统一的open、read、write、ioctl接口。其中open接口用于打开设备,对应设备初始化,read接口对应设备的数据输入,write对应设备的数据输出,ioctl用于控制设备,比如设置模式、读取设备状态。这样上层应用可以使用统一的接口访问不同的设备了。
当然,设备毕竟是有差异的,外设的功能细节其实都被集中到ioctl函数中了。该函数的原型设计如下:
int ioctl(int fd, int req, ...);
其中,fd是文件描述符,在调用open函数打开设备时返回。req是操作的命令码,用于表示不同的操作请求。可变参数部分用于传递命令参数,为了便于传递多个参数通常会设计一个结构体。每种外设都需要根据各自的特定需求设计一套命令码以及对应的参数结构体,像复杂一点的显卡,ioctl命令可能多达上百个。
3.3 物联网系统上的外设与BSP
出于成本与功耗的考虑,物联网芯片通常把CPU和外设集成在一个芯片上,即SOC(System-on-a-Chip)或MCU(Microcontroller Unit),且不同的芯片常根据自己的特色集成了不同的外设,这种集成没有统一的标准。用于集成外设的总线通常不具备设备枚举能力,也无法读取设备寄存器判断设备类型。因此,在物联网硬件上开发时,只能查询芯片手册了解芯片上有哪些外设。同时,芯片厂商在集成外设IP核的时候已经为外设分配好IO地址了,开发时查询芯片手册获得该地址,驱动用该地址访问外设。
在这类硬件平台下,只能针对具体芯片定制操作系统了。物联网操作系统的开发者为了尽可能降低碎片化,针对该问题做了很大的努力。一种设计策略是板级支持包(BSP,Board Support Package)。它是外设的抽象层,一种硬件平台对应一个。它为内核提供了统一的外设访问接口,确保内核本身与具体硬件解耦。物联网操作系统提供多个硬件平台的BSP,应用开发者根据自身需求选择使用其中一个,然后重新编译出操作系统镜像。如下图所示:
然而,硬件平台不可穷尽,且为了更好满足不同应用的需求,新的物联网芯片也层出不穷。尤其很多厂商为了满足自身应用的需求常定制硬件,在芯片之外增加一些外设。这种情况下,需根据BSP开发规范新开发一个BSP以支持新的硬件。
从PC硬件解决外设碎片化问题的经验来看,软硬件协同设计是一个比较好的思路。HaaS(Hardware as a Service)物联网设备云端一体开发框架提出的硬件积木化正是这种思路在物联网系统上的一个实践。其设计思想是,基于数量收敛的硬件积木搭建出满足各类物联网应用需求的硬件。传统上,通过软件的组件化满足不同硬件的需求。而HaaS的硬件积木是要通过软硬件的协同组件化满足不同应用的需求,从而解决物联网碎片化问题。如下图所示:
3.4 物联网操作系统上的设备接口
Linux桌面操作系统通过VFS层为应用提供了统一的外设访问接口。一些物联网操作系统,像RTThread,借鉴Linux的思想也实现了简化版的VFS组件,除了用于访问文件系统外,也用于访问外设。
AliOS Things除了支持VFS组件外,还提供了另外一种接口——HAL(Hardware Abstraction Layer)。它根据每一种外设的功能,分别为它们设计一组接口。所以HAL其实是一个接口集。它的优势是简单直观,易使用、易对接、易维护。但可扩展性较差,当出现新外设时需要设计新的接口。同时,一些从Linux上移植过来的应用,希望按Linux的方式使用外设,HAL就无法满足了。
虽然通过VFS可以统一外设接口,但在物联网设备上也会出现水土不服的情况。首先是对接设备工作量比HAL接口要大,尤其对一些简单的物联网外设,像GPIO,也要按VFS的规范进行封装。其次使用起来不够直观,比如像I2C设备访问时需要指定从设备的地址,这种情况下就无法直接用read和write接口简单访问了。
两种接口各有优劣,选择哪种接口要看操作系统的整体设计与定位了。当然也可以选择两种都支持,通过配置选择。下面我们来看组件和配置带来的碎片化问题。
四、组件与配置
物联网操作系统的一个特点是可裁剪性强,通过组件配置,应用只选用需要的组件。以灵活的配置来满足不同应用的需求,这也是解决碎片化的一个方法。这是一个好的思路,但在具体实施时也会遇到问题。
首先是组件内部的碎片化。针对物联网领域的第三方组件在设计时通常希望能在更多的平台上运行,主要考虑两类平台:(1)无操作系统,在该类平台上无需考虑多线程问题;(2)有操作系统,在该类平台上需要考虑多线程问题,因此依赖操作系统提供的接口。组件会提供一个对接层,需针对目标操作系统实现该对接层。有些组件基于标准接口开发,比如Posix,或者用C++语言写。如下图所示:
为了支持不同类型的物联网平台,三方组件通常提供了灵活的配置,比如占用的内存数量、使能的功能个数,以适用于更多的场景。越灵活意味着可配置的项越多。有些组件为了降低依赖,或提高易用性、性能还会实现一些基础功能,比如内存管理、校验算法,甚至C库。另外每个组件可能有自己的调试信息输出方式、接口设计理念等等。当所有这些设计不同的组件组合在一起的时候,操作系统在整体上就不一致了,功能冗余、配置方式、代码风格不同等等。当然,我们可以对三方组件做本地化的努力,但这将为日后升级将带来阻碍。
其次是如何配置。一种方法是集中配置,在一个或少量几个头文件中集中用宏配置。该方法比较直观,但由于物联网系统的配置项非常多,且组件之间存在依赖(一个组件选上后需要选上另一个组件)或互斥(一个组件选上后不能选上另一个组件),这将导致配置工作比较繁琐,用户很难配置出一个符合其需求的配置集。这种方法只适合小型的物联网操作系统,FreeRTOS采用了该方法。
另一种方法是离散配置,即把每个组件的配置信息放在组件内部。由于配置项分散在各个组件中,配置起来有难度,因此有些系统会利用类似于menuconfig的图形化配置工具。该方法能够辅助用户处理组件之间的依赖与互斥关系,即当用户选中一个组件时,它依赖的组件也被自动选上,与它互斥的组件被排除。然而当配置项多了之后,整个图形配置层级较多,用户配置起来也会遇到困难,比如要配置某个参数,若不熟悉的话,可能会像走迷宫一样找不到位置。大部分物联网操作系统,像AliOS Things、RTThread、ARM Mbed都采用了这种离散配置的方法。
配置方式处理得不当,最后的结果就是很多不是必须的组件都被配置进去了,失去了系统的可裁剪性。在Linux系统上,由于用户会跑各类应用,因此内核需要支持全功能,但若把所有模块都加载到RAM上,将导致内存浪费。为此Linux开发了动态加载功能,一些组件作为模块存放在外部硬盘上,当需要时才载入内存。相比静态配置的方式,动态加载更为方便。然而,有些物联网系统非易失性存储空间也很紧张,甚至比RAM还小,无法存下所有待加载的组件。
一种发展方向是收缩应用面,往垂直领域发展。这样各类组件不再需要为不同类型的应用提供多种配置,还可以针对垂直领域的应用需求做针对性的优化,不仅提高了易用性,也增加了性能。当然,更好的方式是打造平台化能力,这要求操作系统具备良好的组件“分”“合”的能力。在保持组件独立性的前提下,能快速组合。比如,离散+集中的混合配置方式,离散配置方式生成一个集中的配置文件,该配置文件就是一个垂直方案。既简化了配置,又可实现配置的复用。如下图所示:
五、接口与框架
物联网操作系统的接口主要包括两类,一是针对应用开发者的API,它代表了操作系统提供给用户的能力;二是针对BSP开发者的平台对接接口,它代表了操作系统所依赖的硬件能力。在物联网领域,应用开发者和BSP开发者很有可能是同一个人。
物联网操作系统通常会提供一套原生的C接口,使用原生接口的好处是性能高、占用的资源少,这有利于那些资源受限的平台。但它不是一套通用的标准接口,增加了应用开发者的学习成本,以及移植成本。当把其他系统上的应用移植过来时需要修改应用调用的系统接口。移植的工作量因系统差异而不同,比如若遇到没有相同语义的接口,则需调整应用代码或新增一个接口,若两个系统任务优先级值的定义不同,需要调整应用中每个任务的优先级。
Linux系统上的应用基于Posix接口开发,这是一套标准接口。一些应用开发者希望自己只维护一份代码,它既能在Linux跑又能在物联网系统上跑。为了方便这些开发者,物联网操作系统,比如AliOS Things,提供了Posix接口支持,这大大降低了把Linux应用移植到物联网操作系统的工作量。带来的代价是增加Posix层的开销。
物联网应用正朝着更加智能化的方向发展,一些AI组件,比如语音识别、手势识别,以及图形引擎,常用C++语言开发,因此对物联网操作系统提出了支持C++语言的要求。一个特例是ARM Mbed物联网操作系统,它把整个系统用C++进行了封装。
为了便于不熟悉C语言的开发者开发物联网应用,HaaS物联网设备云端一体开发框架提供了JS与Python语言的支持。
原生接口满足内存占用小的需求,Posix接口满足复杂应用的需求,C++语言及其库提供的接口满足面向对象开发的需求,JS与Python满足轻量级开发的需求。四五套接口看起来似乎很碎片化,有一种用碎片化应对碎片化的感觉。但我认为判断软件碎片化的标准应该是:为了满足不同应用的需求,需要增加的代码量或调整代码的量(包括配置项数量)。这四五套接口能够应对绝大部分应用的需求,并不需要因为应用的变化而增加接口或调整接口,所以我认为这部分的碎片化是低的。
BSP开发者通过实现硬件对接接口,把物联网操作系统移植到目标平台上,该套接口的设计决定了物联网操作系统支持一个新平台的工作量,所以它也是影响物联网操作系统易用性的关键因素之一。
我曾把FreeRTOS、RTThread、ARM Mbed三个物联网操作系统移植到同一个开发板上并跑通性能测试用例。这个过程中花时间最少的是FreeRTOS,最多的是ARM Mbed。两个系统的差别是,ARM Mbed的硬件对接接口背后有一整套极为完善、复杂的框架,想要适配一个新的平台需对该框架的使用有一定的了解,而这个过程需要不少时间。FreeRTOS没有设备框架,内核直接调用到硬件对接接口。可见,硬件对接接口背后的框架是一把双刃剑,用的好是助力,用的不好将成为掣肘。它需要具备如下三个特性:(1)抽象外设功能,整合通用操作,以降低对接的工作量;(2)具备良好的可扩展性;(3)使用的边际成本低,虽然一开始接触比较花时间,但后续使用可以减少大量时间。
如果不具备上述特性,那么硬件对接接口背后的框架将变成一场灾难。用户想要添加新功能不知道从何处入手,想了解背后的机制需要了解一整套框架,学习成本、维护成本、使用成本大大增加。同时,在外设碎片化需求的冲击下,框架本身逐渐被瓦解,变得越来越碎片化。
六、总结
相比硬件,软件的最大优势是边际成本低,甚至可以趋于零。然而,碎片化问题将大大增加物联网操作系统的复用成本,阻碍物联网操作系统成为通用的设备端软件平台。处理得不好,不仅将使软件边际成本低的优势荡然无存,甚至还会使之成为高边际成本的软件怪物,比如一些针对特定平台的特殊处理成为在其他平台上应用的障碍。
在开发物联网操作系统时将面对多个决策维度,包括性能、功能、易用性、实时性、资源占用小等等。基于不同的设计决策,开发的代码差异较大。因此,务必要确保基于同一个设计决策开发,这样才能使整个操作系统具备良好的一致性。另外,文档是解决碎片化问题的一个重要辅助手段。