第一章:Cyber RT基础入门与实践
内容导入:
问题:当我们在开车的过程中,假设我们要进行从左侧车道向右侧车道的换道动作的时候,我们是怎么处理的?
下图是Apollo8.0的架构图,软件核心层中,感知、规划、控制等模块的存在使得汽车具备了类人的驾驶能力,但是这些模块在软件系统中是相互解耦的,Cyber RT就担负起了把自动驾驶中的各个算法模块组织串联的重任,满足了各算法模块之间的数据通信需求,同时也保证在数据通信过程中的实时性与可靠性。这就好比是人的血管、肌肉与大脑的关系。
Apollo是部署在RTOS(Real Time OS 实时操作系统)之上的,目前是基于linux内核的Ubuntu来运行的,RTOS一般采用时间片轮转调度,简单来说就是CPU把任务根据优先级划分,并且划分不同的时间片,通过时间片轮转,使CPU看起来在同一时间能够执行多个任务,就好像一个人同时交叉的做几件事情,看起来多个事情是一起完成的一样。关于优先级的设置相对比较通用,交互型进程的优先级要比脚本型进程的优先级要高,比如,有时候我们在看视频的时候,想要通过搜索视频名称去切换到别的视频,在键盘输入字符的时候会卡顿一下,那是因为键盘输入的进程高于视频播放器的进程,发生了进程的调度。相比于RTOS的通用调度,自动驾驶场景的调度算法要复杂的多,比如,上面的问题发生在自动驾驶车辆上,在那一时刻,右侧相机的优先级肯定是最高,而cyber RT就提供了这样的调度算法。
关于cyber的更多知识可以通过以下课程学习:
1、Cyber RT 开发者课程:https://apollo.chjunkong.com/community/public-course/394
2、Apollo星火计划2.0之Cyber详解:https://apollo.chjunkong.com/community/course/outline/2?activeid=248
3、Apollo开发者社区文章:https://apollo.chjunkong.com/community/article/140
1.1 Cyber简介
Apollo Cyber是首个专为自动驾驶定制的高性能且开源的实时通信框架,于2019年与Apollo 3.5开放平台同期发布,它主要解决了自动驾驶系统的高并发、低延迟、高吞吐、任务调度等问题,同时还提供了多种通信机制和用户级的协程,在资源有限的情况下会根据任务的优先级来进行调度处理。如下图所示,Cyber RT通过Component来封装每个算法模块,通过有向无环图(DAG)来描述Components之间的逻辑关系。对于每个算法模块,都有其优先级、运行时间、使用资源等方面的配置。系统启动时,系统会结合DAG文件、调度配置等信息,创建相应的任务,从框架内部来讲,就是协程,然后图中间的调度器把任务放到各个处理器的队列中,最后由左上角Sensor输入的数据,驱动整个系统运转。
1.1.1 框架优势
Apollo Cyber框架的优点主要有以下几点:
- 高性能:Apollo Cyber是专为自动驾驶定制的高性能运行时框架,能够处理高并发、低延迟、高吞吐等自动驾驶系统中的复杂任务。
- 灵活性:Apollo Cyber提供了多种通信机制和用户级的协程,可以根据任务的优先级来进行调度处理,能够灵活地应对各种自动驾驶场景。
- 可靠性:Apollo Cyber通过Component来封装每个算法模块,通过有向无环图(DAG)来描述Components之间的逻辑关系,能够确保系统各个模块之间的稳定性和可靠性。
- 扩展性:Apollo Cyber的架构可以支持各种不同类型和不同层次的自动驾驶任务,并且可以方便地扩展和升级。
1.1.2 框架结构
如下头所示为Cyber RT的框架结构图。
- 第一层:主要是Apollo实现的基础库,比如有Lock-Free的对象池,Lock-Free的队列等。实现基础库可以减少相关的依赖并提高运行效率。
- 第二层、三层:主要是负责管理Cyber的通信机制,包括服务发现(负责管理通信中的Node节点)和Publish-Subscribe通信机制。并且Cyber也支持跨机、进程间、进程内通信,而且会根据不同的数据传输和业务逻辑自动选择效率最高最匹配的通信方式来进行通信。
- 第四层:主要是对传输过后的数据进行缓存,并会根据不同传感器得到的数据进行融合得到一个可处理可读的数据发送给另一个模块。
- 第五、六层:主要是管理每一个任务的调度和数据处理。
- 第七层:提供给开发者的一些API接口,让开发者有更多可操作性,提高开发效率。
1.1.3 通信构成
Node是Cyber的基础构建,每一个模块都会包含一个Node节点,模块之间通过Node节点来进行通信。Node之间的通信可以设定不同的模式,有Reader/Writer和Service/Client。
Cyber采用的是分布式系统,Node是通过Topology来管理的,每个Node都是这个拓扑图的顶点,其中每个Node顶点是通过Channel或者Service来连接的。Node节点是去中心化的,可以动态监控节点的增加和删除。Channel可以理解为一块共享内存,采用共享内存的通信方式,可以大大提高通信效率。下图1-2就能更加形象得表现Node之间的关系与数据流向。
1.1.4 程序构成
如图1-3所示,这是由482个Component(Cyber把每个模块,例如Camera驱动、控制模块都会有对应的Component组件)组成的一个简单的网络, 最左边Camera Driver作为系统的输入节点,从传感器读取数据并发送到/sensor/camera/image这个Channel(Component之间通信的通道)中,然后中间的这个Perception Component,它会订阅相机驱动传感器的数据,从/sensor/camera/image这个Channel中取出数据并进行相应的算法处理,将得到的结果从/perception/obstacles这个channel中进行输出,最右边的是Planning component,它会从/perception/obstacles这个Channel中取出感知结果并得到一个决策规划的结果。下面就是具体的Component的实例,实例中的Perception它不需要关心上下模块是Camera还是Planning,只需要定义自己的输入,也就是SensorMessage这个类型的数据,以及在配置文件中定义输入的Channel名字即可,在开发阶段就能大大减少两个模块之间的耦合。
1.1.5 系统安装
在使用Cyber框架时会进入到Apollo的docker容器中,所以要先安装好Apollo并进入到容器内操作,Cyber框架代码在apollo/cyber。
1.2 Bazel简介
在学习和使用CyberRT时需要用到Bazel,所以这里我们了解一下Bazel。Bazel是Google研发的一款开源构建和测试工具,也是一种简单、易读的构建工具。其优点如下:
- Bazel 仅重建必要的内容。借助高级的本地和分布式缓存,优化的依赖关系分析和并行执行,可以获得快速而增量的构建。
- 构建和测试 Java、C++、Android、iOS、和其他各种语言平台。Bazel 可以在 Windows、macOS 和 Linux 上运行。
- Bazel 帮助你扩展你的组织、代码库和持续集成系统。它可以处理任何规模的代码库。
1.2.1 Bazel项目结构
1.2.1 Bazel结构
在构建项目之前,您需要设置其工作区。工作区是一个保存项目源文件和 Bazel 构建输出的目录。它还包含 Bazel 识别为特殊的文件:
- 该
WORKSPACE
文件将目录及其内容标识为 Bazel 工作区并位于项目目录结构的根部, - 一个或多个
BUILD
文件,告诉 Bazel 如何构建项目的不同部分。
其目录结构大致如下:
project|-- pkg| |-- BUILD| |-- src.cc|-- WORKSPACE
1.2.2 了解Build文件
1.2.2 了解BUILD文件
一个BUILD
文件包含多种不同类型的 Bazel 指令。最重要的类型是构建规则,它告诉 Bazel 如何构建所需的输出,例如可执行二进制文件或库。文件中构建规则的每个实例BUILD
称为目标,并指向一组特定的源文件和依赖项。一个目标也可以指向其他目标。例如:
cc_binary(name = "hello_world",srcs = ["hello_world.cc"],)
在示例中,hello-world
目标实例化 Bazel 的内置 cc_binary规则。该规则告诉 Bazel 从源文件构建一个独立的可执行二进制文件。
BUILD文件中常见的两种规则:
- cc_binary:表示要构建对应文件变成二进制文件。
- name:表示构建完成后的文件名字。
- srcs:表示要构建的源文件。
- deps:表示构建该文件所依赖相关的库。
- cc_library:表示要构建对应文件变成相关依赖库。
- hdrs:表示的是源文件对应的头文件路径。
- package(default_visibility = ["//visibility:public"])这段代码则表示该文件是公开的,能被所有对象找到并依赖。
1.2.2 buildtool认知
1.2.1 了解 buildtool工具
Apollo源码版采用原生的bazel编译,但包管理环境下我们使用buildtool来对代码进行编译。Apollo buildtool是一个命令行工具,提供编译、测试、安装或运行Apollo模块等功能。基于buildtool,不仅可以方便地安装Apollo中各个模块的二进制包,还可以对这些源代码进行二次开发、编译和测试,而不需要下载整个Apollo。buildtool可以让开发者只关注需要开发的模块,提高整体开发效率。
1.3 实践案例
本教程涵盖了使用 Bazel 和buildtool构建 cyber 应用程序的基础知识。您将设置工作区并构建一个简单的 cyber项目,该项目说明了 Bazel和buildtool的关键概念,例如target和BUILD
文件。
1.3.1 Hello Apollo
1.3.1 Hello Apollo
实践内容:
创建Cyber RT工程目录,同时在终端窗口中输出Hello Apollo。
实践目的:
掌握Cyber RT工程框架结构;熟悉buildtool包管理工具。
在本教程中,您将学习如何:
- 建立一个cyber工程目录
- 编写单个的cyber源码
- 编写Apollo包管理相关的BUILD和cyberfile文件
- 使用buildtool进行Cyber工程的编译
实践流程:
<1> 创建本节实验工程目录;
<2> 编写包管理相关的BUILD和cyberfile.xml文件文件
<3> 编写源文件及BUILD文件;
<4> 编译代码目录;
<5> 运行可执行文件。
<1> 创建本节实验工程目录,创建完成后,工程目录如下所示:
cyber_demo|-- cyber_01|-- demo_main| |-- BUILD| |-- main.cc|--BUILD|--cyberfile.xml|--cyber_demo.BUILD
<2> 编写Apollo包管理相关的BUILD和cyberfile.xml文件文件
除了 bazel 规范外,为了 Apollo 能够使用包管理,Apollo 额外添加了一些自定义规则,在模块目录下的 BUILD 文件,添加 Apollo 自定义 rule,本示例的模块下的BUILD文件内容如下所示:
load("//tools/install:install.bzl", "install", "install_src_files")install(name = "install",data = ["cyber_demo.BUILD","cyberfile.xml",],deps = ["//cyber_demo/cyber_01/demo_main:install",],)install_src_files(name = "install_src",src_dir = ["."],dest = "cyber_demo/src",filter = "*",deps = ["//cyber_demo/cyber_01/demo_main:install_src",])
其中:
install 规则可以将模块 BUILD 文件中定义的 target安装到本地仓库;
install_src规则根据特定规则直接安装文件到本地仓库,并保留源码目录结构;
cyberfile.xml 是模块描述文件,用来描述一个模块制作成软件包的相关信息,比如包名、版本号、依赖的软件包。本示例cyberfile.xml 内容如下所示:
<package><name>cyber_demo</name><version>1.0.0</version><description>cyber_demo</description><maintainer email="ad-platform">[email protected]</maintainer><type>module</type><src_path>//cyber_demo</src_path><license>BSD</license><author>Apollo</author><depend type="binary" repo_name="cyber">cyber-dev</depend><builder>bazel</builder></package>
关于Build和cyberfile.xml的详细信息可查阅:https://apollo.chjunkong.com/community/apollo-homepage-document/apollo_doc_cn_8_0 > 软件包简介 > 软件包二次开发 > 模块扩展概念介绍。
<3> 编写源文件及BUILD文件;
通过main.cc输出 "hello Apollo" 内容。
#include<cyber/cyber.h>int main(int argc, char const *argv[]){apollo::cyber::Init(argv[0]);AINFO << "hello Apollo";AWARN << "hello Apollo";AERROR << "hello Apollo";AFATAL << "hello Apollo";return 0;}
编写完源文件后main.cc后,可以通过实例化 Bazel 的内置 cc_binary规则,将源文件main.cc构建一个独立的可执行二进制文件,BUILD文件内容如下:
load("@rules_cc//cc:defs.bzl", "cc_binary", "cc_library")load("//tools/install:install.bzl", "install", "install_src_files")load("//tools:cpplint.bzl", "cpplint")package(default_visibility = ["//visibility:public"])cc_binary(name = "main",srcs = ["main.cc"],deps = ["//cyber"],)install(name = "install",runtime_dest = "cyber_demo/bin",targets = [":main"],)install_src_files(name = "install_src",src_dir = ["."],dest = "cyber_demo/src/cyberatest",filter = "*",)
<4> 编译代码目录
在apollo_workspace目录下执行buildtool编译指令。
buildtool build -p cyber_demo
编译完成后,系统会在/opt/apollo/neo/bin目录下生成可执行文件main,如下图所示:
为了能够看到结果,通过以下命令将输出结果打印到窗口,命令如下:
export GLOG_alsologtostderr=1
<5> 运行可执行文件
在/opt/apollo/neo/bin目录下,执行以下命令:
./main
执行完成后,可以看到命令行窗口打印出了"hello Apollo"的内容。
实验总结:该目标构建单个的源文件,并没有其他依赖项,如下图所示,编译生成可执行文件main只依赖于main.cc。
单个源码的工程可以满足小型项目的需要,但在实际使用中,我们常希望将较大的项目拆分为多个包,以允许快速的增量构建(即仅重建更改的内容)接下来,我们介绍使用多个包来管理你的工程。
1.3.2 构建多包工程
实践内容:
使用包的方法,创建Cyber RT工程目录,在终端窗口中输出Hello Apollo。
实践目的:
在本教程中,您将学习如何:
- 建立一个cyber工程目录
- 编写cyber包源码
- 编写Apollo包管理相关的BUILD和cyberfile文件
- 使用buildtool进行Cyber工程的编译
实践流程:
该示例项目的实践流程与前两个实验类似,这里不在赘述。
<1> 本实验在test目录下已经创建了该实验,实验目录结构如下所示:
test|-- test_bazel|-- demo_lib| |-- BUILD| |-- getName.cc| |-- getName.h|-- demo_main| |-- BUILD| |-- main.cc|--BUILD|--cyberfile.xml|--test.BUILD
- 现在有两个子目录,每个子目录都包含一个
BUILD
文件。因此,对于 Bazel 来说,工作区现在包含两个包,demo_lib
和demo_main
。
<2> 编写包管理相关的BUILD和cyberfile.xml文件文件
BUILD文件内容如下所示:
//BUILDload("//tools/install:install.bzl", "install", "install_src_files")install(name = "install",data = ["test.BUILD","cyberfile.xml",],deps = ["//test/test_bazel/demo_main:install",],)install_src_files(name = "install_src",src_dir = ["."],dest = "test/src",filter = "*",deps = ["//test/test_bazel/demo_main:install_src",])
cyberfile.xml文件内容如下所示:
该项目依赖与cyber库,并采用bazel的编译方法。
<package><name>test</name><version>1.0.0</version><description>test component</description><maintainer email="ad-platform">[email protected]</maintainer><type>module</type><src_path>//test</src_path><license>BSD</license><author>Apollo</author><depend type="binary" repo_name="cyber">cyber-dev</depend><builder>bazel</builder></package>
<3> 编写源文件及BUILD文件;
编写demo_lib库实现get_name功能,获取字符串的输入并与"Hello" 拼接。
// test_bazel/demo_lib/getName.h#pragma once#include <string>std::string get_name(const std::string& name);// test_bazel/demo_lib/getName.cc#include "getName.h"std::string get_name(const std::string& name){return "Hello" + name;}
源码编写完成后,通过cc_library规则将getName.cc源码构建为库文件getName_lib,hdrs表示的是源文件对应的头文件路径,demo_lib的BUILD文件内容如下:
load("@rules_cc//cc:defs.bzl", "cc_binary", "cc_library")package(default_visibility = ["//visibility:public"])cc_library(name = "getName_lib",srcs = ["getName.cc"],hdrs = ["getName.h"])
编写main.cc,实现 "Hello Apollo"的输出。
#include "test/test_bazel/demo_lib/getName.h"#include <iostream>int main(){for (int i = 0; i< 5; ++i){std::cout << get_name(" Apollo ") << std::endl;}return 0;}
demo_main的BUILD文件
通过cc_binary配置将main.cc编译构建可执行文件main,deps表示构建该文件所依赖相关的库。
load("@rules_cc//cc:defs.bzl", "cc_binary", "cc_library")load("//tools/install:install.bzl", "install", "install_src_files")load("//tools:cpplint.bzl", "cpplint")package(default_visibility = ["//visibility:public"])cc_binary(name = "main",srcs = ["main.cc"],deps = ["//test/test_bazel/demo_lib:getName_lib"],)install(name = "install",runtime_dest = "test/bin",targets = [":main"],)install_src_files(name = "install_src",src_dir = ["."],dest = "test/src/cyberatest",filter = "*",)
<4> 编译代码目录
在apollo_workspace目录下执行buildtool编译指令。
buildtool build -p test
编译完成后,如下图所示:
<5> 运行可执行文件
为了能够看到结果,通过以下命令将输出结果打印到窗口,命令如下:
export GLOG_alsologtostderr=1
执行构建的可执行程序main。
// 执行cd /apollo_workspace/bazel-bin/test/test_bazel/demo_main/./main
运行结果如下图所示:
实验总结:该实验项目构建了一个多包工程,其中目标main
依赖于包中的目标getName_lib依赖于 包中的目标getName_lib
,而Bazel 则是通过deps
属性知道这一点。如下图所示:
本章节内容到此结束。