Automotive Grade Linux 系统介绍

Posted on Jul 7, 2019


Automotive Grade Linux是一套开源的车载系统平台。本文是对该平台的整体介绍。

介绍

Automotive Grade Linux (简称AGL)是一个由Linux基金会主导的一个开源项目。这是一个由汽车制造商,供应商,技术公司共同组建的开发团体开发的项目。

AGL是一个为车载环境开发的操作系统,我们通常称这种系统叫做车载信息娱乐系统(In-Vehicle Infotainment,简称IVI)。

大家熟悉的Android AutoApple CarPlay也都属于IVI系统。

我之前写过一篇文章介绍Android IVI系统,有兴趣的可以移步:《Android与汽车》。

AGL项目于2012年启动,最初的成员包括:捷豹路虎,日产,丰田,富士通,英伟达,三星等公司。

截至目前(2019年5月)该项目包含的成员已经超过100家,具体成员列表可以见这里:AGL Members

IVI市场现状

这里,我们大致了解一下整个汽车行业的IVI市场现状。

下表整理了几大汽车厂商的IVI系统以及底层系统的信息:

品牌 专属IVI系统 底层系统
福特 SYNC 3 QNX
奔驰 COMAND/MBUX QNX
奥迪 MMI QNX
宝马 iDriver QNX
大众 Composition Meida QNX
沃尔沃 Sensus QNX
丰田 G-Book Linux
特斯拉 Version Linux
雪佛兰 MyLink Linux
本田 Honda Connect Android
蔚来 NOMI Android

到目前为止,黑莓的QNX在市场占有率上有绝对的优势。下面是几个主流系统的对比:

操作系统 占有率 优势 劣势 合作厂商与供应商
QNX 约50% 安全性,稳定性极高,符合车规级要求 商业软件,需要授权费用,只应用在较高端车型上 通用,克莱斯勒,凯迪拉克,雪佛兰,雷克萨斯,路虎,保时捷,奥迪,宝马,大陆,博士等
Linux 约20% 免费,灵活 应用生态不完善,技术支持差 丰田,日产,特斯拉
Android 目前较低 开源,有强大的移动生态环境 安全性较差,无法适配仪表盘等安全要求高的部件 奥迪,通用,蔚来,小鹏,吉利,比亚迪,博泰,英伟达等
WinCE 约16% Windows应用开发便利 即将退出历史舞台 福特Sync 1, Sync2等

就目前来说,AGL的市场影响力还很小。仅仅在以下两款车型上使用:

不过,随着越来越多的车厂以及供应商加入AGL,以Linux免费和开源的特性,也会吸引更多的开发者。

相信在未来会逐步扩大市场份额。

AGL版本历史

AGL以一年两个版本的节奏进行开发。每个版本包含3~4个月的开发时间和一段时间的功能冻结阶段。

AGL以统一代码库(Unified Code Base)的形式对外发布版本。这是结合了汽车制造商以及设备供应商的Linux发行版本。

详细的版本历史如下:

  • 2014年6月30日,初始版本发布
  • 2016年1月,Agile Albacore (UCB 1.0)发布
  • 2016年7月,Brilliant Blowfish Release (UCB 2.0)发布
  • 2017年1月,Charming Chinook Release (UCB 3.0)发布
  • 2017年8月, Daring Dab Release(UCB 4.0)发布
  • 2018年1月, Electric Eel Release(UCB 5.0)发布
  • 2018年10月,Funky Flounder Release (UCB 6.0)发布

最新版本的特性可以访问这里:Unified Code Base

系统构建

AGL系统的构建流程如下:

  • 准备开发的宿主系统
  • 下载AGL编译环境
  • 初始化编译环境
  • 编译系统镜像

下面我们就按这个流程来进行构建。

准备开发的宿主系统

AGL的开发环境需要使用Yocto Project编译系统,所以需要以此为基础来准备开发环境。

目前,Yocto项目支持的Linux版本包括下面这些:

  • Ubuntu 14.10
  • Ubuntu 15.04
  • Ubuntu 15.10
  • Ubuntu 16.04 (LTS)
  • Fedora release 22
  • Fedora release 23
  • Fedora release 24
  • CentOS release 7.x
  • Debian GNU/Linux 8.x (Jessie)
  • Debian GNU/Linux 9.x (Stretch)
  • openSUSE 13.2
  • openSUSE 42.1

考虑到Ubuntu Linux的使用的人群最多,因此笔者建议大家以这个系统来进行准备。

在安装好Ubuntu系统之后,还需要安装如下的开发软件包以便编译AGL系统:

sudo apt-get install gawk wget git-core diffstat unzip texinfo gcc-multilib \
     build-essential chrpath socat cpio python python3 python3-pip python3-pexpect \
     xz-utils debianutils iputils-ping libsdl1.2-dev xterm

下载AGL编译环境

AGL项目中包含了很多子项目,因此建议大家创建一个新的文件夹来存放相关文件。

这里以workspace_agl文件夹为例来构建编译环境。

export AGL_TOP=$HOME/workspace_agl
mkdir -p $AGL_TOP

AGL使用repo工具来管理源码,所以需要先下载这个工具:

mkdir -p ~/bin
export PATH=~/bin:$PATH
curl https://storage.googleapis.com/git-repo-downloads/repo > ~/bin/repo
chmod a+x ~/bin/repo

获取工具之后就可以根据需求来下载不同版本的环境。

如果是希望获取稳定版源码,可以通过下面这个命令:

cd $AGL_TOP
repo init -b flounder -u https://gerrit.automotivelinux.org/gerrit/AGL/AGL-repo
repo sync

如果是希望获取最新的版本,可以使用下面这个命令:

cd $AGL_TOP
repo init -u https://gerrit.automotivelinux.org/gerrit/AGL/AGL-repo
repo sync

初始化编译环境

下载完成之后,需要通过一个脚本来初始化编译环境,脚本位于下面这个路径:

$AGL_TOP/meta-agl/scripts/aglsetup.sh

通过这个脚本可以设置编译的参数,例如:目标硬件,编译产出的目录等等。

可以通过下面这个命令来查看帮助:

source meta-agl/scripts/aglsetup.sh -h

在执行aglsetup.sh脚本之前,需要先确定想要编译出AGL系统的哪些特性。

特性的选择可以通过指定分层来确定,目前主要使用的分层有下面三个:

  • meta-agl:包含AGL发行版的最小集合。
  • meta-agl-demo:包含了参考UI实现以及演示平台和应用。
  • meta-agl-devel:包含了开发中的模块。这个也会包含OEM需要的但不在AGL中的软件包。

关于特性的详细说明请看这里

这里我们只要编译出模拟器中可以运行的演示系统即可,因此执行下面这条命令:

source meta-agl/scripts/aglsetup.sh -f -m qemux86-64 agl-demo agl-devel

执行完这条命令之后,脚本便会生成编译需要的配置文件。

编译系统镜像

接下来输入下面这条命令:

bitbake agl-demo-platform

这个步骤会比较耗时,可能需要若干个小时,因为其中包含了下载和编译的过程。具体时间长度取决于你的网络环境和开发机器的硬件配置。

编译完成之后会自动生成系统镜像。

笔者在Ubuntu系统上搭建了编译环境,使用 gcc 4.9.2 编译失败。升级到 gcc 6.5.0之后编译成功。

启动编译产物

编译出的镜像位于 build/tmp/deploy/images/ 目录下。根据编译时的设置会存在于不同的子文件夹中。镜像的文件名以 vmdk 为后缀。

例如,根据上面的配置,本次编译出的镜像是:

build/tmp/deploy/images/qemux86-64/agl-demo-platform-qemux86-64-20190529011851.rootfs.wic.vmdk

这个文件可以通过QEMU启动,也可以通过VirtualBox启动。

笔者将上面这个vmdk文件拷贝的MacBook Pro上之后,通过VirtualBox新建了虚拟机,配置如下:

启动之后界面如下:

如果你没有自己的编译环境,也可以到这里:AGL/release 直接下载官方编译好的镜像。

系统架构

AGL系统的完整功能说明见这里:《Automotive Grade Linux Requirements Specification》。

这份文档中描述了AGL的整体系统架构,并对每一个模块都做了说明。

系统架构图如下所示:

从这幅图中我们可以看出,整个AGL系统可以分为四层。从上至下以此是:

  • 应用和界面层:包含各种类型的应用程序,以及用户接触到的操作界面。
  • 应用程序框架层:提供接口来管理运行中的应用程序。这一层又分为:本地应用,AGL应用框架,Web应用三个部分。
  • 服务层:包含了位于用户空间的所有应用程序可以访问的服务,这些服务分为平台服务和汽车服务两大类。
  • 操作系统层:包含了系统内核以及驱动设备。

浏览源码

事实上,通过repo工具获取的并非是系统的源码,仅仅是项目的元数据。真正的源码在编译的过程中才会拉取。拉取后,源码便位于build/tmp/work/目录下。

通过浏览源码会发现,AGL项目使用CMake做为编译的配置工具,因此可以通过查看每个工程的CMakeLists.txt来了解工程的目标产物。

对于没有自己搭建编译环境的读者,也可以到这里:AGL Gerrit 浏览所有工程。每个工程的描述页面都会包含git命令来获取相应的源码。

由于篇幅所限,下文选取系统中最核心应用程序框架和人机界面框架做一些介绍。

应用程序框架

应用程序框架(Application Framework)负责管理所有的应用程序。管理主要分为两类:

  • 静态的:包括应用程序的安装和卸载。
  • 动态的:应用程序的运行时管理,例如:应用启动和退出,应用间的切换等。

应用程序框架最核心的就是afm-system-daemonafm-user-daemon两个模块。这是两个独立的进程,它们通过D-Bus进行通信。如下图所示:

其中,

  • afm-system-daemon 负责应用的安装和卸载
  • afm-user-daemon 负责应用程序生命周期的管理,包括:启动,退出,暂停和恢复等。

应用程序框架的项目地址见这里:src/app-framework-main

可以通过下面这条命令直接获取其源码:

git clone "https://gerrit.automotivelinux.org/gerrit/src/app-framework-main"

AGL使用systemd来管理系统进程的启动。

afm-system-daemonafm-user-daemon的启动配置分别如下:

[Unit]
Description=Application Framework Master, system side
Requires=afm-system-setup.service

[Service]
#User=afm
#Group=afm
SyslogIdentifier=afm-system-daemon
ExecStart=/usr/bin/afb-daemon --name=afm-system-daemon --no-httpd --no-ldpaths --binding=@afm_libexecdir@/afm-binding.so --ws-server=sd:afm-main
Restart=on-failure
RestartSec=5
CapabilityBoundingSet=CAP_DAC_OVERRIDE CAP_MAC_OVERRIDE

[Install]
WantedBy=multi-user.target
[Unit]
Description=Application Framework Master, User side

[Service]
Type=dbus                                
BusName=org.AGL.afm.user          
ExecStart=/usr/bin/afm-user-daemon --user-dbus=unix:path=%t/bus unix:@afm_platform_rundir@/apis/ws/afm-main
Environment=AFM_APP_INSTALL_DIR=%%r
EnvironmentFile=-@afm_confdir@/unit.env.d/*
Restart=on-failure
RestartSec=5


[Install]
WantedBy=default.target

从上面这个配置可以看出:

  • afm-system-daemon/usr/bin/afb-daemon 可执行文件启动。
  • afm-user-daemon/usr/bin/afm-user-daemon 可执行文件启动。

afb-daemon 是Application Framework Binder Daemon的简写。

AGL系统进程通过D-Bus 暴露接口。关于这两个服务暴露的接口说明见这里:The afm daemons: The D-Bus interface

afm-system-daemon 暴露的接口包括下面这些

  • install
  • uninstall

afm-user-daemon 暴露的接口包括下面这些

  • runnables
  • detail
  • start
  • once
  • terminate
  • pause
  • resume
  • runners
  • state
  • install
  • uninstall

其中,afm-user-daemon的install和uninstall接口最终还是会请求afm-system-daemon

afm-user-daemon

afm-user-daemonmain函数位于afm-user-daemon.c文件中。

这是一个守护进程,守护进程在启动之后通常都会一直在loop上循环等待消息的处理。

/* start servicing */
if (jbus_start_serving(user_bus) < 0) {
  ERROR("can't start server");
  return 1;
}

/* run until error */
for(;;)
  sd_event_run(evloop, (uint64_t)-1);
return 0;

对外接口名称和内部实现的绑定在 afm-binding.c 文件中完成:

static const afb_verb_t verbs[] =
{
	{.verb=_runnables_, .callback=runnables, .auth=&auth_detail,    .info="Get list of runnable applications",          .session=AFB_SESSION_CHECK },
	{.verb=_detail_   , .callback=detail,    .auth=&auth_detail,    .info="Get the details for one application",        .session=AFB_SESSION_CHECK },
	{.verb=_start_    , .callback=start,     .auth=&auth_start,     .info="Start an application",                       .session=AFB_SESSION_CHECK },
	{.verb=_once_     , .callback=once,      .auth=&auth_start,     .info="Start once an application",                  .session=AFB_SESSION_CHECK },
	{.verb=_terminate_, .callback=terminate, .auth=&auth_kill,      .info="Terminate a running application",            .session=AFB_SESSION_CHECK },
	{.verb=_pause_    , .callback=pause,     .auth=&auth_kill,      .info="Pause a running application",                .session=AFB_SESSION_CHECK },
	{.verb=_resume_   , .callback=resume,    .auth=&auth_kill,      .info="Resume a paused application",                .session=AFB_SESSION_CHECK },
	{.verb=_runners_  , .callback=runners,   .auth=&auth_state,     .info="Get the list of running applications",       .session=AFB_SESSION_CHECK },
	{.verb=_state_    , .callback=state,     .auth=&auth_state,     .info="Get the state of a running application",     .session=AFB_SESSION_CHECK },
	{.verb=_install_  , .callback=install,   .auth=&auth_install,   .info="Install an application using a widget file", .session=AFB_SESSION_CHECK },
	{.verb=_uninstall_, .callback=uninstall, .auth=&auth_uninstall, .info="Uninstall an application",                   .session=AFB_SESSION_CHECK },
	{.verb=NULL }
};

这是一个数组,每一项对应了一个接口的绑定。.callback指定的是逻辑实现的回调函数。

afm-user-daemon中对应用程序生命周期的管理实现在以下这些函数中:

extern int afm_urun_start(struct json_object *appli, int uid);
extern int afm_urun_once(struct json_object *appli, int uid);
extern int afm_urun_terminate(int runid, int uid);
extern int afm_urun_pause(int runid, int uid);
extern int afm_urun_resume(int runid, int uid);
extern struct json_object *afm_urun_list(struct afm_udb *db, int all, int uid);
extern struct json_object *afm_urun_state(struct afm_udb *db, int runid, int uid);
extern int afm_urun_search_runid(struct afm_udb *db, const char *id, int uid);

例如,下面这段代码负责应用程序的启动:

int afm_urun_once(struct json_object *appli, int uid)
{
	const char *udpath, *state, *uscope, *uname;
	int rc, isuser;

	/* retrieve basis */
	rc = get_basis(appli, &isuser, &udpath, uid);
	if (rc < 0)
		goto error;

	/* start the unit */
	rc = systemd_unit_start_dpath(isuser, udpath);
	if (rc < 0) {
		j_read_string_at(appli, "unit-scope", &uscope);
		j_read_string_at(appli, "unit-name", &uname);
		ERROR("can't start %s unit %s for uid %d", uscope, uname, uid);
		goto error;
	}

	state = wait_state_stable(isuser, udpath);
	if (state == NULL) {
		j_read_string_at(appli, "unit-scope", &uscope);
		j_read_string_at(appli, "unit-name", &uname);
		ERROR("can't wait %s unit %s for uid %d: %m", uscope, uname, uid);
		goto error;
	}
	if (state != SysD_State_Active) {
		j_read_string_at(appli, "unit-scope", &uscope);
		j_read_string_at(appli, "unit-name", &uname);
		ERROR("start error %s unit %s for uid %d: %s", uscope, uname, uid, state);
		goto error;
	}

	rc = systemd_unit_pid_of_dpath(isuser, udpath);
	if (rc <= 0) {
		j_read_string_at(appli, "unit-scope", &uscope);
		j_read_string_at(appli, "unit-name", &uname);
		ERROR("can't getpid of %s unit %s for uid %d: %m", uscope, uname, uid);
		goto error;
	}

	return rc;

error:
	return -1;
}

如果你浏览这些源码就会发现,这里面还有些逻辑没有实现完全:

int afm_urun_pause(int runid, int uid)
{
	return not_yet_implemented("pause");
}

int afm_urun_resume(int runid, int uid)
{
	return not_yet_implemented("resume");
}

afb-daemon

afb-daemon可执行文件的源码位于另外一个工程中:src/app-framework-binder

可以通过下面这条命令获取其源码:

git clone "https://gerrit.automotivelinux.org/gerrit/src/app-framework-binder"

binder提供了将应用程序连接到所需服务的方法。利用binder,可以安全地为各种语言编写并几乎可在任何地方运行的应用程序提供服务。

这里的binder和Android中的binder不是一回事。

afb-daemon与应用程序的关系如下图所示:

afb-daemon 提供了同时提供了http接口和WebSocket接口。

afb-daemonmain函数位于main-afb-daemon.c文件中。其main函数中主体逻辑做了四件事情:

  1. 解析传入的参数。
  2. 根据name参数设置进程名称。回顾一下上文,启动afm-system-daemon的时候就是通过systemd的配置文件指定了name参数:--name=afm-system-daemon
  3. 设置为守护进程。
  4. 开始服务。

考虑到守护进程在同一时刻可能被多个客户端并发请求,所以afb-daemon以多线程的方式工作。具体是通过下面这个接口实现的:

int jobs_start(int allowed_count, int start_count,
  int waiter_count, void (*start)(int signum, void* arg), void *arg)

这里的几个参数说明如下:

  • allowed_count 最大支持的线程数量
  • start_count 初始启动的线程数量
  • waiter_count 最多等待的请求
  • start 处理请求的函数指针

main函数中对于这几个参数设定的值如下:

jobs_start(3, 0, 50, start, NULL);

afm-util

应用程序框架提供了一个命令行工具来方便开发和测试,这个工具的名称叫做afm-util

提供的功能包含下面这些:

list
runnables      list the runnable widgets installed

add wgt
install wgt    install the wgt file

remove id
uninstall id   remove the installed widget of id

info id
detail id      print detail about the installed widget of id

ps
runners        list the running instance

run id
start id       start an instance of the widget of id

kill rid
terminate rid  terminate the running instance rid

status rid
state rid      get status of the running instance rid

人机界面框架

人机界面(Human-Machine Interface,简称HMI)框架主要包含下面几个模块:

  • WindowManager
  • HomeScreen
  • SoundManager
  • InputManager

最新版本(2019年1月)的HMI架构文档见这里:AGL HMI Framework Architecture Document

2018年的演示幻灯片见这里:Proposal for AGL HMI-Framework

WindowManager

WindowManager正如其名称所示,该模块负责系统的窗口管理。通过下面的命令可以获取其源码:

git clone https://gerrit.automotivelinux.org/gerrit/apps/agl-service-windowmanager

WindowManager需要管理的硬件包括:显示设备,GPU,输入设备和显存。它的主要职责包括:

  • 绘制窗口
  • 管理好窗口的层级
  • 处理可视化效果动画
  • 显示帧率管理
  • 有效利用各种硬件,并减少硬件依赖
  • 多窗口,多屏幕管理
  • 兼容性管理

官网上该模块的文档 Window Manager Application Guide 已经有很详细的说明。

下面仅对该模块的做一个简单介绍。

WindowManager的整体架构如下图所示:

WindowManager模块包含四个部分:

  • afb-binder
  • 服务绑定库
  • 用于策略管理的共享库
  • 配置文件

应用程序可以利用 libwindowmanager.so 来使用WindowManager的接口。接口包括下面这些:

int requestSurface(const char* role);
int requestSurfaceXDG(const char* role, unsigned ivi_id);
int activateWindow(const char* role, const char* area);
int activateWindow(const char* role);
int deactivateWindow(const char* role);
int endDraw(const char* role);
struct Screen getScreenInfo();
int getAreaInfo(const char* role, Rect *out_rect);
void setEventHandler(const WMHandler& wmh);

HomeScreen

桌面模块的实现包含在下面几个项目中:

  • homescreen:HomeScreenGUI,这是一个Qt的应用程序
  • launcher:LauncherGUI,这是一个Qt的应用程序
  • gl-service-homescreen:HomeScreenBinder的绑定库
  • libhomescreen:提供来给应用程序与HomeScreenBinder通讯的库
  • libqthomescreen:提供给Qt应用程序与HomeScreenBinder通讯的库,基于libhomescreen

libhomescreen中提供了一组接口用来管理桌面:

	int init(const int port, const std::string& token);

	int tapShortcut(const char* application_id);
	int onScreenMessage(const char* display_message);
	int onScreenReply(const char* reply_message);

	void set_event_handler(enum EventType et, handler_func f);

	void registerCallback(
		void (*event_cb)(const std::string& event, struct json_object* event_contents),
		void (*reply_cb)(struct json_object* reply_contents),
		void (*hangup_cb)(void) = nullptr);

	int call(const std::string& verb, struct json_object* arg);
	int call(const char* verb, struct json_object* arg);
	int subscribe(const std::string& event_name);
	int unsubscribe(const std::string& event_name);

	int showWindow(const char* application_id, json_object* json);
	int hideWindow(const char* application_id);
	int replyShowWindow(const char* application_id, json_object* json);
	int showNotification(json_object* json);
	int showInformation(json_object* json);
	int getRunnables(void);

这些接口的说明请参见 libhomescreen.cpp 文件。

例如,当有应用被点击的时候,被会调用showWindow接口显示影响的应用:

void HomescreenHandler::tapShortcut(QString application_id)
{
    HMI_DEBUG("Launcher","tapShortcut %s", application_id.toStdString().c_str());
    struct json_object* j_json = json_object_new_object();
    struct json_object* value;
    value = json_object_new_string("normal.full");
    json_object_object_add(j_json, "area", value);

    mp_hs->showWindow(application_id.toStdString().c_str(), j_json);
}

AGL应用开发

AGL系统既支持Web应用,也支持Native应用。系统框架会处理好不同类型应用的管理。

很显然,在进行AGL应用开发之前必须先获取相应的系统镜像。如果没有自己的编译环境,可以从官网上下载编译好的镜像

AGL应用开发有两种模式:

  • 使用Software Development Kit(简称SDK)
  • 使用Cross Development System(简称XDS)

XDS是官方推荐的方式,XDS支持通过XDS dashboard 或XDS命令行在目标系统上构建,部署和运行个人项目。

关于如何开发AGL的应用程序请参见这里:Developer Guides

QNX Hypervisor

前面已经提到,目前IVI市场上占有率最高是黑莓的QNX系统。这主要是因为其安全稳定的特性。

不过,QNX本身是收费的商业软件,并不开源,因此对其深度化定制就比较难了。

好在QNX系统了Hypervisor功能,这个功能类似于电脑上的虚拟机功能:在QNX的基础上运行其他的操作系统,例如Linux或者Android系统。如下图所示:

借助这个功能,在使用QNX的基础上,进行AGL的深度定制或许是一个不错的方案。

参考资料与推荐读物


原文地址:《Automotive Grade Linux 系统介绍》 by 保罗的酒吧
 Contents