文档概述
说明:
1.文章由移远通信技术股份有限公司提供
2.以下内容包含了个人理解,仅供参考,如有不合理处,请联系笔者修改

1. 说明与范围

本文只讨论标准系统启动主线,关注 base/startup/init 及其直接拉起的关键组件。

几个边界先说明清楚:

  • 这里的主入口是 PID 1 的 init,不是 bootstrap_litebootstrap_lite 面向小型系统,是另一套启动路径。
  • 标准系统的启动编排核心是 init.cfg 的解析和执行;ueventd.config 也属于启动期配置,但它的语法和 init 的 JSON .cfg 不是一套。
  • 仓库中存在 .rc 文件和 SystemExecuteRcs() 这条兼容路径,但在当前标准系统主线里,真正承担服务编排职责的核心仍是 .cfg

如果只保留一句话来概括这套实现,可以这样理解:

init 先将机器从“刚出内核”的状态带到“可以读配置、可以创建设备节点、可以启动服务”的状态,再根据 .cfg 将系统拆分为 job、service、trigger 三类运行单元,最终将启动过程推进到稳定态。

2. 启动入口

标准系统入口在 base/startup/init/services/init/main.c

main() 的判断很直接:

  1. 如果当前还是 first-stage,就执行 SystemPrepare()
  2. 如果已经带着 --second-stage 参数重新进入 init,就继续 second-stage 初始化。
  3. 随后按固定顺序执行:
    • SystemInit()
    • SystemExecuteRcs()
    • SystemConfig()
    • SystemRun()

这意味着标准系统并非单次线性执行完毕,而是典型的两阶段模型:

  • first-stage 负责搭建最小可运行环境;
  • second-stage 才开始参数服务、SELinux、配置解析、服务拉起和常驻循环。

下面是 main() 函数的两阶段决策流程,清晰展示 first-stage 与 second-stage 的入口分叉:

flowchart TD
    A["内核启动,PID 1 进入 init main()"] --> B{"当前阶段判断"}
    B -->|"first-stage"| C["SystemPrepare()"]
    C --> D["建立基础文件系统与设备节点"]
    D --> E["执行 first-stage hook"]
    E --> F["解析 fstab,等待关键 uevent"]
    F --> G["挂载必需分区"]
    G --> H["execv('/bin/init', '--second-stage')"]
    H --> I["SystemInit()"]
    I --> J["SystemExecuteRcs()"]
    J --> K["SystemConfig()"]
    K --> L["SystemRun()"]
    L --> M["进入稳定态:参数服务 + service 生命周期管理"]
    B -->|"second-stage"| I

3. 启动主架构

从职责上看,标准系统启动可以拆分为以下几层:

3.1 first-stage init

这一阶段在 SystemPrepare() 中完成,其职责是尽快将系统从“只有内核”推进到“可以切换至 second-stage”:

  • 建立基础文件系统和设备节点;
  • 执行 first-stage hook;
  • 解析必需挂载项,触发早期 uevent,等待关键块设备出现;
  • 挂载必需分区;
  • 通过 execv("/bin/init", "--second-stage") 进入 second-stage。

此阶段不进行完整的服务编排,其目标是“让系统具备继续启动的前提条件”。

3.2 second-stage init

second-stage init 是标准系统的真实调度中心,职责集中在 SystemInit()SystemConfig()

  • 初始化日志、keyring、socket 目录、control fd 等;
  • 初始化参数服务;
  • 加载 group 配置,决定当前 boot group 下哪些 job/service 可见;
  • 加载 SELinux policy;
  • 读取默认参数配置;
  • 解析 .cfg 配置文件;
  • 触发 pre-initinitpost-init 以及后续的文件系统阶段;
  • 启动 bootnormal 两类服务;
  • 最后进入参数服务主循环。

3.3 ueventd

ueventd 是启动阶段的设备节点和 sysfs 权限处理器,并非总调度器,但它是 first-stage 和 early-boot 必须依赖的基础设施。

它负责两件事:

  • 接收并重放内核 uevent,创建设备节点;
  • 依据 ueventd.config 调整设备节点和 sysfs 属性的 owner/mode。

对标准系统来说,它的意义不是“启动更多服务”,而是“让后续服务有正确的设备入口和权限环境”。

3.4 job / service / trigger

init 运行时不是把配置文件当脚本逐行顺序执行,而是先把配置解析成内部对象,再靠 trigger 驱动执行:

  • job:一组命令,通常对应某个阶段或某个条件触发点;
  • service:可启动、监控和重启的长期进程;
  • trigger:把事件和 job 关联起来的机制。

init.cfg 里的 pre-initinitpost-initfspost-fslate-fspost-fs-databoot,本质上都是启动流程中的阶段性 job。

3.5 group 机制

group 不是另一个启动阶段,而是启动配置的裁剪机制。

InitParseGroupCfg() 会根据 boot group 读入 /system/etc/<group>.cfg,把允许生效的 jobs/services/groups 加载到内存中。后续在解析 job 和 service 时,会通过 CheckNodeValid() 判断当前配置项是否应该进入本次启动流程。

这层机制的价值显而易见:

  • 同一套镜像里可以同时放多组配置;
  • 不同启动模式只暴露自己需要的 job/service;
  • 充电、正常启动等模式不必靠大量运行时 if-else 去隔离。

3.6 appspawn

appspawn 不在 PID 1 主线里,但它是标准系统从“系统服务启动完成”到“应用进程可以被拉起”的关键节点。

在现有配置里:

  • appspawn.cfg 定义了 appspawn 服务本身、socket、cgroup 和 start-mode
  • appspawn.rc 又在 late-fs 触发 start appspawn

从启动结构上看,可以将其理解为由 second-stage init 编排的一个关键基础服务:它并非启动主控,但后续所有应用进程的拉起都需经由它。

3.7 启动主线流程图

下面是标准系统启动主线的整体流程图,从 first-stage init 到进入稳定态(SystemRun),清晰展示五个主要阶段及关键函数调用和触发事件:

flowchart TD
    A["内核启动,PID 1 进入 init main()"] --> B{"当前阶段判断"}
    B -->|"first-stage"| C["SystemPrepare()"]
    C --> D["建立基础文件系统与设备节点"]
    D --> E["执行 first-stage hook"]
    E --> F["解析 fstab,等待关键 uevent"]
    F --> G["挂载必需分区"]
    G --> H["execv('/bin/init', '--second-stage')"]
    H --> I["SystemInit()"]
    I --> J["关闭 stdio、建立 keyring、创建 socket 目录、初始化 control fd"]
    J --> K["SystemConfig() 前半段"]
    K --> L["初始化参数服务"]
    L --> M["解析 group 配置"]
    M --> N["加载 SELinux policy"]
    N --> O["装载默认参数"]
    O --> P["ReadConfig() 解析 .cfg"]
    P --> Q["触发 pre-init job"]
    Q --> R["启动 ueventd、watchdog_service"]
    R --> S["触发 init job"]
    S --> T["内核参数、sysctl、目录初始化"]
    T --> U["启动 start-mode=boot 的服务"]
    U --> V["触发 post-init job"]
    V --> W["post-init 分发:early-fs → fs → post-fs → late-fs → post-fs-data → early-boot → boot"]
    W --> X["启动 start-mode=normal 的服务"]
    X --> Y["SystemRun() → StartParamService()"]
    Y --> Z["进入稳定态:参数服务 + service 生命周期管理 + trigger 响应"]

这张图对应正文第 4 节中详细展开的五个阶段,可作为阅读后续章节的路线图。

流程图与第 4 节五阶段的对应关系

上图覆盖了第 4 节详细展开的五个阶段,每个阶段在图中都有对应的节点区间:

  1. 第一阶段:环境拉起(对应 4.1 节)—— 图中从 A["内核启动"]H["execv('/bin/init', '--second-stage')"]。这一段的决策节点 B{"当前阶段判断"} 是 first-stage 与 second-stage 的分水岭:只有 first-stage 才会执行 SystemPrepare() 完成基础文件系统、设备节点、分区挂载等硬依赖准备,然后通过 execv 重新进入 init 以进入 second-stage。

  2. 第二阶段:init 自身初始化(对应 4.2 节)—— 图中 I["SystemInit()"]J["关闭 stdio、建立 keyring…"]。这一阶段不涉及任何配置解析或服务启动,只将 init 进程自身转变为一个可长期运行的调度进程。

  3. 第三阶段:参数、策略与配置装载(对应 4.3 节)—— 图中 K["SystemConfig() 前半段"]P["ReadConfig() 解析 .cfg"]。这里有两个关键决策点值得关注:

    • M["解析 group 配置"]:group 配置必须在 job/service 解析之前完成,否则 CheckNodeValid() 无法判断哪些 job/service 应被裁剪。这是启动配置的“准入过滤器”。
    • N["加载 SELinux policy"]:SELinux 必须在正式服务拉起之前加载,否则服务进程的上下文和文件标记可能不完整,导致权限异常。
  4. 第四阶段:阶段性 job 和服务拉起(对应 4.4 节)—— 图中 Q["触发 pre-init job"]X["启动 start-mode=normal 的服务"]。这一段的触发链非常密集:pre-initinit → boot 服务 → post-init → normal 服务,而 post-init 内部又通过 W 节点展开为 early-fs → fs → post-fs → late-fs → post-fs-data → early-boot → boot 这一系列子阶段。图中 W 节点集中体现了 post-init 作为“阶段分发器”的角色。

  5. 第五阶段:进入稳定态(对应 4.5 节)—— 图中 Y["SystemRun() → StartParamService()"]Z["进入稳定态"]SystemRun() 只做一件事——启动参数服务,之后 init 以常驻进程身份继续负责 service 生命周期管理、trigger 响应和异常重启。

图中几个关键决策点的意义

  • **B{"当前阶段判断"}**:这是 第一阶段与第二阶段 的入口分叉。first-stage 只做最小环境准备,不涉及任何配置解析和服务编排;second-stage 才是完整的系统调度中心。这种两阶段设计保证了早期依赖最小化——即使系统还没进入完整 根环境,也能把启动推进下去。

  • **M["解析 group 配置"]**:group 配置的生效时机非常关键。它在 ReadConfig() 解析 .cfg 之前执行,意味着同一份 .cfg 文件在不同启动模式下(正常启动、充电模式、救援模式等)可以产生不同的 作业/服务可见集合。这是"配置驱动替代硬编码启动顺序"的核心实现之一。

  • **W["post-init 分发…"]**:这个节点不是简单的"执行一个 job",而是把 post-init 拆解为一系列文件系统阶段的触发链。从 早期文件系统引导 共 8 个子阶段,每个阶段都可以独立挂载文件系统、启动关键服务。这种设计让文件系统准备和服务启动可以交错进行,而不是等所有文件系统就绪后再统一启动服务。

4. 启动阶段梳理

按执行顺序看,标准系统启动可以划分为五个阶段。

4.1 第一阶段:环境启动

对应 SystemPrepare(),主要工作是:

  • 创建基础节点;
  • 执行 first-stage hook;
  • 解析 fstab 中 required 分区;
  • 借助 uevent 重放并等待关键设备出现;
  • 挂载必需的分区;
  • 切入 second-stage。

这一阶段的目标只有一个:将 rootfs 的早期环境准备就绪。

4.2 第二阶段:init 自身初始化

对应 SystemInit(),主要工作是:

  • 关闭标准输入输出;
  • 建立 session keyring;
  • 设置 umask;
  • 创建 /dev/unix/socket
  • 初始化 fd holder socket;
  • 初始化 control fd;
  • 设置时区相关默认状态。

这一段仍然没有开始执行系统配置,它只是把 init 自身变成一个可长期运行的守护进程。

4.3 第三阶段:参数、策略与配置装载

对应 SystemConfig() 前半段,顺序非常关键:

  1. 初始化参数服务;
  2. 解析 group 配置;
  3. 等待 cgroup/早期文件准备就绪;
  4. 加载 SELinux policy;
  5. 加载默认参数;
  6. 记录内核启动时间;
  7. 调整 init 优先级;
  8. 调用 ReadConfig() 解析所有启动配置。

这里最值得注意的是顺序依赖:

  • group 配置必须早于 job/service 解析,否则裁剪条件不生效;
  • SELinux 必须早于正式服务启动,否则服务上下文和文件标记可能不完整;
  • 参数装载必须早于 import 参数展开,否则 ${...} 替换没有值可用。

4.4 第四阶段:阶段性 job 和服务拉起

ReadConfig() 结束后,init 会依次触发:

  • pre-init
  • init
  • 启动 start-mode=boot 的服务
  • post-init
  • 启动 start-mode=normal 的服务

post-init 本身又继续触发:

  • early-fs
  • fs
  • post-fs
  • late-fs
  • post-fs-data
  • firmware_mounts_complete
  • early-boot
  • boot

也就是说,post-init 更像一个阶段分发器,真正细粒度的启动推进发生在后面这一系列 trigger 上。

从默认的 init.cfg 来看,几个关键点比较明确:

  • pre-init 会启动 ueventdwatchdog_service,并进行 /data 的早期准备;
  • init 会完成一批内核参数、sysctl、目录和环境变量的初始化;
  • post-fspost-fs-data 负责补齐系统和应用运行所需的目录结构;
  • boot 阶段做最后一批内核和系统参数收尾。

4.5 第五阶段:进入稳定态

SystemRun() 只做一件事:StartParamService()

这意味着 second stage 结束后,init 并没有退出,而是以常驻进程身份继续负责:

  • 参数服务;
  • Service 生命周期管理;
  • 按 trigger 响应后续事件;
  • 异常服务重启和保活。

5. CFG 配置文件解析

5.1 init.cfg 语法和角色

标准系统主线中的 init 配置采用 JSON 格式,至少包含三类核心数据:

  • import
  • jobs
  • services

默认主配置文件是 /etc/init.cfg。在当前仓库中,base/startup/init/services/etc/init.cfg 是此配置的模板来源。

默认的 init.cfg 主要完成两件重要工作:

  1. 首先通过 import 引入板级和子模块配置;
  2. 然后定义系统公共阶段的 job,例如 pre-initinitpost-initboot

因此,/etc/init.cfg 不是“把所有内容都写完”的总文件,而更像总装入口。

5.2 配置源选择

ReadConfig() 会先读 ohos.boot.mode,然后根据启动模式决定配置源:

  • 正常启动:先读 /etc/init.cfg,再按优先级扫描 etc/init
  • charger_mode:读 /etc/init.cfg/system/etc/charger,再扫描 etc/init
  • charger:只读 /system/etc/charger
  • penglai:扫描 etc/penglai
  • rescue:读 /system/etc/rescue
  • maintenance_recovery:读 /system/etc/maintenance_recovery
  • updater:回退到 /etc 目录下的 .cfg

这一步决定的是“本次启动会读取哪些配置文件”,而非简单的固定目录遍历。

5.3 单文件解析链路

单个 .cfg 文件的解析入口是 ParseInitCfg(),调用链很短但职责分层清晰:

  1. ReadFileToBuf() 读取文件;
  2. cJSON_Parse() 解析为 JSON 对象;
  3. ParseInitCfgContents() 分发解析内容;
  4. 释放 JSON 对象和缓冲区。

ParseInitCfgContents() 只做三件事:

  • ParseAllServices()
  • ParseAllJobs()
  • ParseAllImports()

这意味着 init 的解析不是“一边读一边执行”,而是标准的“读取 -> 建模 -> 再由 trigger 执行”。

5.4 import 解析

ParseAllImports() 会遍历 import 数组,对每个字符串执行参数展开,再递归调用 ParseInitCfg()

这里有两个实现细节值得注意:

  • import 支持参数替换,例如 /vendor/etc/init.${ohos.boot.hardware}.cfg
  • import 是递归解析,不是预处理阶段一次性展开。

所以板级差异、SoC 差异、产品差异最终都能收敛到统一入口,由主 init.cfg 将配置组合起来。

5.5 job 解析

ParseAllJobs() 最终会进入 trigger / job 解析框架。

这一层不是简单地把 cmds 存储下来,而是将:

  • job 名称;
  • 触发条件;
  • 命令列表;

组织成可按事件触发的结构。

同时,job 是否允许生效,还需经过 group 校验。也就是说,配置中定义了 job 并不等于本次启动一定会执行。

5.6 service 解析

ParseAllServices() 会遍历 services 数组,对每个服务执行:

  1. 按名字查重;
  2. 不存在则 AddService() 创建运行时对象;
  3. ParseOneService() 解析基础属性;
  4. ParseServiceSocket() 解析 socket;
  5. ParseServiceFile() 解析文件依赖;
  6. 保存 onrestart 等附加命令。

解析后的 service 会进入 g_serviceSpace 和 group 管理结构,后续由 TriggerServices() 根据 start-mode 拉起。

这里要区分两类服务:

  • boot / normal 服务:在启动阶段批量投递为 job 后启动;
  • on-demand 服务:先创建 socket,等请求到来时再启动。

所以 .cfg 里的 start-mode 不是注释性字段,它直接影响 second-stage 的启动时机。

5.7 优先级目录扫描

除主配置外,ParseCfgByPriority() 还会通过 GetCfgFiles() 获取策略目录,再对这些目录执行 .cfg 扫描。

这层机制的意义是:

  • 把公共配置和板级配置拆开;
  • 让不同 policy 目录按既定顺序叠加;
  • 避免把所有服务都硬编码进一个总文件。

对工程维护来说,这比单文件巨型配置更易于管理。

下面是 .cfg 配置文件的完整解析链路,从 ReadConfig() 入口到最终生成运行时对象:

flowchart LR
    A["ReadConfig()"] --> B{"读取 ohos.boot.mode"}
    B -->|"正常启动"| C["读取 /etc/init.cfg"]
    B -->|"charger_mode"| D["读取 /etc/init.cfg + /system/etc/charger"]
    B -->|"charger"| E["读取 /system/etc/charger"]
    B -->|"rescue"| F["读取 /system/etc/rescue"]
    C --> G["ParseInitCfg()"]
    D --> G
    E --> G
    F --> G
    G --> H["ReadFileToBuf()"]
    H --> I["cJSON_Parse()"]
    I --> J["ParseInitCfgContents()"]
    J --> K["ParseAllImports()"]
    J --> L["ParseAllJobs()"]
    J --> M["ParseAllServices()"]
    K --> N["参数展开 + 递归 ParseInitCfg()"]
    L --> O["按 trigger 组织 job 结构"]
    M --> P["AddService() + ParseOneService()"]
    P --> Q["ParseServiceSocket()"]
    P --> R["ParseServiceFile()"]
    O --> S["g_jobSpace"]
    P --> T["g_serviceSpace"]
    N --> G

6. ueventd.config 的解析链路

ueventd 的配置解析不能与 init 的 JSON .cfg 混为一谈,它是另一条独立的链路。

6.1 文件与语法

ueventd 默认读取:

  • /etc/ueventd.config
  • /vendor/etc/ueventd.config
  • /vendor/etc/ueventd_factory.config

语法是分段文本,不是 JSON,典型的 section 有:

  • [device]
  • [sysfs]
  • [firmware]

例如 [device] 下每一行是:

<device node> <mode> <uid> <gid> [parameter]

6.2 解析过程

ParseUeventdConfigFile() 的处理过程较为直接:

  1. 打开文件并校验大小;
  2. 将整个文件读入内存;
  3. 按行分割;
  4. 跳过空行和注释;
  5. ParseUeventConfig() 识别 section 或数据行;
  6. 根据当前 section 将行交给对应的解析函数。

section 和解析函数是一一对应的:

  • device -> ParseDeviceConfig()
  • sysfs -> ParseSysfsConfig()
  • firmware -> ParseFirmwareConfig()

6.3 解析结果的落点与处理

ueventd 不会把配置变成 job/service,而是分别存入三个链表:

  • g_devices
  • g_sysDevices
  • g_firmwares

后续在处理 uevent 时:

  • 设备节点权限查询走 GetDeviceNodePermissions()
  • sysfs 权限调整走 ChangeSysAttributePermissions()

也就是说,ueventd.config 的本质不是“启动脚本”,而是“设备节点和 sysfs 权限策略表”。

下面是 ueventd.config 的解析链路,从文件读取到最终权限策略生效:

flowchart TD
    A["ueventd 启动"] --> B["ParseUeventdConfigFile()"]
    B --> C["打开文件并校验大小"]
    C --> D["整文件读入内存"]
    D --> E["按行分割"]
    E --> F{"跳过空行和注释"}
    F --> G["ParseUeventConfig()"]
    G --> H{"识别 section 类型"}
    H -->|"[device]"| I["ParseDeviceConfig()"]
    H -->|"[sysfs]"| J["ParseSysfsConfig()"]
    H -->|"[firmware]"| K["ParseFirmwareConfig()"]
    I --> L["存入 g_devices 链表"]
    J --> M["存入 g_sysDevices 链表"]
    K --> N["存入 g_firmwares 链表"]
    L --> O["uevent 到达时调用 GetDeviceNodePermissions()"]
    M --> P["uevent 到达时调用 ChangeSysAttributePermissions()"]
    N --> Q["固件加载时使用"]

7. 标准系统启动的工程化理解

在抽离源码细节后,这套启动框架实际上体现了三层稳定的设计。

7.1 两阶段 init 保证早期依赖最小化

第一阶段 只解决“挂载、设备、阶段切换”这些硬依赖,不把复杂策略和服务编排前置。这样即使系统还没进入完整 root 环境,也能把启动推进。

7.2 配置驱动替代硬编码启动顺序

真正的启动顺序不是硬编码在 C 代码里的,而是通过:

  • job trigger
  • service start-mode
  • import 递归
  • group 裁剪

共同决定。

这使得平台、产品和板级间的差异能收敛在配置层,而无需侵入 PID 1 的主逻辑。

7.3 init 负责编排,ueventd 负责设备,appspawn 负责应用孵化

三者职责边界比较清楚:

  • init 解决“什么时候做什么”;
  • ueventd 解决“设备节点是否可见、权限是否正确”;
  • appspawn 解决“应用进程如何按规则被创建”。

标准系统启动结构的核心,不是把所有事情都堆进 init,而是由 init 组织多个专用组件进入各自的工作状态。

下面是 initueventdappspawn 三者的职责分工与协作关系图:

flowchart TD
    subgraph Init["init(PID 1)"]
        A["两阶段启动编排"]
        B[".cfg 配置解析"]
        C["job / service / trigger 管理"]
        D["参数服务主循环"]
    end
    subgraph Ueventd["ueventd"]
        E["接收内核 uevent"]
        F["创建设备节点"]
        G["调整 sysfs 权限"]
    end
    subgraph Appspawn["appspawn"]
        H["应用进程孵化"]
        I["socket 监听"]
        J["按规则创建进程"]
    end
    A -->|"first-stage 依赖"| E
    A -->|"second-stage 启动"| E
    A -->|"late-fs 阶段启动"| H
    C -->|"trigger 驱动"| B
    D -->|"常驻循环"| C
    E --> F
    E --> G
    H --> I
    H --> J

8. 建议的阅读顺序

如果后续要继续深入研究,建议按以下顺序阅读源码:

  1. base/startup/init/services/init/main.c
  2. base/startup/init/services/init/standard/init_firststage.c
  3. base/startup/init/services/init/standard/init.c
  4. base/startup/init/services/init/init_config.c
  5. base/startup/init/services/etc/init.cfg
  6. base/startup/init/services/init/init_group_manager.c
  7. base/startup/init/ueventd/ueventd_main.c
  8. base/startup/init/ueventd/ueventd_read_cfg.c
  9. base/startup/appspawn/appspawn.cfg

这几处基本可以覆盖标准系统启动的主干流程。

Logo

社区规范:仅讨论OpenHarmony相关问题。

更多推荐