OpenHarmony标准系统启动结构梳理(一)
文档概述
说明:
1.文章由移远通信技术股份有限公司提供
2.以下内容包含了个人理解,仅供参考,如有不合理处,请联系笔者修改
1. 说明与范围
本文只讨论标准系统启动主线,关注 base/startup/init 及其直接拉起的关键组件。
几个边界先说明清楚:
- 这里的主入口是 PID 1 的
init,不是bootstrap_lite。bootstrap_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() 的判断很直接:
- 如果当前还是 first-stage,就执行
SystemPrepare()。 - 如果已经带着
--second-stage参数重新进入init,就继续 second-stage 初始化。 - 随后按固定顺序执行:
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-init、init、post-init以及后续的文件系统阶段; - 启动
boot和normal两类服务; - 最后进入参数服务主循环。
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-init、init、post-init、fs、post-fs、late-fs、post-fs-data、boot,本质上都是启动流程中的阶段性 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 节详细展开的五个阶段,每个阶段在图中都有对应的节点区间:
-
第一阶段:环境拉起(对应 4.1 节)—— 图中从
A["内核启动"]到H["execv('/bin/init', '--second-stage')"]。这一段的决策节点B{"当前阶段判断"}是 first-stage 与 second-stage 的分水岭:只有 first-stage 才会执行SystemPrepare()完成基础文件系统、设备节点、分区挂载等硬依赖准备,然后通过execv重新进入init以进入 second-stage。 -
第二阶段:init 自身初始化(对应 4.2 节)—— 图中
I["SystemInit()"]到J["关闭 stdio、建立 keyring…"]。这一阶段不涉及任何配置解析或服务启动,只将init进程自身转变为一个可长期运行的调度进程。 -
第三阶段:参数、策略与配置装载(对应 4.3 节)—— 图中
K["SystemConfig() 前半段"]到P["ReadConfig() 解析 .cfg"]。这里有两个关键决策点值得关注:M["解析 group 配置"]:group 配置必须在 job/service 解析之前完成,否则CheckNodeValid()无法判断哪些 job/service 应被裁剪。这是启动配置的“准入过滤器”。N["加载 SELinux policy"]:SELinux 必须在正式服务拉起之前加载,否则服务进程的上下文和文件标记可能不完整,导致权限异常。
-
第四阶段:阶段性 job 和服务拉起(对应 4.4 节)—— 图中
Q["触发 pre-init job"]到X["启动 start-mode=normal 的服务"]。这一段的触发链非常密集:pre-init→init→ boot 服务 →post-init→ normal 服务,而post-init内部又通过W节点展开为early-fs → fs → post-fs → late-fs → post-fs-data → early-boot → boot这一系列子阶段。图中W节点集中体现了post-init作为“阶段分发器”的角色。 -
第五阶段:进入稳定态(对应 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() 前半段,顺序非常关键:
- 初始化参数服务;
- 解析 group 配置;
- 等待 cgroup/早期文件准备就绪;
- 加载 SELinux policy;
- 加载默认参数;
- 记录内核启动时间;
- 调整 init 优先级;
- 调用
ReadConfig()解析所有启动配置。
这里最值得注意的是顺序依赖:
- group 配置必须早于 job/service 解析,否则裁剪条件不生效;
- SELinux 必须早于正式服务启动,否则服务上下文和文件标记可能不完整;
- 参数装载必须早于
import参数展开,否则${...}替换没有值可用。
4.4 第四阶段:阶段性 job 和服务拉起
在 ReadConfig() 结束后,init 会依次触发:
pre-initinit- 启动
start-mode=boot的服务 post-init- 启动
start-mode=normal的服务
而 post-init 本身又继续触发:
early-fsfspost-fslate-fspost-fs-datafirmware_mounts_completeearly-bootboot
也就是说,post-init 更像一个阶段分发器,真正细粒度的启动推进发生在后面这一系列 trigger 上。
从默认的 init.cfg 来看,几个关键点比较明确:
pre-init会启动ueventd、watchdog_service,并进行/data的早期准备;init会完成一批内核参数、sysctl、目录和环境变量的初始化;post-fs、post-fs-data负责补齐系统和应用运行所需的目录结构;boot阶段做最后一批内核和系统参数收尾。
4.5 第五阶段:进入稳定态
SystemRun() 只做一件事:StartParamService()。
这意味着 second stage 结束后,init 并没有退出,而是以常驻进程身份继续负责:
- 参数服务;
- Service 生命周期管理;
- 按 trigger 响应后续事件;
- 异常服务重启和保活。
5. CFG 配置文件解析
5.1 init 的 .cfg 语法和角色
标准系统主线中的 init 配置采用 JSON 格式,至少包含三类核心数据:
importjobsservices
默认主配置文件是 /etc/init.cfg。在当前仓库中,base/startup/init/services/etc/init.cfg 是此配置的模板来源。
默认的 init.cfg 主要完成两件重要工作:
- 首先通过
import引入板级和子模块配置; - 然后定义系统公共阶段的 job,例如
pre-init、init、post-init、boot。
因此,/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(),调用链很短但职责分层清晰:
ReadFileToBuf()读取文件;cJSON_Parse()解析为 JSON 对象;ParseInitCfgContents()分发解析内容;- 释放 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 数组,对每个服务执行:
- 按名字查重;
- 不存在则
AddService()创建运行时对象; ParseOneService()解析基础属性;ParseServiceSocket()解析 socket;ParseServiceFile()解析文件依赖;- 保存
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() 的处理过程较为直接:
- 打开文件并校验大小;
- 将整个文件读入内存;
- 按行分割;
- 跳过空行和注释;
ParseUeventConfig()识别 section 或数据行;- 根据当前 section 将行交给对应的解析函数。
section 和解析函数是一一对应的:
device->ParseDeviceConfig()sysfs->ParseSysfsConfig()firmware->ParseFirmwareConfig()
6.3 解析结果的落点与处理
ueventd 不会把配置变成 job/service,而是分别存入三个链表:
g_devicesg_sysDevicesg_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 组织多个专用组件进入各自的工作状态。
下面是 init、ueventd、appspawn 三者的职责分工与协作关系图:
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. 建议的阅读顺序
如果后续要继续深入研究,建议按以下顺序阅读源码:
base/startup/init/services/init/main.cbase/startup/init/services/init/standard/init_firststage.cbase/startup/init/services/init/standard/init.cbase/startup/init/services/init/init_config.cbase/startup/init/services/etc/init.cfgbase/startup/init/services/init/init_group_manager.cbase/startup/init/ueventd/ueventd_main.cbase/startup/init/ueventd/ueventd_read_cfg.cbase/startup/appspawn/appspawn.cfg
这几处基本可以覆盖标准系统启动的主干流程。
更多推荐

所有评论(0)