通用内核映像

Android 通用内核 (ACK) 是所有 Android 产品内核的基础。供应商内核和设备内核位于 ACK 的下游。供应商通过修改内核源代码并添加设备驱动程序,添加了对 SoC 和外围设备的支持。这些修改内容可能很多,以至于设备上运行的代码中有多达 50% 是树外代码(并非来自上游 Linux 和 AOSP 通用内核)

因此,设备内核由以下部分组成:

  • 上游:来自 kernel.org 的 Linux 内核
  • AOSP:AOSP 通用内核的其他 Android 专用补丁程序
  • 供应商:供应商提供的 SoC 和外围设备支持以及优化补丁程序
  • 原始设备制造商 (OEM)/设备:其他设备驱动程序和自定义项

几乎所有设备都具有自定义内核。这就导致了内核碎片化问题。

Android 内核层次结构导致碎片化问题

图 1. Android 内核层次结构导致碎片化问题

碎片化的代价

内核碎片化会对 Android 社区产生若干负面影响。

安全更新需要耗费大量人力

Android 安全公告 (ASB) 中引用的安全补丁程序必须向后移植到每个设备内核中。但是,由于存在内核碎片化问题,向正常使用的 Android 设备传播安全修复的代价非常之高。

很难合并长期支持的更新

长期支持 (LTS) 版本包含安全修复和其他重大问题修复。事实证明,使用最新的 LTS 版本是提供安全修复的最有效方式。我们发现,ASB 报告的内核安全问题中有 90% 都已在保持最新状态的 Pixel 设备上得到修复。

不过,由于设备内核中所有的自定义修改,很难仅将 LTS 修复合并到设备内核中。

妨碍 Android 平台进行版本升级

由于碎片化问题,很难向正常使用的设备添加需要更改内核的 Android 新功能。Android 框架代码必须假设支持的内核版本多达 5 个,并且没有针对新的平台版本进行任何内核更改(Android 10 支持内核版本 3.18、4.4、4.9、4.14 和 4.19;在某些情况下,这些版本自 2017 年 Android 8 发布以来还未添加新功能)。

很难将内核更改贡献回上游 Linux

对内核进行完所有更改后,大多数旗舰设备附带的内核版本已经至少存在 18 个月了。例如,kernel.org 于 2017 年 11 月发布了 4.14 版内核,而首批使用 4.14 版内核的 Android 手机于 2019 年春季才发布。

上游内核发布与产品发布之间的这种长时间延迟导致 Android 社区很难将所需的功能和驱动程序馈送到上游内核中,因此解决碎片化问题并非易事。

解决碎片化问题:通用内核映像

通用内核映像 (GKI) 项目通过统一核心内核并将 SoC 和板级支持从核心内核移至可加载模块中,解决了内核碎片化问题。GKI 内核为内核模块提供了稳定的内核模块接口 (KMI),因此模块和内核可以独立进行更新。

GKI 具有以下特点:

  • 基于 ACK 来源构建而成。
  • 每个架构和每个 LTS 版本的单内核二进制文件以及关联的可加载模块(目前只有适用于 android11-5.4android12-5.4 的 arm64)。
  • 经过关联 ACK 支持的所有 Android 平台版本的测试。在 GKI 内核版本的生命周期内不会发生功能弃用。
  • 为给定 LTS 中的驱动程序提供了稳定版 KMI。
  • 不包含 SoC 专用代码或板卡专用代码。

下图即为实现了 GKI 的 Android 设备。

GKI 架构

图 2. GKI 架构

GKI 是一项复杂的更改,将从 Android 11 平台版本中的 v5.4 内核开始,分几个阶段逐步推出。

GKI 1.0 - GKI 兼容性要求

对于 Android 11 平台版本,为了保证与 Treble 兼容,必须对运行 v5.4 内核的设备进行 GKI 测试。

用于 GKI 兼容性测试的分区

图 3. 用于 GKI 兼容性测试的分区

具备 GKI 兼容性是指设备通过将 GKI 启动映像刷写到 boot 分区并将 GSI 系统映像刷写到 system 分区来安装通用系统映像 (GSI) 和 GKI 内核,因此通过了 VTS 和 CTS-on-GSI+GKI 测试。设备可以附带不同的产品内核,并且可以使用 GKI 未提供的可加载模块。不过,产品内核和 GKI 内核都必须从相同的 vendor_bootvendor 分区加载模块。因此,所有产品内核都必须具有相同的二进制内核模块接口 (KMI)。供应商可以扩展产品内核的 KMI,前提是它与 GKI KMI 兼容。GKI 1.0 不要求供应商模块可卸载。

GKI 1.0 的目标

  • 当产品内核被 GKI 内核取代时,不在 VTS 或 CTS 中引入性能降低问题。
  • 减少 OEM 和供应商为了使 AOSP 通用内核保持最新状态而进行的内核维护工作。
  • 无论是升级到新 Android 平台版本的设备还是新发布的设备,都在内核中加入核心 Android 变更。
  • 绝不破坏 Android 用户空间。
  • 将硬件专用组件作为可加载模块从核心内核中分离出来。

GKI 2.0 - GKI 产品

搭载 Android S (2021) 平台版本且使用内核版本 v5.x(5.x 是 2020 年年底被选为 LTS 的内核版本)或更高版本的设备必须附带 GKI 内核。将提供已签名的启动映像,并通过 LTS 和重大问题修复定期对其进行更新。由于 KMI 将保持二进制稳定性,因此无需对供应商映像进行任何更改,即可安装这些启动映像。

GKI 2.0 的目标

  • 不为 GKI 引入明显的性能或能效降低问题。
  • 使 OEM 无需供应商参与即可提供内核安全修复和问题修复 (LTS)。
  • 降低更新设备主要内核版本(例如,将 v5.x 更新为 v5.y)所需的费用。
  • 通过按照清晰的升级过程更新内核版本,只为每个架构维护一个 GKI 内核二进制文件。

GKI 设计

KMI 内核分支

GKI 内核基于 ACK KMI 内核分支构建而来,详情请参阅 Android 通用内核。KMI 由内核版本和 Android 平台版本唯一标识,因此分支的命名方式为 <androidRelease>-<kernel version>例如,Android 11 的 5.4 KMI 内核分支名为 android11-5.4.。对于 Android S(尚未提交,列于此处只是为了展示新分支模型未来如何演变发展),预计还会另外增加两个 KMI 内核:android12-5.4,以及另一个基于新 LTS 内核(应该会在 2020 年年底声明)的内核。

KMI 内核层次结构

图 4 显示了 5.x KMI 内核的分支层次结构(5.x 是 2020 年年底被选为 LTS 的内核版本)。android12-5.x 是与 Android S 对应的 KMI 内核,android13-5.x 则对应 Android 13(尚未提交,列于此处只是为了展示新分支模型未来如何演变发展)。

5.x KMI 内核层次结构

图 4. 5.x KMI 内核层次结构

如图 4 所示,KMI 分支会经历三个阶段:开发阶段 (dev)、稳定阶段 (stab) 和冻结阶段。Android 通用内核中对这些阶段进行了详细介绍。

KMI 内核被冻结后,除非发现严重的安全问题,并且在不影响稳定版 KMI 的情况下无法解决该问题,否则不会接受任何 KMI 破坏性更改。分支在其整个生命周期内都将保持冻结状态。

冻结的分支中可以接受问题修复和合作伙伴功能,前提是不破坏现有 KMI。只要不影响构成当前 KMI 的接口,就可以使用新的导出符号扩展 KMI。将新接口添加到 KMI 后,它们会立即进入稳定状态,并且不能被将来的更改破坏。

例如,不允许执行向 KMI 接口所用的结构添加字段的更改,因为这会改变接口定义:

struct foo {
  int original_field1;
  int original_field2;
  int new_field; // Not allowed
};

int do_foo(struct foo &myarg)
{
  do_something(myarg);
}
EXPORT_SYMBOL_GPL(do_foo);

但可以添加新函数:

struct foo_ext {
  struct foo orig_foo;
  int new_field;
};

int do_foo2(struct foo_ext &myarg)
{
  do_something_else(myarg);
}
EXPORT_SYMBOL_GPL(do_foo2);

KMI 稳定性

为了实现 GKI 的目标,为驱动程序维持稳定版 KMI 至关重要。GKI 内核以二进制文件的形式构建和推出,但供应商可加载模块在单独的树中构建。生成的 GKI 内核和模块必须正常运行,就如同它们是一起构建的一样。如需进行 GKI 兼容性测试,需要将包含内核的启动映像替换为包含 GKI 内核的启动映像,并且无论使用哪种内核,供应商映像中的可加载模块都必须正常运行。

KMI 并非包含内核中的所有符号,甚至并非包含完整的 3 万多个导出符号。相反,可供模块使用的符号都明确列在一组符号列表文件中,这些文件在内核树的根目录中公开维护。所有符号列表文件中所有符号的并集定义了一组维持为稳定版的 KMI 符号。abi_gki_aarch64_db845c 就是符号列表文件的一个示例,该文件声明了 DragonBoard 845c 必需的符号。

只有符号列表中列出的符号及其相关结构和定义才会被视为 KMI 的一部分。如果符号列表中没有您需要的符号,您可以对其发布更改。当新接口加入符号列表,并因此成为 KMI 描述的一部分后,它们会维持为稳定版;在分支被冻结后,不得将其从符号列表中移除,也不得进行修改。

每个 KMI 内核树都有自己的一组符号列表。系统不会尝试在不同的 KMI 内核分支之间提供 ABI 稳定性。例如,android11-5.4 的 KMI 完全独立于 android12-5.4 的 KMI。

通常情况下,Linux 社区对 Mainline 内核的内核内 ABI 稳定性这一概念并不满意。面对不同的工具链、配置和不断发展的 Linux Mainline 内核,在 Mainline 中维持稳定版 KMI 并不可行。但是,这在十分受限的 GKI 环境中还是有可能的。限制条件如下:

  • KMI 仅在相同的 LTS 内核(例如 android11-5.4)内保持稳定。
    • 不对 android-mainline 维持 KMI 稳定性。
  • 只使用 AOSP 中提供的并且为相应分支指定的特定 Clang 工具链来构建内核和模块。
  • 只有在符号列表中指定的已知由模块使用的符号才会受到稳定性监控,并被视为 KMI 符号。
    • 最后的结果就是,模块只能使用 KMI 符号。这一点通过在需要非 KMI 符号时使模块加载失败来强制执行。
  • KMI 分支被冻结后,将不能进行任何会破坏 KMI 的更改,这些包括:
    • 配置更改
    • 内核代码更改
    • 工具链更改(包括更新)

KMI 监控

ABI 监控工具可在预提交测试期间监控 KMI 稳定性。破坏 KMI 的更改无法通过预提交测试,必须重新处理才能实现兼容性。合作伙伴和公众可以将这些工具集成到其构建流程中。在开发过程中以及合并 LTS 版本时,Android 内核团队使用这些工具来查找 KMI 遭到破坏的情况。如果在 LTS 合并期间检测到 KMI 遭到破坏的情况,则可以移除违规补丁程序,或重构补丁程序以实现兼容性,从而保留该 KMI。

如果以不兼容的方式修改了现有 KMI 符号,相应 KMI 会被视为已遭到破坏。示例:

  • 向 KMI 函数添加了新参数
  • 向 KMI 函数所用的结构添加了新字段
  • 添加了新的枚举值,进而更改了 KMI 函数所用的枚举的值
  • 更改了配置,进而更改了影响 KMI 的数据成员的存在状态

添加新符号不一定会破坏 KMI,但必须将所用的新符号添加到符号列表和 ABI 表示形式中。否则,系统便无法识别将来对 KMI 这一部分的更改。

合作伙伴应使用 ABI 监控工具来比较其产品内核与 GKI 内核之间的 KMI,并确保它们兼容。

最新的 android11-5.4 二进制 GKI 内核可以从 ci.android.com (kernel_aarch64 build) 下载。

单编译器

更改编译器可能会改变影响 ABI 的内部内核数据结构布局。由于保持 KMI 稳定性非常重要,因此用于构建 GKI 内核的工具链必须与用于构建供应商模块的工具链完全兼容。GKI 内核是使用 AOSP 中包含的 LLVM 工具链构建的。

从 Android 10 开始,所有 Android 内核都必须使用 LLVM 工具链构建。使用 GKI 后,用于构建产品内核和供应商模块的 LLVM 工具链必须生成与 AOSP 中的 LLVM 工具链相同的 ABI,并且合作伙伴必须确保 KMI 与 GKI 内核兼容。

Android 内核 build 文档描述了参考 GKI 内核的构建方式。具体来说,文档中介绍的过程可确保使用正确的工具链和配置来构建内核。我们建议下游合作伙伴使用相同的工具来构建最终内核,以避免工具链或其他构建时依赖项导致的不兼容问题。

内核配置

GKI 内核是使用 arch/arm64/configs/gki_defconfig 构建的。由于这些配置会影响 KMI,因此对 GKI 配置的管理非常谨慎。对于冻结的 KMI 内核,只有在不影响 KMI 的情况下,才能添加或移除配置。

GKI 内核必须配置为可在各种设备上运行。因此,该内核必须内置对所有这些设备所需的所有子系统和选项的支持,其中不包括用于启用硬件的可加载模块。合作伙伴应请求对开发内核进行所需的配置更改

启动更改

为了帮助 GKI 与供应商组件完全分离开来,boot 分区仅包含通用组件,其中包括内核和带有 GKI 模块的 ramdisk。我们定义了新版启动头文件 (v3),用于表明符合 GKI 架构规范。GKI 版本的启动映像由 Google 提供,会在测试 GKI 兼容性时取代供应商版本的启动映像。

第一阶段 initrecoveryfastbootd 的 ramdisk 是一个 initramfs 映像,包含两个由引导加载程序串联的 CPIO 归档。第一个 CPIO 归档来自新的 vendor_boot 分区。第二个则来自 boot 分区。

此处提供了更改摘要。如需了解详情,请参阅供应商启动分区

boot 分区

boot 分区包括头文件、内核以及内含启动 ramdisk 通用部分的 CPIO 归档。

boot 分区使用 v3 版启动头文件后,先前的 boot 分区的以下部分将不复存在:

  • 第二阶段引导加载程序:如果设备具有第二阶段引导加载程序,则必须将相应引导加载程序存储在自己的分区中。
  • DTB:DTB 存储在供应商启动分区中。

boot 分区包含一个 CPIO 归档,内含以下 GKI 组件:

  • 位于 /lib/modules/ 的 GKI 内核模块
  • first_stage_init 及其依赖的库
  • fastbootdrecovery(用于 A/B 和虚拟 A/B 设备)

GKI 启动映像由 Google 提供,必须用于 GKI 兼容性测试

最新的 arm64 android11-5.4 boot.img 可以从 ci.android.comaosp-master 分支的 aosp_arm64 构建工件中下载。

最新的 arm64 android11-5.4 内核映像 (Image.gz) 可以从 ci.android.comaosp_kernel-common-android11-5.4 分支的 kernel_aarch64 构建工件中下载。

供应商启动分区

vendor_boot 分区随 GKI 引入。该分区是采用虚拟 A/B 的 A/B 分区,包含一个头文件、供应商 ramdisk 和设备树 Blob。供应商 ramdisk 是一个 CPIO 归档,其中包含设备启动所需的供应商模块。这包括用于启用关键 SoC 功能的模块,以及启动设备和显示启动画面所需的存储和显示驱动程序。

该 CPIO 归档包含:

  • 第一阶段 init 供应商内核模块,位于 /lib/modules/
  • modprobe 配置文件,位于 /lib/modules
  • modules.load 文件,用于指示要在第一阶段 init 期间加载的模块

引导加载程序要求

引导加载程序必须在加载完供应商 ramdisk CPIO 映像(从 vendor_boot 分区)后,立即将通用 ramdisk CPIO 映像(从 boot 分区)加载到内存中。解压缩后,结果是通用 ramdisk 叠加在供应商 ramdisk 的文件结构之上。

GKI 兼容性测试

对于 Android 11 平台版本,搭载 v5.4 内核的设备必须使用 Google 提供的 GKI 启动映像运行 VTS 和 CTS-on-GSI 测试。

为 GKI 内核贡献代码

GKI 内核基于 AOSP 通用内核构建而成,起始版本为 android11-5.4。提交的所有补丁程序都必须符合这些贡献准则,其中记录了两种策略:

  1. 最佳做法:对上游 Linux 进行所有更改。如果适用,向后移植到稳定版本。这些补丁程序会自动合并到相应的 AOSP 通用内核中。如果补丁程序已在上游 Linux 中,则发布符合以下补丁程序要求的补丁程序的向后移植。
  2. 次佳做法:在树外(从上游 Linux 的角度)开发补丁程序。除非这些补丁程序修复的是 Android 特有的错误,否则不太可能被接受,但通过 kernel-team@android.com 进行了协调的情况除外。

对于实现 GKI 的合作伙伴来说,可能有合理的理由需要树外补丁程序(尤其是存在必须满足的芯片时间表时)。对于此类情况,请提交 Buganizer 问题。

首先通过 Gerrit 将 GKI 更改提交到 android-mainline 分支,然后根据需要向后移植到其他版本分支。

与可加载模块关联的源代码不需要贡献到 GKI 内核源代码树,但强烈建议将所有驱动程序提交到上游 Linux。

请求从 Mainline 向后移植

一般来说,GKI 合作伙伴所需的 Mainline 补丁程序可以向后移植到 GKI 内核。请按照贡献准则将补丁程序上传到 Gerrit。

如果补丁程序已发布到上游,但尚未被接受,建议您等到它被接受为止。如果时间表不允许等待上游接受补丁程序,请提交 Buganizer 问题。

添加和移除 GKI 配置

如需请求在 arch/arm64/configs/gki_defconfig 中添加或移除配置,请提交 Buganizer 问题,清楚地说明具体的请求及理由。如果没有可访问的 Buganizer 项目,请将带有配置更改的补丁程序发布到 Gerrit,并确保提交消息清楚说明了需要该配置的原因。

对冻结 KMI 内核的配置更改不得影响 KMI。

修改核心内核代码

不建议修改 AOSP 通用内核中的核心内核代码。可以先将补丁程序发送到上游 Linux,然后将其向后移植。如果有合理的理由表明需要更改核心内核,请提交 Buganizer 问题,清楚地说明具体的请求及理由。未通过 Buganizer 问题提交的 Android 专用功能不会被接受。如果您无法创建 Buganizer 问题,请发送电子邮件至 kernel-team@android.com

通过 EXPORT_SYMBOL_GPL() 导出符号

不要向上游发送仅包含符号导出项的补丁程序。为了让上游 Linux 考虑该补丁程序,添加 EXPORT_SYMBOL_GPL() 时需要一个使用该符号的树内模块化驱动程序,因此请将新驱动程序或对现有驱动程序的更改添加到与导出项相同的补丁程序集内。

在向上游发送补丁程序时,提交消息必须包含明确的理由,说明为什么需要该补丁程序以及它对社区有何益处。对上游维护人员而言,使导出项对树外驱动程序或功能有益这一理由并没有说服力。

如果由于某种原因,无法向上游发送该补丁程序,请提交 Buganizer 问题,并说明无法向上游发送该补丁程序的原因。一般来说,作为内核子系统支持接口的符号很可能会被接受为导出项。但是,随机的内部辅助函数不太可能被接受,您需要应要求重构代码以避免使用它们。

向 KMI 符号列表添加符号

必须使用供应商模块所用的 GKI 内核符号来更新 KMI 符号列表。由于只有 KMI 符号才会被维持为稳定版,因此 GKI 不允许加载依赖于非 KMI 符号的模块。

extract_symbols 脚本可从内核 build 树中提取相关符号,还可用于更新 KMI 符号列表。如需详细了解,请参阅符号列表文档