每当谈到自动驾驶的软件开发,人们首先想到的,是深不可测的人工智能算法,是各种感知融合,是各类路径规划...但是,就算是再智能再高深的算法,如果没有底层操作系统的支持,一切都将是纸上谈兵。
而即便拥有了一套优秀的操作系统也还远远不够,我们还需要一条重要的“纽带”来承上启下,使上层应用与底层操作系统可以紧密的”连接“起来!
而这条重要的“纽带”,就是我在本文中将为大家详细介绍的一种全新的,或许能够支撑起自动驾驶未来整体软件架构的“主心骨” - iceoryx「冰羚」。
IceOryx冰羚系统的结构
冰羚的组成如下:
- 一个RouDi守护进程
- 多个加载了"Posh Runtime"运行时的进程
RouDi守护进程
RouDi 的名称由来是’‘Rou’‘ting 和’‘Di’'scovery,其是冰羚系统的核心,并且负责如下功能:
- Service discovery(服务发现):
RouDi是Publisher(发布者)和Subscriber(订阅者)的中心节点 - Shared memory management(共享内存管理):
RouDi初始化冰羚系统所使用的共享内存以及内存分配 - System introspection(系统性能监查):
RouDi掌握冰羚系统当前已经存在的端口的所有信息,包括端口自的连接以及其内存的使用情况,并且RouDi提供工具给应用方来查询这些信息。
RouDi可以被认为是冰羚系统的管理服务,任何运行的冰羚系统中都有一个运行的RouDi实例,RouDi使用Posh运行库中的模组来完成其管理的功能。
Posh Runtime运行时
Posh Runtime运行时是一个拥有独立内存地址空间的运行实体,并且其参与了冰羚系统的运行。
在一个POSIX系统中,一个Posh runtime运行时和一个Posix进程是等价的。
Posh runtime运行时可以向冰羚系统发布服务,或者发现别的Posh runtime运行时发布的服务。
通过Posh runtime运行时发布的服务之间通过event(事件)进行通信,并且事件流是使用了合理的发布-订阅语法。
发布的服务必须显式地向RouDi进行注册以进行通信(和RouDi以及其他Posh runtime)。
共享内存管理
基础
当一个POSIX进程启动后,其就拥有了自己独立的虚拟地址空间。虚拟地址空间的范围对于每一个进程都是相同的,但是对于特定地址上的数据来说,该地址上的数据对于每一个进程来说,其数据内容都是不同的。
一个应用使用指针访问其进程运行后所拥有的虚拟地址空间。在进程的虚拟地址空间中,有不少内存区域(memory areas)的数据是被加载或者映射(mapped)的。对于进程的地址空间来说,这些内存区域是不连续的。下面是可能出现在这些内存区域中的一些内容:
- 程序的运行指令(例如可执行文件中的.text段)
- 静态变量的申明(例如执行文件中的.data段)
- 加载的共享库中的执行指令(例如so中的.text段)
- 进程的栈
- 进程的堆
- 共享内存段
共享内存段依赖进程外部的物理存储(例如RAM上的一些区域或者文件系统),这些外部的物理存储是通过map映射的方式映射到自己的地址空间中,这样进程就可以访问这些存储。
一个存储段可以被映射到多个进程中,但是在每个进程中,其被映射的基地址很可能是不同的。
The POSIX API provides the utilities for working with
shared memory segements.
组织
冰羚系统使用一个“management”段来管理服务间时间通信所使用的任意数量的“user”段。
这些“user”段是被逻辑分区到"mempools“(内存池)中,Mempools中包含一定数量的大小相同的“memory chunks”(存储块)
在冰羚系统中,Memory chunks(存储块)是访问共享内存的基础单元。
冰羚所使用内存段(segments)的数量是定义在其RouDi守护进程启动运行的时候跟随的配置文件中的[mempools]项目下的,配置文件中[mempools]下面包含一个或者多个segments的配置,其中定义了每一个segment中包含的chunk的数量和segment中每一个chunk的大小。
对于这个配置文件的使用,详细的可以查看 usage guide
通信机制
在这个章节中,我们会了解一下冰羚系统中的服务间通信的概念。
端口(Port)
端口代表的数据流的实体,冰羚中有不同类型的端口,他们在使用时,携带的信息的类型是有区别的。
目前存在的端口类型有以下几种:
SenderPort
- 服务使用这种端口来输出任意数据ReceiverPort
- 服务使用这种端口从其他服务接收任意数据InterfacePort
- 本地冰羚系统和远程冰羚系统之间交互信息所使用的网关端口(下面会有更多关于网关的说明)
本地冰羚系统中,服务之间的数据流我们一般使用Sender Port和Receiver Port之间的连接(connections)来描述。
冰羚系统中,一个 Publisher
(发布者)通过一个SenderPort
发布数据,类似的,一个Subscriber
(订阅者)通过一个 ReceiverPort
接收数据。
服务发现(Service Discovery / 端口连接(Port Wiring)
在冰羚中,Publisher
s 和 Subscriber
是通过其内在的SenderPort
s 和 ReceiverPort
s.之间的连接来进行匹配的。SenderPort
s 和 ReceiverPort
s.之间的连接是建立在服务描述(service description)上的。
服务描述(service description)有如下组成:
- A service id - 服务ID,标识服务的类型
- A service instance id - 服务实例的ID,标识具体的服务实例
- An event id - 事件ID,标识当前服务的输出
所有的SenderPort
s and ReceiverPort
s创建时都需要提供service description服务描述,冰羚系统会根据端口创建时的服务描述来进行相互匹配并且自动连接不同的Port端口。
端口出现的顺序不是重要的因素(或者说不需要很关心端口出现的顺序),已经存在的ReceiverPort
s会自动连接到后面创建的 SenderPort
s,只要其服务描述能够匹配即可连接。
此外,冰羚系统中已经存在的 SenderPort
s的信息是依赖于 InterfacePort
s.的,这就允许一些实体(例如网关)使用这些Port端口,并且hook到本地冰羚系统的数据流以及创建一个和外部冰羚系统间的桥接。
零拷贝服务间通信(Zero-copy Interservice Communication)
已经互相连接的SenderPort
s 和ReceiverPort
s 通过共享内存可以进行通信,这个催生了零拷贝通信(zero-copy communication)
一个 SenderPort
被分配了共享内存用于写入数据,在一个POSIX系统中,这个操作被文件访问权限所约束,因为这个共享内存段实际上代表了虚拟文件。
为了输出数据,一个 SenderPort
在其分配的内存段中预定了一个内存块,冰羚系统会只能的选择符合输出数据结构的大小最小的那个内存块,注意,那整个内存块被预定急事数据结构的大小小于这个内存块实际占有的大小。
一个 SenderPort
显式地选择何时写入数据到内存块中并且分发数据给到所有依附于他的ReceiverPort
s (通过服务发现机制建立连接的那些Receiver Port),当发送数据后,指向内存块的一个指针会被放置到ReceiverPort
. 的队列中,ReceiverPort
. 因此可以方便的使用指针访问这个内存块的数据。
一个 ReceiverPort
必须显式的标识出何时他已经完成了对于接收到的指定内存块的处理,之后,当所有和该 SenderPort
建立连接的 ReceiverPort
都标识出已经完成指定内存块的处理时,该内存块会被归还给内存池。
一个关于指针的备注(A Note on Pointers)
像之前已经讨论过的那样,在一个进程的虚拟地址空间中共享内存段可以被映射到不同的区域,为了处理这个特性,冰羚使用了特殊的指针类型: iox::RelativePointer
和 iox::RelocatablePointer
.
使用这些类型,不同地址的内存映射就不再会影响对于内存块的定位了。
更多关于上述指针类型的详细讨论可以参考here.
/iceoryx_utils/doc/relocatable_pointer/relocatable_pointer.md
冰羚系统间的通信(Internode Communication)
部署在不同主机上的冰羚系统间可以通过网桥(“Gateways”)进行网络连接。网桥负责同步不同冰羚系统中 SenderPort
s之间发布的数据。
1 中间件
在解释iceoryx「冰羚」之前,我们不妨先来简单地了解一个重要的概念- 「中间件」
「 中间件 」的主要任务,是负责各类应用软件模块之间的通信以及对系统资源的调度。
它的优点,是可以大大降低应用层软件的开发难度,使研发工程师可以完全把注意力集中到功能算法的开发上。而目前最为业内所熟知的「中间件」当属Classic AUTOSAR中的RTE(Runtime Environment)了。它不仅负责上层SWC(Sofware Component)之间的通讯,也同时负责对SWC进行调度以及对底层操作系统及通讯服务的调用。
总的来说,「中间件」是整个软件架构的核心组成部分。
如果你使用过Linux或QNX等操作系统,就一定会接触一种使用进程间通信的机制(IPC:inter-process communiction),来完成拥有不同虚拟地址空间(virtual address space)的系统应用(Application)之间的数据传输。
而本文的主角iceoryx「冰羚」就是由罗伯特·博世公司 (Robert Bosch GmbH) 自动驾驶部门的架构大牛Michael Pöhnl先生发明的
一种基于 「 零拷贝 」 (zero-copy)和 「 共享内存 」 (shared memory)技术来优化 「 进程间通信 」 (IPC)的 「中间件」(Middleware)。
2 IPC的核心问题
众所周知,在汽车,机器人和游戏等领域,各个软件系统的不同部分之间需要传输大量数据。
在过去的几十年中,由最初的发动机控制系统,发展到当下热门的辅助驾驶/自动驾驶等等,汽车电子技术有了革命性的发展。随之而来的,是电子控制单元(ECU)中不同执行线程(threads of execution)之间的数据传输单位从KB/s增加到了GB/s,而能够 完整地“支撑”自动驾驶功能的数据传输速度甚至将会达到10 GB/s(图 1)。
图 1:ECU内部数据交换的演变 [1]
其中最典型的一种支持IPC的「中间件」解决方案是在传递消息时通过「中间件」来回拷贝数据。由此而产生的后果是,系统将在「中间件」堆栈内部,产生多个数据副本,甚至在必要时对数据的有效负载(payload)进行序列化,这将在无形中极大的消耗系统的资源(图 2)。
图 2:典型IPC中间件解决方案的消息复制 [1]
换句话说,当传输速度达到GB/s级别时,在 「中间件」堆栈中所创建的每个消息副本都会使系统在运行时间(runtime)和延迟(latency)方面付出巨大的代价(图 3)。而 事实上我们本应将宝贵的系统 运行时间用于进行功能计算(上层应用),而不是浪费在“来回移动内存中的字节”上。
图 3:运行时间和延迟随着越来越高的传输速度剧增
3 真正的“零拷贝,共享内存”数据传输
它的工作原理是这样的:
1. 通过iceoryx API,“发布者”将信息直接写入到事先由 「中间件」预订好的内存块(memory chunk)中。
4. 「中间件」iceoryx在“幕后”则对内存块 进行引用计数(reference counting), 当其发现被引用的次数变为零时就会释放内存块(图4)。
图 4:真正的零拷贝通信 [1]
图 5:像货车车厢一样的内存池 [2]
整个过程, “货物”只被装了一次车,直到被“提货”前都再没有离开过车厢
= 数据仅仅只被"写入"了内存一次,但并没有被复制,故而 实现了真正的“零拷贝”!
接下来我们在列举一个例子:如下图6中所示,假设这个时候又有两条新的数据(0xFACE和0xCAFE)被写入,而之前在车厢0xBEEF里面的货物已经被提走(数据已经被读取)。
图 6:共享内存的自动回收 [2]
这个时候,之前提到的引用计数机制(reference counting)就可以派上用场了:当它意识到这个车厢0xBEEF内的货物已经被提空的时候(没有再次发生读取操作),就可以判定这个“车厢”可以被再次回收利用了!(内存块再次被释放,重新回到可以被预订的状态)。
这时,如果我们重新绘制图3,就会得到下面图7这样的一条水平线。
图 7:使用iceoryx实现了恒定时间内几乎无限的数据传输 [2]
它表明:在使用 “零拷贝,共享内存” 的 「中间件」 iceoryx之后,系统的运行时间和延迟已经跟传输的数据量没有关系了。换句话说,系统可以在一定时间内实现几乎无限的数据传输。
4 技术细节
由于在整个数据的传输过程中, 「中间件」iceoryx只是在来回的传递“指针”,因此它在数据传输的过程中实际并不需要实际传输数据。 换句话说,无论数据信息的大小如何,数据在传输过程中所消耗的时间都是恒定的。
除此之外,iceoryx API在支持轮询访问(polling)的同时,也支持 事件驱动回调(event-driven callback)。这个特性使其可以被 广泛适用于大量的不同应用场景,包括实时系统的应用程序。
而共享内存也可以分为具有不同访问权限和可配置内存池的段。
这里我们需要了解使用共享内存的两个限制:
- 一个是它只支持固定长度的消息,
- 另一个是不能使用virtual members以防止member function被派生类重新定义。
除此之外,被传输的数据消息中不能包含任何指向进程中内部虚拟内存空间的“指针”,此限制也适用于基于堆的数据结构(heap-based data structures)。即使无法满足上述条件,「中间件」iceoryx仍然可以与能够处理消息序列化与反序列化的上层软件进行合作,实现零拷贝的底层传输。
「中间件」iceoryx依赖 可移植操作系统接口(POSIX API)。目前,它支持Linux和QNX作为底层操作系统而运行。由于API间往往存在细微的差异,因此当我们需要将iceoryx移植到另一个基于POSIX的操作系统时,可能需要进行较小的配置改动。
iceoryx通过使用服务发现机制实现了动态调度(dynamic scheduling):它可以在实时状态下建立应用之间的连接,这个特性与基于SOA架构(service-oriented architecture)的adaptive AUTOSAR可谓完美契合。
同样,在大家都关心的功能安全领域,iceoryx的目标是达到ASIL-D安全等级,保证软件运行的确定性则是实现这个目标的钥匙。
为此我们需要遵守以下几点编码准则:
- 禁止使用堆,只使用静态的内存
- 只使用部分STL(C++ standard template library)
- 禁止未定义的行为
- 禁止使用exception
目前支持 「中间件」iceoryx的平台有:
- Linux
- QNX
- macOS(未完全测试)
- Windows 10(正在开发阶段)
由于iceoryx是一种与数据无关的基于共享内存的传输机制,因此它仅仅提供了相当底层的API。因此,Pöhnl先生及其研发团队不建议开发者直接使用该API,而是应该将其集成到一个更大的能够提供高级API和工具的框架中去,比如AUTOSAR Adaptive Platform和ROS(Robotic Operating System)。
5 使用许可
说了这么多和「中间件」iceoryx相关的内容,那么究竟谁最应该熟练掌握iceoryx的开发和应用呢?
答案是:自动驾驶系统开发人员,机器人系统开发人员和独立游戏系统开发者!
首先,iceoryx本身是开源的 [3],并且它使用了非常宽松的Apache-2.0许可证。也就是说,任何个人或者开发团队都可以把iceoryx用于商业用途而不必公开其衍生产品的源代码。
如果你想使你开发的 iceoryx达到较高的安全级别,则需要从博世的子公司ETAS GmbH购买相关的安全手册,其中内容包括符合各安全级别的编译器设置信息和VRTE等配套产品,如ara_on_iceoryx (AUTOSAR API)等等。
当然,如果你的开发团队有足够的实力,也可以尝试自己开发整套API并取得安全级别认证。
小结
随着整车的EE架构越来越集中化,域控制器和中央计算平台的装车率越来越高,基于POSIX的操作系统也会在汽车上越来越频繁的出现,各种新的功能也将会以服务或者APP的形式被不断的添加到车载系统上。
如何实现高效的IPC,都是各APP间通信一个绕不过去的坎儿。因此,IPC的效率直接决定了整个系统的实时性能。
同冯诺依曼瓶颈有些相似的是,无论顶层应用算法多么优秀,一个低效 的 「中间件」都会在开发的过程中“演变”为一个巨大的瓶颈,进而会慢慢地拖垮整个系统的运行。
幸运的是,iceoryx的出现则完全解决了这个存在于IPC中的“顽固问题”,它通过“零拷贝,共享内存”的特性,实现了恒定时间内近乎无限的数据传输!
iceoryx的这个特性在实际应用中意义重大!
在大大提高中央域控制器自身实时性能的同时,它也允许整个系统任意增加传感器或者提高传感器的分辨率,而无需承受由于传输数据量的增加带来的系统性能方面的损失,可谓是给自动驾驶技术的发展(至少在数据传输方面)铺平了道路。
因此,我们有理由相信,作为软件架构的核心部件,中间件Iceoryx这根backbone将完全撑的起自动驾驶技术的未来。
注:本文中大部分内容源自作者对参考文献的翻译与总结以及与Pöhnl先生及其开发团队核心成员Hoinkis先生的技术讨论。除个别图片来源于互联网,其他所有文中配图的使用均已得到Pöhnl先生的允许。
参考文献
[1] Michael Pöhnl - 博世
https://www.eclipse.org/community/eclipse_newsletter/2019/december/4.php
[2] Simon Hoinkis - 博世
https://fosdem.org/2020/schedule/event/ema_iceoryx/attachments/slides/3723/export/events/attachments/ema_iceoryx/slides/3723/introduction_to_iceoryx_fosdem_2020.pdf
[3] Iceoryx
https://github.com/eclipse/iceoryx
[4] ROS
https://github.com/ros2/rmw_iceoryx
[5] ETAS GmbH
https://www.etas.com/en/products/rta-vrte.php
[6] Eclipse
https://github.com/eclipse-cyclonedds/cyclonedds
[7] Continental
https://github.com/continental/ecal