自动驾驶软件架构之:中间件与SOA(三)

来源:焉知智能汽车 ,作者肖猛
2023-02-22
2306

04

用于自动驾驶的SOA中间件



4.1三个应用领域对SOA的推动



目前而言,SOA 架构在汽车软件中的应用也是刚刚开始。对SOA 在汽车领域的应用很多企业也是在摸索着前进。在这个过程中,有三大需求在推动这SOA的发展:


  1. 传统汽车电子电器架构的 SOA 化;
  2. 车载娱乐系统(IVI, In-Vehicle Infotainment) 与车联网需要SOA进行更广泛的应用集成;
  3. 自动驾驶的软件架构也适合以SOA的方式来组织。



这三个方向上有些需求是共通的,有些又有各自的领域的一些特点。这三个领域又各自有一些典型的中间件产品。

1.png
图4. 1三个应用领域对SOA的推动

Adaptive AutoSAR 有更多的汽车行业软件特点,标准体系完善,定义了SOA 的基础通讯协议 SOME/IP,对功能安全和信息安全有较多的考虑,但是产品价格昂贵。

车载娱乐与车联网领域,GENIVI组织提供了一套开源的技术栈,实现了从IDL定义到 SOME/IP 协议通讯支持的完整方案,理论上可以与 Adaptive 进行互操作。不过目前产品化程度一般。

ROS 是开源的机器人领域的中间件,自动驾驶系统把车当作一个大号机器人,就能借用很多机器人领域的成熟经验。所以ROS常被用于开发自动驾驶的原型系统。但是ROS一方面过于庞杂,一方面机器人应用毕竟是低速场景,用于自动驾驶的高速场景,也需要做很多改进。所以有中间件产品基于ROS进行改进,希望用于量产。基于ROS演进的产品也具备了很多SOA的风格特性,但是与Adaptive AutoSAR不能直接互操作性,需要开发定制的应用层网关。


4.2 自动驾驶对于中间件的要求


4.2.1 SOA 架构风格与自动驾驶系统的适用性



SOA 架构风格对于自动驾驶系统来说也是一个非常合适的架构决策。最直接的因素有两个:


  1. 自动驾驶系统中“多种传感器的感知和融合、复杂多变场景的规划决策、高实时要求的控制执行”涉及大量类型差异很大的计算,需要能被分解成不同的服务子系统,独立进化;
  2. 采用“发布/订阅”的通讯模式可以有效的降低各服务之间通讯的偶合性。


 
这两点正好对应于SOA架构风格中的“客户-服务器”风格和“基于事件/消息的发布订阅”风格两个架构约束。这两个风格解决的核心问题就是如何将复杂系统分解为多个软件组件并以低耦合的方式集成。从这个角度看,要达到这个目的有很多方法。比如,整个系统基于DDS 进行集成,每个服务拆解为不同DDS节点,只管收消息、处理消息、发送结果。这种方案,DDS 厂商是最开心的。

软件架构上满足上面两点足够支持一个自动驾驶系统的原型产品。但是作为量产的产品,需要考虑的问题就更多了。SOA风格的其它方面就能够提供较好的支持。

比如服务发现机制帮助确认需要依赖的其它服务是否就绪,服务代理帮助与车内其它系统进行交互,服务装配提供自动驾驶各软件模块在部署时的灵活性,并为通讯优化提供可能。服务监督提供整体系统的可靠性,可用性。


4.2.2 自动驾驶相关软件的特殊要求



SOA架构本身能够为自动驾驶系统的开发带来很多优势。但是自动驾驶系统需要的软件架构相对于其它软件系统依然有很多独特的要求。这些要求有些不属于SOA架构风格的一部分,但是可以在SOA架构体系中去解决;有些非架构的问题则需要单独去处理,比如特定算法的有效性。这里我们主要讨论前者。


高带宽的传感器数据通路要求



自动驾驶系统中,涉及到大量的传感器数据传输需求。最典型的是摄像头图像数据和激光雷达点云数据。

就目前自动驾驶系统中广泛运用的摄像头技术为例,一个800万像素的摄像头,在摄像头硬件接线是如果采用GMSL2 ,可以让图像捕捉达到约30帧的采集帧率,数据带宽已经在6Gbps 以上。

基于 SOA 架构,我们希望把图像数据的捕捉作为一个服务,图像数据“发布(Publish)” 出来,多个数据处理的服务去“订阅(Subscribe)”,获取数据并利用。数据处理的服务可以有多个,有的执行车道线和车辆识别算法,有的将图像连续帧编码为视频格式存储。这是典型的“客户-服务器”风格与“发布/订阅”风格的组合。

但是数据传输所需要的带宽,已经不是一般千兆以太网所能承接的了,必须使用共享内存进行数据传输加速。如果这几个相关的服务运行在同一个芯片的统一个OS上,共享内存还是简单的,如果是不同的OS(如 TI TDA2/TDA4),就涉及到跨 OS的内存共享。如果是在一个 PCB 电路板上集成了多个SoC主芯片,还需要能支持基于 PCIe 的跨芯片跨OS的内存共享,否则就只能限定某个SoC芯片只能处理接入到它的摄像头数据。

也就是说,一方面我们希望利用 SOA 带来的架构便利性,一方面我们希望在SOA 架构的框架体系内解决传感器相关的高带宽数据传输要求。

解决的办法至少有一下几种:


  1. SOA 的通讯协议采用 DDS, 某些 DDS 的实现已经内置了共享内存通讯,这是目前采用比较多的方式。有数据序列化动作,实现零拷贝有一定难度。优点是已经有成熟的实现。
  2. 基于共享内存的虚拟网卡,这个适合基于PCIe的跨SoC通讯。有数据序列化动作,实现零拷贝有一定难度。
  3. 与服务装配相配合,透明实现进程内、跨进程、跨主机的多种优化手段自动适配。后文详述。

高效任务调度与确定性执行



自动驾驶系统在运行中可以看作是一个“感知融合→决策规划→控制执行”的流水线在持续运转。其中涉及大量不同种类的算法接续执行、相互合作。算法对执行性能的要求主要有两点:


  1. 单个算法的执行速度尽可能快,从而让整个流水线的总延迟时间尽可能短
  2. 每个算法的执行时间的波动率尽可能小,从让整个流水线延迟的波动率稳定



总延迟时间越短,说明对车辆行驶干预的频率越高,越能支持高速自动驾驶;总延迟时间越稳定,越能够产生稳定的控制输出,车辆行驶越平稳,对突发事件能够及时响应。

总延迟长度取决与每个算法的执行延迟,数据传输的延迟、以及算法的编排,这些需要做针对性的优化。但对总延迟稳定性的影响,主要取决与对算法执行的调度。

在一个自动驾驶功能的ECU只执行几个固定场景下的功能时(如L1/L2),任务调度的重要性并不突出,因为算法的启动/停止的生命周期,流水线设置都是静态编排好的。一旦量产,就不再变更。但是进入域控制器时代,一个高算力自动驾驶域控制器,在不同场景下有不同的功能体现,涉及的传感器和算法都会有些差别。而且还可以通过 OTA 进行升级新的功能和场景支持。这种情况下对任务精细调度就比较重要了。
 
一种极端的任务调度方式是全部交给操作系统来处理。每个算法服务作为单独的进程存在,由操作系统去分配进程的执行时间。开发者能影响任务调度的方式只是设置每个进程的优先级。大多数自动驾驶原型系统的开发就是这么做的。这种方式的缺陷在于:


  1. 以“操作系统进程”为单位的调度粒度过大,无法对算法动作执行的时机做精准调度
  2. SOA架构风格可能会产生较多的服务数量,当操作系统中的进程过多时,进程切换的系统开销占用整体系统时间的比例过高。



优化方案可以使用“服务装配”风格将多个SOA服务装配到同一个服务容器进程中,为每一个服务分配一个“操作系统线程”。这会让上述两个问题得到一定的改善,调度的粒度缩小到线程级别,同一进程内的线程切换的开销会小于进程切换的开销。

无论是基于进程还是基于线程进行调度,都是依赖操作系统本身的并发机制,每一次进程切换或线程切换都是需要在操作系统内核态完成,CPU再切换到用户态执行用户的代码。进程数量或线程过多时会显著增加 CPU 在内核态执行(上下文切换)的时间,极端情况下会达到30%以上甚至更多。这意味着执行用户代码时间减少。
 
更进一步的优化是不依赖操作系统的并发机制,只是使用少数线程作为执行机制。每次算法的处理动作被封装成一个任务,由用户编写的调度机制来编排任务的执行。

比如以SOME/IP协议为例,每一次对 Method 调用的响应处理,每收到一个 Event 的响应处理都是作为一个任务。交由任务调度机制安排在合适的线程中执行。这些任务有很多种称呼,有的叫用户态线程,有的叫消息,有的叫协程或纤程。这样任务的切换完全在用户态进行,系统开销小。同时我们可以为每一个任务标注其调度需求,由调度器做更精准的调度。


软硬件异构平台的支持



用于自动驾驶域控制器的SoC芯片往往是高度集成化的,除了多核CPU外,还会集成DSP、NPU、FPGA或其它专用计算加速单元,有的还会集成MCU芯片。这是硬件上的异构。

此外,还有软件上异构。多核的CPU经过虚拟化之后,可以在不同的虚拟机上运行不同的操作系统。比如运行Linux来充分利用Linux社区丰富成熟的软件生态,运行QNX或 VxWorks 来达到更好的实时性。SoC内集成的 MCU单元还会运行RTOS系统。DSP和NPU还会有平台特定的访问机制。

用与自动驾驶的SOA中间件,最好能对服务实现屏蔽软硬件异构平台的差异性。一方面让异构平台都能够以SOA的方式交互,一方面让通用的功能逻辑能在不同硬件平台之间移植。

比如将DSP或NPU的使用封装在平台特定的库中,对外呈现为标准的SOA服务。在MCU的RTOS系统中也采用SOME/IP与外部通讯,RTOS上运行的任务对外同样呈现为SOA服务。


对自动驾驶开发阶段的支持



在自动驾驶系统的开发测试阶段,也需要中间件系统的支持。首先是数据的录制、回放、以及可视化。这在自动驾驶系统开发过程中尤为重要。因为不可能每个软件服务开发的时候都使用实车环境。SOA服务对外的数据交互如果能够被录制下来,软件开发的时候使用录制的数据进行回放,模拟与服务组件的交互,会在开发上带来很多便利。数据的可视化也为直观的理解数据提供了方便,更利于理解软件执行的结果,排查错误。ROS提供了 rosbag 工具进行录制和回放,RViz 工具进行数据可视化。这也是ROS被广泛运用的原因之一。

自动驾驶仿真系统是进行自动驾驶功能测试的重要工具,可以大大降低开发和测试的成本。中间件对仿真系统的支持一般不体现在中间件的核心架构上,而是与仿真系统对接的各种接口适配上。

不同的开发环境下,我们往往使用不同的开发语言。MCU开发以 C 语言为主,Linux/QNX环境可以很好的支持C++语言,测试人员更喜欢使用Python,为了更好的程序内存安全性,Linux端和MCU端都可以使用 Rust作为系统编程语言。如果中间件能够支持多种程序语言,并且无缝互操作,会给开发过程带来极大的便利。


4.3 构建适用于自动驾驶SOA中间件



前面论述了SOA中间件与自动驾驶系统之间相辅相成的关系。SOA中间件需要为自动驾驶功能做针对性优化,一方面能够让自动驾驶系统开发有一个坚实的基础平台,一方面在特定领域的深度应用,也能让SOA中间件的发展有更好的迭代演进。

Adaptive AutoSAR(后文简称AP)正在快速发展,是正在逐步成熟的SOA中间件标准。但是虽然AP的标准文档是公开的,但是几乎所有AP厂商的产品都价格昂贵,大多数开发者学习和使用还是有很大的门槛。GENIVI 组织提供了一套开源的SOA技术栈,也能跟AP平台进行交互,虽然不够完整,但是也可以用于构建SOA软件架构体系。

这一章将针对这两个技术路线做一些对比和技术展望,提出构建适用于自动驾驶的SOA中间件思路。


4.3.1 相关技术及产品介绍


4.3.1.1 POSIX 标准简述



首先简单说明一下POSIX 标准,尤其是嵌入式POSIX标准。因为车载SOA中间件以及基于此中间件的应用程序,需要一个明确定义的操作系统运行环境标准来界定其能在什么样的环境中运行。

POSIX表示可移植操作系统接口(PortableOperating System Interface of UNIX,缩写为 POSIX ),POSIX标准定义了操作系统应该为应用程序提供的接口标准。POSIX是一个标准家族,有一系列标准,隔几年还会有一些更新。本文重点说明与嵌入式系统相关的两个关键标准。

最主要的标准是 POSIX.1 ,其正式称呼为IEEE 1003.1,被接纳为国际标准名称为ISO/IEC 9945。2017年的版本[28],近4000页的篇幅定义了“基本定义、系统接口、Shell和实用程序、基本原理”四大部分。Linux 和 QNX Neutrino 都适配了此标准。

另一个是 IEEE 1003.13标准,定义为实时和嵌入式应用程序可以使用环境配置和API接口。该标准为实时和嵌入式应用定义了4个不同基本的配置,PSE51 为最小系统,也是最受限的系统。PSE52、PSE53、PSE54 逐步放宽限制,允许更多功能。

下图总结了这几个配置的功能范围。
 
2.png
图4. 2嵌入式POSIX标准配置对比

我们用 “AP平台”来代表按照AP标准规范实现的AP中间件产品,一般包括AP基础库和平台服务功能的实现,以单独的中间件产品形式存在。用 “AP应用程序”表示基于“AP平台”开发的应用程序。

需要注意的是,AP应用程序受限于PSE51API,但AP平台可能使用非PSE51 API,甚至是操作系统特定的 API。特定API的使用将由AP平台的实现者决定,并且不是标准化的[30]8.1.1。

因为AP应用程序受限于PSE51API,所以它没法调用网络,没法访问文件系统。那么AP平台就提供“通讯管理”能力和“持久化”模块来支持这两项需求。但是AP平台为了提供这两个能力,它所使用的API是超过了 PSE51 限制的。

如果我们的目标是实现SOA中间件平台,下表列举出了各个 POSIX 配置在这方面的能力。

可以看到,仅仅 PSE 51 是不够的。“PSE 52 +网络通讯”就已经具备了实现SOA中间件平台的能力。这样的平台是典型的 RTOS 平台,如 VxWorks。这些平台没有MMU支持,不支持多处理器,整个系统是一个进程,进程内可以有多个分时执行的线程或任务。

3.png


实际上很难找到仅仅支持 PSE53/54 的OS系统,一般对POSIX支持到这个程度就直接支持POSIX.1 了。

PSE52 是一个分水岭, 基于PSE52来实现SOA中间件平台,就可以用于大多数 RTOS系统,如 VxWorks, RT-Thread 等。很多其它RTOS也有自己的POSIX兼容层,都可以作为SOA中间件的运行平台。在这些平台OS平台上,SOA 应用程序能够使用的API应该被限制在 PSE51 范围内。

更完整功能的SOA中间件平台就需要PSE54或POSIX.1 兼容的OS。典型的以Linux和 QNX为主。目前大多数AP平台实现都是基于Linux 和 QNX;GENIVI 技术栈只支持 Linux,移植到QNX难度不大。对应SOA 应用程序能够使用的API范围越小,移植性越好,但是对于一些跟传感器强相关的服务和算法,使用平台特定API是不可避免的。在充分保证SOA架构的一致性前提下,PSE54或POSIX.1 兼容OS能为应用提供更多的灵活性。


4.3.1.2 Adaptive AutoSAR 简述



完整的 AP 介绍不是本文的目的。这里只以SOA架构的视角对AP 做简单的分析。更详细的信息请参见其它资料。

AP 功能被划分为多个功能组。下图根据AP标准文档表示出了功能组之间主要的依赖关系(箭头所指表示依赖方向)。

AP平台产品中,“升级和配置管理”、“网络管理”和“状态管理”一般是作为单独的SOA服务存在。其它大多数功能组一般以静态库的形式提供,AP应用程序实现特定的SOA服务功能,并链接这些静态库。

无论是AP平台自身的服务和用户开发的服务,如果在POSIX.1 兼容OS平台,就会是一个独立的进程;在 PSE52兼容OS平台,就是系统唯一进程内的一个任务。
“执行管理”功能组起到了“服务装配”和“服务容器”的作用,“通讯管理”提供了服务之间的通讯能力。按AP规范要求,必须实现SOME/IP 协议的支持,有的AP平台实现了对DDS和共享内存的支持。

“状态管理”和“健康管理”功能组起到了“服务监督”的作用。
 
4.png
图4. 3 Adaptive AutoSAR 功能组依赖关系

服务通讯接口,服务装配,执行管理机制都在一个XML格式的 Manifest [14]文件中配置。


4.3.1.3 GENIVI技术栈简介



GENIVI组织提供了一个开源的SOA 技术平台基础。平台实现及应用程序都是运行在POSIX.1兼容OS(主要是 Linux ,其它OS需要移植,难度不大)。

图4. 4为GENIVI技术栈中各软件模块的依赖图。

蓝色部分是与特定通讯通道无关的公共部分,包括3个模块:


  1. SOA通讯接口定义 IDL规范:Franca。以及Franca的语义解析工具。
  2. 根据Franca 生成通讯代码的工具
  3. 通讯通道无关的运行时库,Common API Runtime


这三个模块 GENIVI都有开源实现,可以直接使用。
 
绿色部分是特定通讯通道相关,包括:


  1. 特定通讯通道的绑定规范
  2. 特定通讯通道的代码生成工具
  3. 特定通讯通道的运行时库


GENIVI 提供了两个通讯通道的实现,SOME/IP 和 D-Bus.
 
橙色部分是用户需要为特定 SOA 应用需要编写的内容,包括:


  1. 特定SOA应用的接口定义,需要遵循 Franca 规范
  2. 特定SOA应用对某一个通讯通道的绑定定义,需要遵循该通讯通道的绑定规范。
  3. 特定SOA 应用的功能代码。

5.png
图4. 4 GENIVI技术栈中各软件模块依赖
 
灰色部分是工具生成的代码。

用户编写的SOA应用代码和工具生成的代码与两个运行库被打包在一起,才是最终发布的内容。可以看到,用户编写的SOA应用代码并不会直接使用通讯通道特定功能(绿色部分)。所以用户代码在编写时与特定通讯通道无关,在运行时被绑定到特定通讯通道。我们可以再增加其它的通讯通道绑定,如共享内存、DDS等。

这套技术架构与 Adaptive AutoSAR 的“通讯管理”有对应关系,如图4. 5:

6.png
图4. 5 AP ara::com 与GENIVI的对应

Franca接口规范、 AP接口定义与SOME/IP 规范的映射关系如图4. 6。

7.png
图4. 6 AP、Franca 与 SOME/IP的映射

所有的接口需求都能对应上,因此基于 SOME/IP,GENIVI技术栈可以实现与 Adaptive AutoSAR 互操作。

GENIVI全套技术栈都是开源的,架构上提供了较好的扩充能力。可以自行对接口定义语言、代码生成工具、Runtime库进行扩展改进,也可以添加新的通讯通道绑定。可以作为一个SOA中间件的架构基础,并与Adaptive AutoSAR互操作。下文会以此为基础讨论进一步改进优化方案,为实现适用于自动驾驶的SOA中间件提供思路。


4.3.2 动态多通讯通道绑定



AP 和 GENIVI都提供了以太网的 SOME/IP 通讯绑定能力,都能支持“基于请求响应的RPC”和“基于事件的发布订阅”这是完成SOA服务通讯的基础。对于GENIVI技术栈来说,至少还需要增加以共享内存为通讯通道的绑定能力。

8.png
图4. 7 SOA 服务部署示例

共享内存解决的是不同进程之间如何进行高效通讯,前文说到,多个SOA服务可以被装配到同一个服务容器进程中。同一个进程中多个服务,其通讯实际上是可以直接通过API调用完成。

在图4. 7中,显示了两个通过以太网连接在一起的两个不同的 OS系统。各自OS中运行着一个或多个“服务容器进程”,每个容器进程中有一个或多个SOA服务。

服务的部署情况如下:


  • 服务1 与服务2 在同一个服务容器进程A 中;
  • 服务3部署在容器进程B中,容器进程B 与 A 是同一个 OS 内的两个进程,
  • 服务4、5 在另一个 OS 中的服务容器进程内



服务1 会触发 Event α , 服务2/3/4 都订阅了该事件并使用各自的 handle_α 方法进行处理。但是收到事件的方式各不一样:


  • 服务2 的 handle_α直接被容器进程A通API直接调用
  • 服务3 所在容器进程通过共享内存获取事件再调用其方法
  • 服务4 通过网络(SOME/IP)收到事件并处理



在这个例子中,服务1产生的事件是通过三种不同的方式到达接收者。下表对这三种方式做了对比,假设数据量在100字节的量级(意味着数据量不是延迟的主要原因)。


9.png


进程内API绑定



可以看到进程内的API调用经过CPU缓存优化后,速度是最快的。不需要数据复制,不需要做任何序列化。只需要程序语言能自动对数据内存做自动回收,对C++而言,就是一个带引用计数的线程安全的智能指针。也就是说独立开发编译的两个SOA服务,如果被加载到同一个服务容器进程,在服务容器进程的支持下,直接通过API调用来进行服务交互操作。这可以说完全消除了分解为多个服务带来的额外互操作的开销。

理论上,进程内的API绑定能力应该由服务容器直接提供。在跟进IDL生成的代码的协助下,服务容器应该自动识别本进程内的多个服务的交互,自动将一个服务的请求或事件对接到对应的另外一个或多个服务。

进程内的API绑定的实现不应该是同步执行的,也就是说方法的“调用者”和“响应者”之间、事件的“发送方”和“响应方”之间,不是一个同步的API调用,需要异步执行,或者在不同的线程中执行。具体实现取决于中间件 Runtime 以及服务容器进程。


多通道绑定能力



前面的例子中,服务1的事件是通过三种方式传输出去。但服务1自身并不需要知道实际的事件信息传输方式。服务1的用户代码只是调用了根据IDL生成的代码骨架,由中间件的Runtime库和服务容器配合选择合适的通讯方式。

这要求中间件的 Runtime库和服务容器具备动态多通道绑定能力。目前 AP平台产品的默认实现以及GENIVI的实现都只支持单个通道绑定能力,需要进一步进行扩展。

此外,服务容器最好能感知并记录服务1的事件被哪些服务通过那种通道订阅了。这样根据订阅者的情况决定是否不用向某些通道发送数据。比如,假如上图中服务4取消了事件订阅,则就不用向以太网发送数据了。



进一步的优化



上图例子中,服务容器C内两个服务都需要通过以太网接收数据并处理。以太网接收数据必然要经历一个反序列化的过程。服务4和服务5需要各做一次相同的反序列化。进一步的优化可以让服务容器与中间件Runtime相配合来做反序列化动作,然后将获得的事件数据通过API分别传递给服务4和服务5,这样只需要做一次反序列化。

----

要实现以上优化,对与AP中间件实现,需要对其执行管理进行扩充,对根据Manifest生成的代码进行扩充,与执行管理进行配合。对与 GENIVI 实现,需要扩充其Common API Runtime 以及 Common API 代码生成工具,以支持动态多绑定,直接API绑定,还需要增加共享内存绑定(可以基于iceoryx 实现)。

为了让服务功能更单一,我们尽可能划分出更多的简单、无状态的服务。但是过多的服务通讯会降低性能。所有上述各种优化都是为了降低对服务划分后对性能的影响。开发阶段,我们关注服务的功能,但是在服务部署上,将相互之间通讯密集的服务部署的“近”一些,让它们之间的通讯自动被各种方式优化,减少其通讯开销。这样就达到了架构灵活性与高性能的平衡。


4.3.3 高效任务调度与异步操作



在中间件中,任务调度和异步操作有很大的相关性。

在下图的示例中,IDL 定义了一个加法操作,使用代码生成工具为客户端生成了代理代码,为服务端生成的存根(Stub)代码。

一般代码生成工具都会为客户端的代理代码同时生成同步和异步方法。示例中的同步方法plus会等待结果获取后才返回,异步方法 plusAsync提供了一个回调函数 _callback ,plusAsync方法被调用后会立即返回,计算结果会异步到达,然后用户的 _callback方法会被触发,以提取计算结果。

用户代码实现服务时,从服务的存根代码继承基类型 MathStub,并提供对应虚函数的实现。示例中用户提供的plus 函数会被中间 Runtime 调用。这个例子演示的是“请求/响应”的RPC模式下的行为,如果服务端订阅了一个事件,对事件的处理机制也是类似的,相当于服务端收到了一个不需要回复结果的响应请求(参见3.2.3,fireAndForget 或 oneway)。

10.png
图4. 8同步/异步调用代码示例



4.3.3.1客户端的“同步/异步”机制分析



对与客户端来讲,有表层的同步API /异步API“外观”,和深层的同步IO/异步IO“实现”两个层面的区别。表层的就是上面例子中 plus / plusAsync 两个不同的方法,差别就是同步API调用等待结果到达再返回,异步API则立即返回,结果到达后触发回调函数。这个主要体现在根据IDL生成的代码上。

实际的IO机制是同步还是异步,取决于中间件Runtime 的实现。现代的通讯中间件基本上都会采用异步IO。也就是说,即使对与同步API,底层实际通讯机制也可能是采用了异步IO。只是,在中间件Runtime 中对同步API的调用线程做了挂起再唤醒的处理。实际的发送动作也是异步执行的。

也就是说,一个正常的“请求/响应”RPC调用,在客户端至少有两个任务需要中间件Runtime进行调度执行。一个是异步数据发送任务,一个是收到返回结果后对注册的 _callback函数的回调。


4.3.3.2中间件Runtime 对用户代码的调用



如上文所述,中间件Runtime 至少有三个场景需要对用户代码进行调用:


  • 服务端中间件Runtime收到RPC请求后调用用户代码进行处理
  • 服务端中间件Runtime接收到订阅的事件后调用用户代码进行处理
  • 客户端中间件Runtime收到RPC的响应后回调用户注册的 callback函数



我们把上面每一次调当作中间件 Runtime 要执行的一个任务。中间件Runtime 对任务的执行又衍生出了两个问题:“中间件的线程模型” 和 “中间件的任务调度”。


4.3.3.3中间件的线程模型



一个对用户(中间件的使用者)友好的中间件系统应该对用户屏蔽复杂的线程管理,用户只需要提供方法函数供中间件Runtime 调用。中间件Runtime 执行线程有很多设计决策需要考虑。


  1. 线程与任务的映射关系。

    a)  如果每个任务到来就分配一个线程去执行,新建线程或从线程池中取一个空闲线程,没有空闲线程就等待。这样任务的调度就等价与线程的调度。

    b)  维持较少的线程数量(一般是CPU核心数的两倍),每个线程有个待执行的任务队列,新任务到达就放到线程的任务队列的合适位置。

  2. 对同一个客户端的RPC请求是否要按照顺序执行。如何保证顺序?

  3. 对同一个客户端的多个RPC请求是否允许在不同的线程执行?如果允许,则用户要在自己的代码中对共享资源做加锁处理;如果不允许,用户代码就不用考虑共享资源的加锁,编程简单。

  4. 在为任务分配执行线程的时候,是否考虑请求来自哪个客户端,比如把来自同一个客户端的执行任务分配到同一个线程。

  5. 能否为来自不同客户端的请求绑定一个需要访问的资源标识,将需要相同资源标识的任务分配到同一个任务队列中,这样用户代码就不需要为不同客户端来源的请求访问竞争资源时做加锁处理。



中间件线程模型的设计对用户编写的难易程度有很大影响。中间件自身典型的做法是底层通讯(网络/共享内存)使用异步I/O (典型的反应器模式),同时开启一个线程池,新任务到达就从线程池中获取空闲线程执行。这样所有竞争资源互斥的问题都由用户代码解决,任务调度就依赖操作系统的线程调度。而优秀的中间件Runtime实现应该给用户提供灵活的线程执行机制,以简化用户代码的实现。


4.3.3.4中间件的任务调度



前文说过,对用于自动驾驶中间件来说,应该有更精细的任务调度机制,而不是依赖于操作系统本身的线程调度。一方面线程数量过多会占用较多内存(每个线程都有自己独立的栈空间,Linux上一般默认设置8MB以上),一方面线程切换的系统开销大,线程调度用户不可控。

解决的办法就是维护独立的多个任务队列,少量线程逐个获取执行任务并执行。将CPU内核态的线程切换转变成了用户态的任务切换。任务的来源主要有两个:


  1. 用户主动发起的异步任务
  2. 中间件Runtime对用户代码的调用



在自动驾驶软件系统中,前者典型的是发起异步IO操作,后者是中间件Runtime的典型行为。这种用户态的任务并发设计根据具体的实现机制有不同的称呼,比如“协程”[32]和 “Actor 模式”[31]都是其体现形式。

这种设计对处理计算任务和I/O任务的并发尤为有用,在自动驾驶软件中,还有一个额外的好处,为在CPU的用户态实现精细的任务调度提供了可能。自动驾驶系统中的计算任务有几个特点:


  1. 对完成任务的时间有要求
  2. 同一个类型的任务会被反复执行
  3. 排除I/O的影响后,同一个类型的任务的执行时间相对稳定



比如一个视觉目标检测的算法要求1秒钟能处理 50 帧。这个算法的执行是有时间约束的,算法启用后反复执行,没有特别的意外(主要是I/O上),执行实际基本在一个小范围内波动(执行时间的统计方差小)。当有很多这样的任务需要被调度时,调度算法可以动态的安排任务执行的顺序,保证最大限度的满足任务的时间要求。

为了得到这样的调度协助,中间件的使用者也需要告诉中间件Runtime他所需要的调度要求,这一点在下一节“IDL的定义扩展”中我们继续阐述。中间件的API设计上甚至程序语言的特性上也最好能够对这种调度机制提供支持,我们在下文的“程序语言支持”一节中说明。

----

实现本节相关的功能需要的中间件Runtime,在Adaptive AutoSAR 中对应于其“执行管理”功能组;对于GENIVI,对应于 Common API Core。根据IDL生成的代码也需要与Runtime 的API进行协同。



4.3.4 增强对QoS 的支持



QoS 的重要性第二章已经说过,这里不再赘述。SOME/IP 协议本身并没有定义QoS相关的支持。目前对QoS实现最完善的是DDS相关产品。作为DDS互操作标准的RTPS(Real-Time Publish-Subscribe)协议也定义了对QoS的支持[34]8.7

所以目前大多数中间件产品为了实现对QoS的支持,往往采用直接绑定 DDS 作为底层的通讯方式来实现。Adaptive AutoSAR 的“通讯管理”就是采用绑定DDS来提供QoS能力[35]7.7.3。并在描述服务配置的 ARXML 中指定 QoS需求[14]10.1.2。,SOME/IP-SD 协议中包含对QoS能力的描述,也是用于以DDS为通讯通道时,交换服务的QoS能力。

对于GENIVI,也可以通过提供对DDS的绑定来增加对QoS的支持。但是这种方法是无法对使用SOME/IP的通讯来提供 QoS能力的。为 SOME/IP通讯提供 QoS的方式也有浅度和深度两种。

浅度支持是只中间件 Runtime 在不对 SOME/IP 协议做扩充的前提下增加QoS 单方面的有限支持。比如在 RPC 请求中增加重试次数的设置,但这只适合“幂等”的请求。也可以在 RPC请求中增加Deadline 时间,但是不能期望对端的服务能够支持,只能在本地Deadline时间超过后给出错误提示。也可以在订阅事件时增加FPS要求,同样不能指望事件产生方对这个要求有对应的处理。

深度的支持要求SOA服务的双方(RPC的请求者和响应者,事件的生产者和消费者)都能够对QoS信息进行处理。但是对这些行为的约定并不是SOME/IP协议本身的一部分,如果SOA系统中客户端和服务端采用中间件都是同一个厂商开发的产品,那么是可以做到类似DDS的QoS能力的。


4.3.5 IDL定义的扩展能力



IDL 必须具备的基本能力就是定义服务接口API,包括数据类型、RPC方法、发布订阅的事件等。在前文2.2 节中已经有介绍。

同样,前面几节也提出了几个可以在 IDL 中进行扩展定义的特性。


  • 接口实现的线程模型需求 (4.3.3.3)
  • 接口实现对任务调度的需求(4.3.3.4)
  • 对 QoS 的需求



以图4. 8中的例子来说,我们可以给 IDL 中定义的plus 方法,指定来自同一个客户的方法在同一个线程中执行,要求完成时间不超过20 毫秒,如果因为通讯通道的原因执行失败,自动重试5次,仍然不能成功执行才报告错误。代码生成工具可以根据这些信息生成对应的代码,中间件Runtime也能根据这些信息做好任务的线程安排和调度执行。

Adaptive AutoSAR的接口定义是以ARXML格式的文件表述的。其IDL规范等价于一个定制的XML 规范,定义了可以使用的XML标记集合及语意[14]。作为XML的一个方言, ARXML文档主要是供程序读写,使用者直接阅读是比较困难的。所以Adaptive AutoSAR 厂商需要提供可视化工具来给用户使用。如要对ARXML进行扩充,可以增加自定义的标记,对应的可视化工具要能够识别和处理。

GENIVL的 Franca 类似普通的程序语言,可读性很好,可以供用户直接编写,然后对应的编译器进行解析。Franca规范及编译器也提供了好几种方式供用户对规范进行扩充[11]3.5


4.3.6 服务容器增强服务管理能力



服务容器的基本意图是为了实现3.3.6节“服务装配”机制,让SOA服务获得服务部署与服务管理的灵活性(这里的容器与Docker 容器没有关系)。同时,如果想实现4.3.2 节描述的SOA服务之间的通讯直接通过“进程内API”调用,也必须服务容器的支持。典型的实现方式图4. 9,两个服务共享同一个中间件Runtime,中间件Runtime 负责在内部做API的对接以及异步化处理。一个服务容器进程中也可以有多个中间件 Runtime 供不同的SOA服务使用,不过这样实现服务之间“进程内API”通讯就难一些。

11.png
图4. 9服务容器

在服务装配的基本能力之外,更进一步,我们还可以利用服务容器做更多便利的工作。

服务容器可以负责对服务的监督,对出故障的服务重新启动。还可以对所有受管服务的SOA请求响应和事件处理行为进行监控和统计。这样就可以明确知道某个请求/响应或对某个事件进行处理的平均时间等信息。而且这些统计能力不需要每个SOA服务做特别的工作,只要装配到服务容器内,就能获得这些数据。这对分析整个系统的性能有非常大的帮助。

服务容器也可以缓存并响应服务发现的信息。一个服务容器进程启动,哪怕没有加载任何服务,也可以收到网络上的其它服务发送的广播信息(SOME/IP服务发现协议的 OfferService 报文)。服务容器缓存这些信息可以在加载服务时更快的进行相应。

服务容器与中间件Runtime 配合,可以针对QoS 做一些干预。比如说,一个SOA服务订阅了某个主题的事件通知,其QoS 设定只需要一秒钟接收10次,但是网络上是按照一秒钟30次发送。这时,服务容器可以减少对受管SOA服务的调用次数。反过来,一个SOA服务每秒产生30次某个主题的事件,其所在的服务容器通过对服务发现中QoS信息的分析,发现外部服务对该事件期望的最高帧率只是10帧,服务容器就可以减小对外发送的次数。服务容器的QoS干预行为对受管的SOA服务是透明的,SOA服务装配到服务容器中就可以获得这些能力。

服务容器还可以同时加载两个一样的服务,一个正常工作,另一个备用,但对外体现出来只有一个能够被发现访问点。如果工作中的服务出故障,服务容器自动将请求转交给备用的服务进行处理。这样可以提高服务的可靠性。


4.3.7 服务发现的增强实现



SOME/IP 服务发现协议是一个非集中化的协议。一个SOA服务就绪后,广播OfferService 消息,告诉网络中的其它服务。某个服务需要其它服务的信息时,可以主动发送FindService 消息,等待收到回复的OfferService消息。请求方只关心收到OfferService的结果,并不关心是谁发出来的。这就让我们可以在这里做一些增强性的工作。

上一节提到可以在服务容器中缓存服务发现的结果信息,方便服务加载时快速响应查找服务的请求。同时服务容器也可以在被管理的服务异常退出,没能及时发出“服务不可用”(StopService) 时代替它发出。

另外,SOME/IP 的服务发现协议是去中心化的,但是我们也可以开发专门的“服务发现支持服务”,负责根据服务发现报文缓存网络中的各种服务的访问点信息,提高服务发现的效率和稳定性。这在车载以太网被划分为多个网段,甚至与路侧单元、云端都有SOA服务交互时非常有用。


4.3.8 增加对 RTOS 系统的支持



完善的自动驾驶SOA中间件应该包括对RTOS的支持能力。深入的支持方式是中间件Runtime本身能够运行在基于 POSIX PSE52 的RTOS上。这时候整个RTOS作为一个进程存在,本身就是一个服务容器。

浅层的支持能力只需要在RTOS上支持 SOME/IP协议以及SOME/IP服务发现协议。能让运行在RTOS上的任务对外看起来像一个 SOA服务就可以。

自动驾驶系统一般都会在主SoC之外,用一个满足 ASIL-D的MCU负责高实时性高安全等级的工作,MCU上主流软件平台方案会采用 Classic AutoSAR,目前是可以支持SOME/IP的。

目前用于自动驾驶的高性能SoC芯片往往会在芯片内部集成一个或多个Cortex-R核心用来做高实时运算,也有的内置一个单独供电的MCU岛,用来监控SoC的运行状态。内置的MCU和外置的MCU都采用Classic AutoSAR 从成本上看是很不划算的,因为芯片架构不一样,硬件相关的模块往往再付一份费用,非常昂贵。

这时,SoC内置的MCU运行非Classic AutoSAR的RTOS系统,并具备浅层的SOA支持能力就很有价值。我们可以把“服务监督”服务实现在这个内置的MCU芯片上,以提高整个SOA系统的可靠性。

为了具备这个能力,还需要开发特定的代码生成工具。支持根据 ARXML或 Franca 等IDL规范定义的接口,生成适用于特定RTOS的代码,完成SOA相关的通讯。


4.3.9 增加新的语言支持



多语言支持往往是通讯中间件非常有特色的特性,在同一个协议标准下,不同程序语言开发的软件能够方便的互操作。对 Python语言的支持也是ROS被广泛运用的加分项之一。Python让很多AI算法开发工程师可以很方便将自己的算法集成到整个ROS的软件体系中,而不用去学习复杂的 C++语言。

Adaptive AutoSAR 和 GENIVI目前的都是只支持 C++语言。虽然有很好的执行性能,但是对软件测试,对不同技能的开发人员协作都不友好。

对多语言的支持也有浅层和深层两种形态。对某个开发语言的深层的支持是指用该语言开发完整中间件Runtime系统,也称为原生语言支持。比如ROS基于C语言开发,Adaptive AutoSAR基于C++开发。浅层的支持是在某个基于原生语言的实现开发另一个语言的API接口。比如ROS的 Python接口和 C++接口实际是对C接口的封装。一般来说,原生语言支持难度大工作量也大,浅层的API支持就简单很多。

对于车载软件来说,处于对执行性能和实时性的要求,选择动态语言做原生实现不太现实。但是对与Python这样的动态语言,提供浅层的API支持依然能对自动驾驶系统的开发带来很大好处,尤其是在软件测试和快速原型开发阶段。

对于原生语言的选择,除了C和C++外,近年来快速崛起的Rust语言也不失为一个优秀的可选项。Rust 受关注的特性是其内存安全性,解决了内存安全与执行性能不可兼得的程序语言历史性难题。

12.png
图4. 10 RUST 兼具性能与安全

我们知道GoogleC++编码规范[37]定义了约100页编码规则,AutoSAR CPP14 编码指南[36]更是达到了500页的篇幅。这些规则中很大一部分都是为了防止内存错误。程序语言历史上为了解决内存安全问题的典型方法就是使用自动垃圾收集(Java,.Net 等)。垃圾收集瞬间,程序执行被冻结,这在车载软件中是不可接受的。Rust 通过严格的所有权与生命周期管理成功的解决了这个问题。详情请参考拙作《图解Rust 所有权与生命周期》[38]

除了内存安全,Rust在异步编程方面设计完备的语言级支持,async/await 关键字让发起异步任务极为简便,同时支持完全自定义的任务调度算法。

当然Rust要成为成熟的汽车软件语言,还需要逐步具备符合功能安全要求的工具链体系,需要一定的发展时间,但前景可期。


4.3.10 RESTful 接口的自动生成



SOA 服务在IDL定义中已经给出了详细的数据类型定义,方法名称和参数定义,所以直接根据IDL定义生成基于HTTP的RESTful接口是完全可行的。中间件实现应该允许我们扩展 IDL定义,支持在 IDL 描述中直接指定为某个接口的某个方法生成 RESTful API。比如下面的例子中,指定Math接口支持 RESTful 访问:

13.jpg



 那么代码生成工具应该能生成代码,让plus 方法能够通过以POST动作访问URL “http://xxx:xx/math/plus”来被调用。Web Server 可以直接集成进中间件 Runtime 或者由服务容器内置 Web Server , 收到HTTP请求后代为对受管 SOA服务发出请求。具体的实现方式,不一而足,但是自动生成RESTful API可以节省很多开发时间,让应用集成更为便利。



4.3.11 本章小结



这一章我们结合SOA架构风格和自动驾驶系统的特殊性,提出了对支持自动驾驶的SOA中间件的一些设想。并讨论了对Adaptive AutoSAR 和 GENIVI 两个技术体系分别分析了进行扩充的方向。其中有一些已经在某些中间件产品中得到实施,更多的还没有得到实践的检验,这也说明适用于自动驾驶的SOA中间件还有很大的发展空间。



05

全文结语



这篇文章是纯技术文章,但是在全文的总结里我想把技术放一边,谈谈我对国内自主车载SOA中间件发展的看法。

首先,Adaptive AutoSAR 让我看到浓浓的 CORBA 和 J2EE 的味道:完善但又复杂的标准体系,但学习曲线陡峭,上手困难;标准组织定义规范,提供参考实现,各厂商在标准上合作,在产品上竞争,但是产品的价格昂贵。

下图是某 AutoSAR 厂商不同授权方式的阶梯价格示意。最便宜的授权只能用在指定一级供应商开发的单个ECU产品,指定芯片,只能卖给一个OEM。其在量产项目中使用的价格(图中的单个阶梯块)要超过500万RMB,如果需要功能安全,再乘以1.5倍。

`14.png

在这种价格体系下,如果广大开发商普遍采用AutoSAR,恐怕大家辛苦一场,结果却成了给中间件厂商打工了。

AutoSAR 的商业模式对人才的培养也造成了非常大的阻碍。虽然AutoSAR 的标准可以在网上自由下载,但是作为广大程序员,如果想基于AutoSAR开发,你要先加入一家汽车软件公司,寄希望这家公司至少能先花几百万买一个预研版。因为AutoSAR的配置非常依赖专用IDE工具,而工具一般要卖10万元一个License。所以一般企业也不会买太多。这也造成了人才市场上AutoSAR技术人才奇缺。

互联网领域有很多开源的技术体系,大浪淘沙之下,能够流行的技术一定是在某些方面有特点,比较方便解决了特定的问题。一般来说要么专注于某一个具体的技术点,方便被集成进其它系统架构中;要么是一个整体性架构,但是在使用时可以有较大的灵活性,可以整体采用,也可以部分采用,如Spring Framework。Spring Framework也使用了大量J2EE规范里的技术,但是合适的就用,不合适的就另起炉灶。所谓合适还是不合适,要看能否更好的解决实际问题。而且这些开源技术可以很容易的获取,人才的培养极为便利,有人用,才能发展的更好。

汽车软件领域对开源软件的采用也越来越多,Linux用于车载操作系统,ROS用于自动驾驶原型开发,各种单一功能的开源软件库更是数不胜数。但是比较成熟的开源SOA中间件,尤其是为自动驾驶优化的SOA中间件并不多,做得比较完整的且可用的目前看也就Cyber RT。但是不应该只有一个。我对GENIVI 感兴趣的主要原因在于它只实现了一个SOA中间件的最小核心:IDL规范,多通讯绑定能力,SOME/IP支持。而且架构上每一具体技术点都有办法进行扩充与改进,可以扩展IDL规范,增加通讯绑定,增加新语言支持,改进调度机制等等。可以作为一个产品化的SOA中间件的起点。也可以选择性的对AutoSAR 规范中某些部分进行支持。毕竟SOME/IP也是 AutoSAR规范的一部分。

当我们将互联网领域的常用软件架构、开源技术应用到汽车软件时,也要保持对传统汽车软件严格的开发过程管理,高标准的测试要求保持足够的尊重与敬畏。毕竟,驾驶安全无小事。要在软件敏捷开发、快速迭代与软件安全之间找到合适的平衡点。

总而言之,汽车软件的技术架构、开发方法以及人才结构都在一个快速的变化中,以适应智能网联汽车带来的新的时代。国产的SOA中间件作为重要的汽车基础软件之一,一方面吸取AutoSAR中成熟的标准和技术,保持与AutoSAR的互操作性,同时要在架构的灵活性、成本最优、人才培养上走出自己的路。

参考资料:

[1]. CORBA History https://www.corba.org/history_of_corba.htm

[2]. What is CORBA http://www.ois.com/Products/what-is-corba.html

[3]. omniCORBA http://omniorb.sourceforge.net/

[4]. 反叛之冰:Internet Communications Engine https://blog.csdn.net/grhunter/article/details/45606

[5]. Differencesbetween CORBA and ICE https://forums.zeroc.com/discussion/3035/differences-between-corba-and-ice

收藏
点赞
2000