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

1. 说明与范围

本文是上一篇 init 启动结构梳理的续篇,主题收敛到 base/startup/appspawn

这篇不再展开系统总启动流程,而是只回答下面几个问题:

  • appspawn 在标准系统里处于什么位置;
  • appspawn 是怎么被 init 拉起来的;
  • 客户端请求是怎么进入 appspawn 的;
  • appspawn 怎么把一条请求变成一个新进程;
  • sandbox、模块扩展、hook 机制是怎么接到孵化主链上的。

如果只保留一句话,可以这样概括:

appspawn 是标准系统里的应用孵化器,它本身不决定系统何时启动,但它负责把来自框架侧的一条启动请求,经过消息解析、权限准备、sandbox 装配、fork/clone 和子进程入口切换,落成一个真正可运行的业务进程。

1.1 核心架构图

下面这张 Mermaid 流程图展示了 appspawn 生态中几个核心角色之间的交互关系,包括 init 如何拉起服务、客户端如何发起请求、appspawn 服务内部如何通过模块管理器、hook 阶段和 sandbox 子系统完成孵化,以及最终子进程的诞生路径。

flowchart TD
    A["init 进程"] -->|"启动 appspawn 服务"| B["appspawn main"]
    B -->|"解析 -mode"| C["StartSpawnService"]
    C -->|"加载模块"| D["模块管理器<br/>AppSpawnModuleMgr"]
    D -->|"注册 hook"| E["hook 阶段表<br/>STAGE_SERVER_PRELOAD<br/>STAGE_PARENT_PRE_FORK<br/>STAGE_CHILD_EXECUTE<br/>..."]
    C -->|"预加载配置"| F["sandbox 子系统<br/>sandbox_manager"]
    C -->|"进入事件循环"| G["AppSpawnRun<br/>事件循环"]
    
    H["客户端<br/>(应用框架/系统服务)"] -->|"本地 socket 连接"| I["服务端 socket<br/>OnConnection"]
    I -->|"SO_PEERCRED 校验"| J["消息接收<br/>HandleRecvMessage"]
    J -->|"recvmsg 收包"| K["消息解码<br/>DecodeAppSpawnMsg"]
    K -->|"spawn 请求"| L["ProcessSpawnReqMsg"]
    
    L -->|"STAGE_PARENT_PRE_FORK"| M["fork/clone"]
    M -->|"创建子进程"| N["子进程 AppSpawnChild"]
    N -->|"STAGE_CHILD_PRE_COLDBOOT"| O["环境清理"]
    O -->|"STAGE_CHILD_EXECUTE"| P["sandbox 构建<br/>SpawnBuildSandboxEnv"]
    P -->|"STAGE_CHILD_PRE_RELY"| Q["pipe 回执给父进程"]
    Q -->|"父进程确认"| R["ProcessChildResponse"]
    R -->|"STAGE_PARENT_PRE_RELY"| S["父进程向客户端回包"]
    S -->|"STAGE_PARENT_POST_RELY"| T["父进程清理"]
    Q -->|"STAGE_CHILD_POST_RELY"| U["子进程进入 runChildProcessor"]
    U -->|"执行业务逻辑"| V["业务进程运行"]
    
    style A fill:#e1f5fe,stroke:#0288d1
    style H fill:#e1f5fe,stroke:#0288d1
    style V fill:#c8e6c9,stroke:#388e3c
    style D fill:#fff3e0,stroke:#f57c00
    style E fill:#fff3e0,stroke:#f57c00
    style F fill:#fff3e0,stroke:#f57c00
    style G fill:#f3e5f5,stroke:#7b1fa2

图例说明:

  • 蓝色节点:系统外部角色(init、客户端、业务进程)
  • 橙色节点:appspawn 内部核心子系统(模块管理器、hook 阶段、sandbox)
  • 紫色节点:事件循环主框架
  • 绿色节点:最终产出(业务进程)

这张图覆盖了从系统启动到一次完整孵化请求的全链路,可以作为后续章节的导航参考。

2. appspawn 在启动结构里的位置

从系统分层看,appspawn 不是 PID 1,也不是 system ability 管理器,它的职责边界比较清楚:

  • init 负责启动编排,把 appspawn 服务拉起;
  • appspawn 负责进程孵化,把框架请求变成新进程;
  • 应用框架或系统服务通过本地 socket 与 appspawn 通信;
  • 真正的应用业务代码并不跑在 appspawn 里,而是跑在它孵化出来的子进程里。

因此,appspawn 更像一条“受控进程工厂”:

  • 输入是启动请求、属性、权限、fd、扩展参数;
  • 中间是安全检查、hook 执行、sandbox 建设、父子协同;
  • 输出是一个已经完成环境准备的业务进程 pid。

3. 启动入口

3.1 init 侧入口

appspawn 的服务定义放在 base/startup/appspawn/appspawn.cfg

从这份配置可以直接读出几个关键信息:

  • 进程路径是 /system/bin/appspawn
  • 启动参数里带 -mode appspawn
  • 服务类型是标准系统服务;
  • 本地 socket 名称是 AppSpawn
  • start-modeboot
  • 启动完成后会上报 bootevent.appspawn.started

这说明 appspawn 不是被上层框架临时拉起的,而是系统启动期就由 init 常驻拉起的基础服务。

此外,base/startup/appspawn/appspawn.rc 还定义了:

  • late-fs 阶段执行 start appspawn

也就是说,appspawn 在启动结构里既有 .cfg 服务定义,也保留了 .rc 触发点,最终效果是在系统文件系统阶段推进到足够靠后时进入可用状态。

3.2 appspawn 进程入口

标准系统入口在 base/startup/appspawn/standard/appspawn_main.c

main() 的核心逻辑不复杂,但分流关系很重要:

  1. 初始化通用环境;
  2. 处理 LD_PRELOAD
  3. 根据 -mode 选择启动模板;
  4. 调用 StartSpawnService() 创建服务上下文;
  5. 进入 content->runAppSpawn(...)

当前实现并不是只有一个 appspawn 模式,而是统一支持多种运行模板:

  • appspawn
  • nwebspawn
  • nativespawn
  • hybridspawn
  • 对应的 cold run 模式

因此,appspawn_main.c 的本质不是业务逻辑中心,而是模式分发器。

4. 服务初始化主链

真正把服务拉起来的是 StartSpawnService()

这条链路可以拆成六步:

  1. 重写进程名;
  2. 加载 MODULE_COMMON
  3. 安装额外模块;
  4. 创建 AppSpawnContentAppSpawnMgr
  5. 加载对应模式模块;
  6. 执行 STAGE_SERVER_PRELOAD

如果继续展开,顺序如下:

  1. main
  2. StartSpawnService
  3. AppSpawnLoadAutoRunModules(MODULE_COMMON)
  4. AppSpawnModuleMgrInstall(ASAN_MODULE_PATH)
  5. AppSpawnCreateContent
  6. CreateAppSpawnServer
  7. AppSpawnLoadAutoRunModules(arg->moduleType)
  8. ServerStageHookExecute(STAGE_SERVER_PRELOAD, content)
  9. 进入 AppSpawnRun

这里有两个设计点值得单独记下来。

第一,模块是先于正式运行主循环装配的。也就是说,appspawn 不是一个“主程序写死所有逻辑”的实现,而是先把通用模块和模式模块装好,再进入服务态。

第二,STAGE_SERVER_PRELOAD 是真正的“预加载栅栏”。sandbox 配置、公共策略、SELinux 配置等,都在这个阶段装进内存;如果这一步失败,服务不会继续向下跑。

下面这张流程图展示了 StartSpawnService()main 到进入事件循环的完整调用链,清晰呈现模块加载、服务创建、预加载栅栏等关键步骤:

flowchart TD
    A["main"] --> B["StartSpawnService"]
    B --> C["AppSpawnLoadAutoRunModules<br/>(MODULE_COMMON)"]
    C --> D["AppSpawnModuleMgrInstall<br/>(ASAN_MODULE_PATH)"]
    D --> E["AppSpawnCreateContent"]
    E --> F["CreateAppSpawnServer"]
    F --> G["AppSpawnLoadAutoRunModules<br/>(arg->moduleType)"]
    G --> H["ServerStageHookExecute<br/>(STAGE_SERVER_PRELOAD)"]
    H --> I["AppSpawnRun<br/>进入事件循环"]
    
    style A fill:#e3f2fd,stroke:#1565c0
    style B fill:#e3f2fd,stroke:#1565c0
    style C fill:#fff3e0,stroke:#f57c00
    style D fill:#fff3e0,stroke:#f57c00
    style E fill:#f3e5f5,stroke:#7b1fa2
    style F fill:#f3e5f5,stroke:#7b1fa2
    style G fill:#fff3e0,stroke:#f57c00
    style H fill:#ffebee,stroke:#c62828
    style I fill:#c8e6c9,stroke:#388e3c

图例说明:

  • 蓝色节点:入口函数
  • 橙色节点:模块加载相关
  • 紫色节点:服务对象创建
  • 红色节点:预加载栅栏(失败则服务不启动)
  • 绿色节点:最终进入事件循环

5. 运行时主结构

appspawn 代码时,最先要建立的不是函数图,而是对象图。

5.1 AppSpawnContent

AppSpawnContent 是整个服务的公共运行态,里面保存:

  • 当前运行模式;
  • 长进程名;
  • runAppSpawn 回调;
  • coldStartApp 回调;
  • notifyResToParent 回调;
  • 子进程执行器 runChildProcessor
  • sandbox namespace 相关状态。

可以把它看成所有孵化流程共享的一份“服务基座”。

5.2 AppSpawnMgr

AppSpawnMgr 是标准系统下的顶层管理对象,在 AppSpawnContent 之外又补了几类运行时状态:

  • server socket;
  • signal handler;
  • 已孵化进程队列;
  • 孵化中上下文队列;
  • died queue;
  • 扩展数据 extData
  • 统计信息。

工程上更贴切的理解是:AppSpawnContent 偏接口基座,AppSpawnMgr 才是标准版真正的服务实例。

5.3 AppSpawningCtx

AppSpawningCtx 表示“一次还在进行中的孵化请求”,里面有:

  • 当前请求消息;
  • fork 上下文;
  • 子进程 pid;
  • 当前状态;
  • 启动起始时间;
  • prefork 标记。

这个结构非常关键,因为 appspawn 不是同步 fork 完就立刻结束请求,而是父子进程之间还有 pipe 回执、watcher 和超时控制,整个过程要靠 AppSpawningCtx 串起来。

5.4 AppSpawnMsgNode

AppSpawnMsgNode 是完整消息对象,保存:

  • 消息头;
  • TLV 偏移表;
  • 原始 buffer;
  • 所属连接。

服务端收包不是“读到一段字节直接用”,而是先组装成 AppSpawnMsgNode,再做合法性校验和属性解析。

6. 通信模型

6.1 服务端模型

标准系统下,appspawn 通过本地 socket 对外提供服务。

服务端 socket 由 CreateAppSpawnServer() 创建,关键点有两个:

  • socket 名来自 init 传入的 control socket;
  • socket 挂进 loop event 框架,由事件循环统一驱动。

因此,appspawn 不是阻塞式串行服务器,而是基于事件循环处理连接、收发消息、timer 和 signal。

6.2 客户端模型

客户端实现位于 interfaces/innerkits/client/appspawn_client.c

客户端侧典型流程是:

  1. 按类型选择 socket 名;
  2. 创建 AF_UNIX stream socket;
  3. 设置 SO_PASSCRED 和超时;
  4. 连接 /dev/unix/socket/<socketName>
  5. 通过 sendmsg 发送消息和可选 fd;
  6. 读取响应。

这说明客户端和服务端之间传输的不只是普通字节流,还支持通过 SCM_RIGHTS 附带文件描述符。

6.3 连接侧校验

服务端在 OnConnection() 里会通过 SO_PEERCRED 取对端 uid,并做白名单校验。

允许接入的 uid 不是开放式的,当前实现只允许特定系统身份访问,另外 shell 只有在开发者模式下才放行。

这意味着:

  • appspawn 虽然挂在本地 socket 上;
  • 但它不是通用公开接口;
  • 默认只有受控系统侧组件可以发起孵化请求。

7. 消息接收与分发

7.1 收包路径

服务端的收包链路如下:

  1. OnConnection
  2. HandleRecvMessage
  3. OnReceiveRequest
  4. GetAppSpawnMsgFromBuffer
  5. DecodeAppSpawnMsg
  6. ProcessRecvMsg

其中几个点需要单独说明。

HandleRecvMessage() 使用的是 recvmsg(),不是简单 read(),因为它要同时处理:

  • 消息体;
  • 通过 SCM_RIGHTS 传过来的 fd。

OnReceiveRequest() 负责处理粘包和半包:

  • 如果消息没收完整,就把当前对象挂到 incompleteMsg
  • 同时启动 timer,防止一条不完整消息长期占住连接;
  • 如果后续依然收不完整,就关闭连接并清理上下文。

这部分实现说明 appspawn 的消息层并不是“协议非常简单所以偷懒处理”,而是按可恢复、可超时、可清理的方式做了完整接收状态机。

7.2 消息分发

完成解码后,消息会进入 ProcessRecvMsg()

这里处理的并不只有“启动应用”一种消息,还包括:

  • spawn 请求;
  • dump 类请求;
  • beget / debug 相关请求;
  • remount / mount 等扩展请求。

因此,appspawn 不是纯单功能孵化器,而是围绕“进程孵化与配套控制”扩展出了一组消息类型。

不过从主线角度看,最核心的还是 spawn 请求,也就是 ProcessSpawnReqMsg()

8. 孵化主链

8.1 父进程视角

一条标准的孵化请求,在父进程侧大致经过下面这条链:

  1. ProcessSpawnReqMsg
  2. CheckAppSpawnMsg
  3. CreateAppSpawningCtx
  4. AppSpawnHookExecute(STAGE_PARENT_PRE_FORK)
  5. RunAppSpawnProcessMsg
  6. AppSpawnHookExecute(STAGE_PARENT_POST_FORK)
  7. AddChildWatcher
  8. 等待子进程通过 pipe 回执
  9. ProcessChildResponse
  10. SendResponse

这里真正关键的不是 fork() 本身,而是 fork 前后和回执前后的几段同步动作:

  • fork 前要准备 flags、gid、sandbox staged mount;
  • fork 后父进程要关掉不再需要的 fd;
  • 子进程完成关键初始化后要先通知父进程;
  • 父进程确认成功,才把最终 pid 回给客户端。

因此,appspawn 的孵化不是“直接 fork 后让客户端自己等”,而是带确认语义的受控启动。

8.2 普通 fork 与 prefork

RunAppSpawnProcessMsg() 会先判断两个条件:

  • 系统 boot 是否完成;
  • 当前请求是否支持 prefork。

如果条件满足,就走 AppSpawnProcessMsgForPrefork()
否则走普通路径 NormalSpawnChild()

这意味着 prefork 不是默认硬开,而是一个受启动阶段和请求能力约束的优化路径。

工程上可以把它理解成:

  • 启动早期优先保证正确性;
  • 启动后期在能力允许时再换成更激进的孵化优化策略。

8.3 fork/clone 执行

真正执行进程创建的公共逻辑在 common/appspawn_server.c

这里有两条路径:

  • 普通进程走 fork()
  • NWeb 场景根据进程类型可能走 clone(),并附带 namespace flag。

也就是说,appspawn 并没有把进程创建动作完全统一成一种 syscall,而是保留了按场景分流的能力。

下面这张流程图展示了父进程视角下一条标准孵化请求的完整处理链路,从消息校验到最终回包:

flowchart TD
    A["ProcessSpawnReqMsg"] --> B["CheckAppSpawnMsg"]
    B --> C["CreateAppSpawningCtx"]
    C --> D["AppSpawnHookExecute<br/>(STAGE_PARENT_PRE_FORK)"]
    D --> E["RunAppSpawnProcessMsg"]
    E --> F["AppSpawnHookExecute<br/>(STAGE_PARENT_POST_FORK)"]
    F --> G["AddChildWatcher"]
    G --> H["等待子进程 pipe 回执"]
    H --> I["ProcessChildResponse"]
    I --> J["SendResponse"]
    
    D -.->|"fork 前准备"| K["准备 flags、gid<br/>sandbox staged mount"]
    F -.->|"fork 后收尾"| L["关闭不再需要的 fd"]
    H -.->|"子进程通知"| M["子进程完成初始化后<br/>通过 pipe 通知父进程"]
    
    style A fill:#e3f2fd,stroke:#1565c0
    style B fill:#e3f2fd,stroke:#1565c0
    style C fill:#f3e5f5,stroke:#7b1fa2
    style D fill:#fff3e0,stroke:#f57c00
    style E fill:#e3f2fd,stroke:#1565c0
    style F fill:#fff3e0,stroke:#f57c00
    style G fill:#f3e5f5,stroke:#7b1fa2
    style H fill:#ffebee,stroke:#c62828
    style I fill:#c8e6c9,stroke:#388e3c
    style J fill:#c8e6c9,stroke:#388e3c

图例说明:

  • 蓝色节点:主链核心步骤
  • 橙色节点:hook 执行阶段
  • 紫色节点:上下文管理
  • 红色节点:等待同步
  • 绿色节点:结果处理与回包
  • 虚线框:关键辅助动作

9. 子进程执行链

子进程入口在 AppSpawnChild()

这一段最值得仔细看,因为它基本决定了新进程在真正进入业务运行前做了哪些事情。

当前顺序是:

  1. AppSpawnExecuteClearEnvHook
  2. 如果是 cold start,尝试 cold start 路径
  3. AppSpawnExecuteSpawningHook
  4. AppSpawnExecutePreReplyHook
  5. 向父进程回执关键结果
  6. AppSpawnExecutePostReplyHook
  7. 执行 runChildProcessor

把它翻译成职责语言就是:

  • 先清理环境;
  • 再做孵化期核心准备;
  • 再做回执前必须完成的准备;
  • 父进程确认后,再继续执行子进程专属逻辑;
  • 最后切入对应模式的 child looper。

这套顺序有一个非常明确的设计目标:

父进程回给客户端的“启动成功”,不是单纯表示 fork() 成功,而是表示子进程已经完成了一组最低限度的初始化动作。

下面这张流程图展示了子进程从入口到切入业务 looper 的完整执行顺序,清晰呈现环境清理、核心准备、回执同步和最终切换的职责划分:

flowchart TD
    A["AppSpawnChild"] --> B["AppSpawnExecuteClearEnvHook"]
    B --> C{"是否 cold start?"}
    C -->|"是"| D["尝试 cold start 路径"]
    C -->|"否"| E["AppSpawnExecuteSpawningHook"]
    D --> E
    E --> F["AppSpawnExecutePreReplyHook"]
    F --> G["向父进程回执关键结果"]
    G --> H["AppSpawnExecutePostReplyHook"]
    H --> I["执行 runChildProcessor"]
    I --> J["切入对应模式的 child looper"]
    
    B -.->|"职责:清理环境"| K["清 env、设置 fd/env、token"]
    E -.->|"职责:孵化期核心准备"| L["建 sandbox、设置属性"]
    F -.->|"职责:回执前必须完成的准备"| M["回包前必须完成的准备"]
    H -.->|"职责:回执后补充动作"| N["设置启动时间等尾处理"]
    
    style A fill:#e3f2fd,stroke:#1565c0
    style B fill:#fff3e0,stroke:#f57c00
    style C fill:#ffebee,stroke:#c62828
    style D fill:#f3e5f5,stroke:#7b1fa2
    style E fill:#fff3e0,stroke:#f57c00
    style F fill:#fff3e0,stroke:#f57c00
    style G fill:#c8e6c9,stroke:#388e3c
    style H fill:#fff3e0,stroke:#f57c00
    style I fill:#e3f2fd,stroke:#1565c0
    style J fill:#c8e6c9,stroke:#388e3c

图例说明:

  • 蓝色节点:入口与执行
  • 橙色节点:hook 执行阶段
  • 红色节点:条件判断
  • 紫色节点:特殊路径
  • 绿色节点:回执与最终切换
  • 虚线框:各步骤的职责说明

10. 父子进程回执机制

appspawn 没有直接用“父进程看 fork() 返回值”来代表孵化成功,而是显式做了父子同步。

关键机制如下:

  • 父子之间建立 pipe;
  • 子进程通过 NotifyResToParent() 把阶段结果写回 pipe;
  • 父进程通过 watcher 监听这个 pipe;
  • ProcessChildResponse() 读取结果;
  • 成功后才把 pid 加入已孵化队列,并向客户端回包。

如果失败,也不会默默吞掉,而是明确返回错误码。

这套机制解决了一个很实际的问题:

fork() 成功不代表新进程已经处于可运行状态,真正需要回给上游的是“孵化准备已经完成到可交付程度”。

11. 进程回收与异常处理

AppSpawnRun() 会建立 signal 处理任务,核心监听:

  • SIGCHLD
  • SIGTERM

SIGCHLD 到来后,ProcessSignal() 会循环 waitpid(-1, ..., WNOHANG) 回收子进程,并调用 HandleDiedPid() 更新管理状态。

另外还有两类异常保护:

  • 连接断开时,OnClose() 会清理未完成消息,并杀掉该连接对应的孵化中子进程;
  • 子进程回执超时,WaitChildTimeout() 会直接 kill 子进程并给客户端返回超时错误。

这说明 appspawn 不只是一个“创建进程”的服务,它同时承担了孵化中生命周期托管和失败收口。

12. 模块装配机制

12.1 模块扫描

appspawn 的扩展不是通过巨型 switch 拼出来的,而是走模块管理器:

  • AppSpawnLoadAutoRunModules(type) 扫描指定模块组;
  • AppSpawnModuleMgrInstall() 可额外安装动态模块;
  • MODULE_CONSTRUCTOR 在模块加载时自动注册 hook。

当前模块分组至少包括:

  • appspawn/common
  • appspawn/appspawn
  • appspawn/nwebspawn
  • appspawn/nativespawn

这套组织方式带来的直接收益是:

  • 公共逻辑可以独立模块化;
  • 各种 spawn 模式可以共享框架、局部覆写;
  • 主程序不需要知道每个模块的内部细节。

12.2 hook 管理

所有模块最终都不是直接插进主循环,而是通过 hook manager 挂进阶段点。

appspawn_hook.h 看,阶段大致分成三类:

  • 服务阶段;
  • 父进程孵化阶段;
  • 子进程孵化阶段。

优先级也不是散乱定义,而是统一划成:

  • HOOK_PRIO_HIGHEST
  • HOOK_PRIO_COMMON
  • HOOK_PRIO_SANDBOX
  • HOOK_PRIO_PROPERTY
  • HOOK_PRIO_LOWEST

这意味着模块之间的先后关系,本质上是“阶段 + 优先级”联合决定的。

13. hook 阶段表

下面这张表可以当成读代码时的骨架。

阶段 作用 典型用途
STAGE_SERVER_PRELOAD 服务进入主循环前预加载 读配置、加载 sandbox、装载策略
STAGE_SERVER_EXIT 服务退出前清理 释放预加载资源
STAGE_SERVER_APP_ADD 新进程确认加入运行态 统计、事件上报
STAGE_SERVER_APP_DIED 进程退出 清理和事件上报
STAGE_PARENT_PRE_FORK fork 前父进程准备 计算 flag、补 gid、准备 sandbox
STAGE_PARENT_POST_FORK fork 后父进程收尾 关闭 fd、清理上下文
STAGE_PARENT_PRE_RELY 父进程回包前 最终状态修正
STAGE_PARENT_POST_RELY 父进程回包后 回包后清理
STAGE_CHILD_PRE_COLDBOOT 子进程早期环境准备 清 env、设置 fd/env、token
STAGE_CHILD_EXECUTE 子进程核心执行阶段 建 sandbox、设置属性
STAGE_CHILD_PRE_RELY 子进程回执前 回包前必须完成的准备
STAGE_CHILD_POST_RELY 子进程回执后 回执后补充动作
STAGE_CHILD_PRE_RUN 子进程正式切业务前 设置启动时间等尾处理

这张表比单看枚举更有意义,因为它能直接告诉你:

  • 哪些逻辑必须在父进程完成;
  • 哪些逻辑必须在子进程完成;
  • 哪些逻辑必须在“通知父进程之前”完成。

14. sandbox 配置装载

14.1 预加载阶段

sandbox 不是在每次收到请求后现读 JSON,而是在 STAGE_SERVER_PRELOAD 阶段预加载。

sandbox_manager.c 里这一段逻辑很清楚:

  • 普通 HAP 预加载 EXT_DATA_APP_SANDBOX
  • 隔离沙箱预加载 EXT_DATA_ISOLATED_SANDBOX
  • render/gpu 进程预加载 EXT_DATA_RENDER_SANDBOX / EXT_DATA_GPU_SANDBOX
  • debug HAP 预加载 EXT_DATA_DEBUG_HAP_SANDBOX

对应的配置文件主要是:

  • appdata-sandbox-app.json
  • appdata-sandbox-isolated-new.json
  • appdata-sandbox-render.json
  • appdata-sandbox-gpu.json
  • appdata-sandbox-debug.json

这说明 sandbox 设计是“类型分层”的,而不是一份总配置里靠大量条件硬分支。

14.2 JSON 解析

LoadAppSandboxConfig() 会通过 ParseJsonConfig("etc/sandbox", sandboxName, ...) 把目标 JSON 读入内存,并补全:

  • 依赖 group;
  • permission 索引;
  • namespace 支持情况;
  • full mount 开关。

这一层做的事情不是直接 mount,而是把配置先转成 appspawn 可操作的内部模型。

14.3 请求进入时的 sandbox 预处理

STAGE_PARENT_PRE_FORK,sandbox 模块会执行:

  • SpawnPrepareSandboxCfg
  • SpawnMountDirToShared

这里做的事包括:

  • 根据请求选择 sandbox 类型;
  • 根据权限补充 gid;
  • 更新消息 flag;
  • staged mount system-const
  • 提前准备 shared 目录挂载。

这一步必须放在 fork 之前,因为有一部分准备动作属于父进程视角,或者需要在父子共享上下文里先完成。

14.4 子进程建沙箱

真正把 sandbox 落到新进程里的动作在 STAGE_CHILD_EXECUTE,由 SpawnBuildSandboxEnv() 完成。

这一层会根据请求和模式决定:

  • 是否跳过 sandbox;
  • 使用哪一类 sandbox 配置;
  • 是否启用 pid namespace;
  • 实际执行 mount 和 namespace 构建。

换句话说,sandbox 模块不是“给孵化链挂几个目录”这么简单,它是整个子进程执行环境构建的一部分。

下面这张流程图展示了 sandbox 从服务预加载到子进程实际建沙箱的完整流程,覆盖预加载、JSON 解析、父进程预处理和子进程执行四个阶段:

flowchart TD
    subgraph A["阶段一:服务预加载 (STAGE_SERVER_PRELOAD)"]
        A1["预加载 sandbox 配置"] --> A2["选择 sandbox 类型"]
        A2 --> A3["普通 HAP: EXT_DATA_APP_SANDBOX"]
        A2 --> A4["隔离沙箱: EXT_DATA_ISOLATED_SANDBOX"]
        A2 --> A5["render/gpu: EXT_DATA_RENDER_SANDBOX<br/>EXT_DATA_GPU_SANDBOX"]
        A2 --> A6["debug HAP: EXT_DATA_DEBUG_HAP_SANDBOX"]
    end

    subgraph B["阶段二:JSON 解析"]
        B1["LoadAppSandboxConfig"] --> B2["ParseJsonConfig<br/>(etc/sandbox, sandboxName)"]
        B2 --> B3["补全依赖 group"]
        B3 --> B4["补全 permission 索引"]
        B4 --> B5["补全 namespace 支持情况"]
        B5 --> B6["补全 full mount 开关"]
    end

    subgraph C["阶段三:父进程预处理 (STAGE_PARENT_PRE_FORK)"]
        C1["SpawnPrepareSandboxCfg"] --> C2["根据请求选择 sandbox 类型"]
        C2 --> C3["根据权限补充 gid"]
        C3 --> C4["更新消息 flag"]
        C4 --> C5["staged mount system-const"]
        C5 --> C6["SpawnMountDirToShared<br/>提前准备 shared 目录挂载"]
    end

    subgraph D["阶段四:子进程建沙箱 (STAGE_CHILD_EXECUTE)"]
        D1["SpawnBuildSandboxEnv"] --> D2{"是否跳过 sandbox?"}
        D2 -->|"否"| D3["选择 sandbox 配置"]
        D3 --> D4{"是否启用 pid namespace?"}
        D4 -->|"是"| D5["构建 pid namespace"]
        D4 -->|"否"| D6["执行 mount 和 namespace 构建"]
        D5 --> D6
        D2 -->|"是"| D7["跳过 sandbox 构建"]
    end

    A --> B
    B --> C
    C --> D

    style A fill:#e3f2fd,stroke:#1565c0
    style B fill:#fff3e0,stroke:#f57c00
    style C fill:#f3e5f5,stroke:#7b1fa2
    style D fill:#c8e6c9,stroke:#388e3c

图例说明:

  • 蓝色区域:服务预加载阶段
  • 橙色区域:JSON 解析阶段
  • 紫色区域:父进程预处理阶段
  • 绿色区域:子进程建沙箱阶段

15. 文字版时序图

把最核心的 spawn 主线压成时序,可以写成下面这样:

  1. init 启动 appspawn
  2. appspawn main 解析 -mode
  3. StartSpawnService 加载 common 模块和模式模块
  4. STAGE_SERVER_PRELOAD 预加载公共配置和 sandbox
  5. AppSpawnRun 进入事件循环
  6. 客户端连接 AppSpawn 本地 socket
  7. 服务端校验 SO_PEERCRED
  8. 客户端发送消息与可选 fd
  9. 服务端 recvmsg 收包并重组完整消息
  10. DecodeAppSpawnMsg
  11. ProcessSpawnReqMsg
  12. STAGE_PARENT_PRE_FORK
  13. forkclone
  14. 子进程执行 STAGE_CHILD_PRE_COLDBOOT
  15. 子进程执行 STAGE_CHILD_EXECUTE
  16. 子进程执行 STAGE_CHILD_PRE_RELY
  17. 子进程通过 pipe 通知父进程结果
  18. 父进程 ProcessChildResponse
  19. 父进程把 pid 记录进运行态队列
  20. STAGE_PARENT_PRE_RELY
  21. 父进程向客户端回包
  22. STAGE_PARENT_POST_RELY
  23. 子进程继续 STAGE_CHILD_POST_RELY
  24. 子进程进入 runChildProcessor

这条时序足以帮助定位大部分问题:

  • 如果问题发生在回包前,多半在子进程早期 hook 或 pipe 回执链;
  • 如果问题发生在回包后但进程很快退出,多半在 child looper 或后续业务入口;
  • 如果问题只在特定类型进程触发,优先看 mode 模块和 sandbox 类型分支。

16. 工程化理解

把实现细节抽象掉后,appspawn 这套代码体现了四个稳定思路。

16.1 主程序只保留骨架

mainStartSpawnService 本身很薄,真正复杂的逻辑分散在模块、hook 和 sandbox 子系统里。这样做的好处是,主链稳定,扩展点明确。

16.2 孵化成功有明确语义

appspawn 回给客户端的成功不是“系统调用已经返回”,而是“子进程已经完成最小初始化并通过 pipe 回执确认”。这个语义比裸 fork() 更可靠。

16.3 sandbox 不是附属功能

sandbox 并不是 fork 之后顺手做一下的附加项,而是整条孵化链的主组成部分。它既参与预加载,也参与 parent pre-fork,还参与 child execute。

16.4 模式差异通过模块和 hook 收敛

appspawnnwebspawnnativespawnhybridspawn 没有被拆成完全独立的四套服务逻辑,而是在同一套骨架上通过模块和 hook 做差异化。这让公共能力能复用,差异点也更集中。

17. 建议的阅读顺序

如果后续还要继续下钻,建议按下面顺序看:

  1. base/startup/appspawn/appspawn.cfg
  2. base/startup/appspawn/standard/appspawn_main.c
  3. base/startup/appspawn/standard/appspawn_service.c
  4. base/startup/appspawn/standard/appspawn_manager.h
  5. base/startup/appspawn/common/appspawn_server.c
  6. base/startup/appspawn/modules/modulemgr/appspawn_modulemgr.c
  7. base/startup/appspawn/modules/module_engine/include/appspawn_hook.h
  8. base/startup/appspawn/modules/common/appspawn_common.c
  9. base/startup/appspawn/modules/sandbox/modern/sandbox_manager.c
  10. base/startup/appspawn/modules/sandbox/modern/sandbox_load.c
  11. base/startup/appspawn/interfaces/innerkits/client/appspawn_client.c

按这个顺序,先建立主链,再看扩展,再看 sandbox,会比较容易形成完整脑图。

Logo

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

更多推荐