Skip to content

init.rc主配置

沿 init.rc 的安装、加载、阶段事件、挂载屏障、APEX 与 Zygote 启动链学习真实启动配置。

基于android-17.0.0_r1
Androidinitinit.rcbootAPEX

init.rc主配置 ​

上一篇内置命令下回答了单条命令何时生效;本文把视野扩大到整份 init.rc,但不做“从头到尾翻译配置”的清单。教材目标是建立一条可以回到源码验证的主线:这份文件如何被安装和选中,early-init 为什么能使用哪些资源,late-init 如何把阶段变成队列,post-fs-data 的等待点如何形成真正屏障,以及 zygote-start、boot 和 sys.boot_completed 为什么不是同一个完成信号。

过去的资料常把 cgroup 挂载、Zygote 启动和设备配置都归到 root init.rc。当前实现已经把 cgroup 准备放进 C++ 初始队列,根配置还依赖设备 rc、vendor rc、fstab、APEX 和属性服务。因此阅读本文时,要把“根文件中出现的命令”“C++ 在文件之前排队的动作”和“其他分区追加的 Action”分开。

1. 文件入口 ​

1.1 安装位置 ​

源码文件:system/core/rootdir/Android.bp

相关函数/类型:prebuilt_etc { name: "init.rc" }

make
prebuilt_etc {
    name: "init.rc",
    src: "init.rc",
    sub_dir: "init/hw",
    required: [
        "platform-bootclasspath",
        "init.boringssl.zygote64.rc",
        "init.boringssl.zygote64_32.rc",
        "init-perfetto.rc",
    ],
}

这段构建规则证明了两个边界。第一,源码文件的安装目标是 init/hw 子目录,运行时由 init.cpp 读取 /system/etc/init/hw/init.rc;不能因为源文件位于 rootdir/ 就把它写成运行时 /init.rc。第二,zygote、BoringSSL 自检和 Perfetto 的 rc 文件是构建依赖,主文件的 import 还会根据属性选择硬件和 zygote 变体。

1.2 首部导入 ​

源码文件:system/core/rootdir/init.rc

text
import /init.environ.rc
import /system/etc/init/hw/init.usb.rc
import /init.${ro.hardware}.rc
import /vendor/etc/init/hw/init.${ro.hardware}.rc
import /system/etc/init/hw/init.usb.configfs.rc
import /system/etc/init/hw/init.${ro.zygote}.rc

这些 import 是 Parser 的输入,不是 shell 的 source。${ro.hardware} 和 ${ro.zygote} 在命令执行前由 init 的属性展开规则处理;导入失败和目录导入还有各自的 parser 语义。更重要的是,主文件导入的设备 rc 与 LoadBootScripts() 后续扫描的分区目录不是一回事:前者在配置文本中显式出现,后者由 C++ 决定扫描路径。

1.3 C++选择器 ​

源码文件:system/core/init/init.cpp

相关函数/类型:LoadBootScripts

cpp
static void LoadBootScripts(ActionManager& action_manager, ServiceList& service_list) {
    Parser parser = CreateParser(action_manager, service_list);

    std::string bootscript = GetProperty("ro.boot.init_rc", "");
    if (bootscript.empty()) {
        parser.ParseConfig("/system/etc/init/hw/init.rc");
        if (!parser.ParseConfig("/system/etc/init")) {
            late_import_paths.emplace_back("/system/etc/init");
        }
        parser.ParseConfig("/system_ext/etc/init");
        if (!parser.ParseConfig("/vendor/etc/init")) {
            late_import_paths.emplace_back("/vendor/etc/init");
        }
        if (!parser.ParseConfig("/odm/etc/init")) {
            late_import_paths.emplace_back("/odm/etc/init");
        }
        if (!parser.ParseConfig("/product/etc/init")) {
            late_import_paths.emplace_back("/product/etc/init");
        }
    } else {
        parser.ParseConfig(bootscript);
    }
}

ro.boot.init_rc 非空时,默认主文件路径被替换成该属性指定的脚本;为空时才执行标准路径和分区目录扫描。对某些目录的 ParseConfig() 失败会把路径放入 late_import_paths,后续由 mount_all 的 import_late() 在分区可见后再次导入。由此可见,“文件已安装”不等于“其中所有 Action 已经进入 actions_”。

2. 早期准备 ​

2.1 Cgroup先行 ​

相关源码:

  • system/core/init/init.cpp
  • SecondStageMain
cpp
am.QueueBuiltinAction(SetupCgroupsAction, "SetupCgroups");
am.QueueEventTrigger("early-init");
am.QueueBuiltinAction(ConnectEarlyStageSnapuserdAction,
                      "ConnectEarlyStageSnapuserd");
am.QueueBuiltinAction(wait_for_coldboot_done_action,
                      "wait_for_coldboot_done");
am.QueueBuiltinAction(SetMmapRndBitsAction, "SetMmapRndBits");

根配置的注释说 cgroup 会在 early-init 前挂载,对应的 C++ 调用顺序是:SetupCgroupsAction 先排入队列,然后才是 early-init 事件。init.rc 中后续对 /dev/memcg、cpuctl 或服务 class 的操作,依赖的是这个前置动作,而不是由 early-init 自己首次挂载全部 cgroup。

wait_for_coldboot_done_action 也很关键:start ueventd 只发起服务启动,coldboot 完成由独立的等待动作确认。后续需要设备节点的 action 不能只看 start ueventd 的返回值。

2.2 早期事件 ​

源码文件:system/core/rootdir/init.rc

相关函数/类型:on early-init

text
on early-init
    mkdir /dev/bootchart 0755 root root
    bootchart start

    write /proc/sys/kernel/sysrq 0
    write /proc/sys/kernel/modprobe \n

    restorecon /adb_keys
    restorecon /postinstall

    mkdir /dev/memcg/apps/ 0755 system system
    mkdir /dev/memcg/system 0550 system system

    mkdir /dev/net 0755 root root
    symlink ../tun /dev/net/tun

    setrlimit nice 40 40
    setrlimit nofile 32768 524288
    setrlimit memlock 65536 65536

这个 Action 不是单纯的“启动服务阶段”。它同时修改内核接口、恢复文件标签、建立供 lmkd/zygote 使用的 cgroup 目录、创建 /dev/net/tun 兼容链接,并设置 init 继承的资源限制。每一行的消费者不同:sysctl 由内核读取,restorecon 由 SELinux file-context 规则消费,目录和 rlimit 则成为后续服务启动的输入。

2.3 APEX屏障 ​

源码文件:system/core/rootdir/init.rc

相关函数/类型:on early-init

text
    mkdir /linkerconfig/bootstrap 0755
    mkdir /linkerconfig/default 0755
    write /linkerconfig/bootstrap/ld.config.txt \#
    write /linkerconfig/default/ld.config.txt \#
    chmod 644 /linkerconfig/bootstrap/ld.config.txt
    chmod 644 /linkerconfig/default/ld.config.txt
    mount none /linkerconfig/bootstrap /linkerconfig bind rec

    start ueventd
    mount tracefs tracefs /sys/kernel/tracing gid=3012
    exec_start init_dev_config
    restorecon /metadata
    exec_start apexd-bootstrap
    perform_apex_config

start ueventd 是异步服务启动;exec_start init_dev_config 和 exec_start apexd-bootstrap 则是同步执行服务。这里的顺序有源码含义:APEX bootstrap 以 exec_start 运行,确保提供关键库的 APEX 在后续进程启动前已经可见;perform_apex_config 再把配置动作接入 init 的后续状态。不能把这一段概括成“early-init 启动 apexd”,因为同步/异步差异会改变后续 Action 的可见资源。

图中的两条路径不是并行完成承诺:start ueventd 的消费者是服务状态和 coldboot 等待动作;exec_start apexd-bootstrap 在当前 Action 中形成同步屏障。读者排查早期卡顿时,应先按最后一条 processing action 区分是 exec、property waiter 还是普通 builtin。

3. 初始化段 ​

3.1 设备接口 ​

源码文件:system/core/rootdir/init.rc

相关函数/类型:on init

text
on init
    sysclktz 0
    copy /proc/cmdline /dev/urandom
    copy /proc/bootconfig /dev/urandom

    symlink /proc/self/fd/0 /dev/stdin
    symlink /proc/self/fd/1 /dev/stdout
    symlink /proc/self/fd/2 /dev/stderr

    mkdir /dev/socket/ot-daemon 0770 thread_network thread_network
    mkdir /dev/cpuctl/foreground
    mkdir /dev/cpuctl/background
    mkdir /dev/cpuctl/top-app
    mkdir /dev/cpuctl/system
    mkdir /dev/cpuctl/system-background

on init 先建立后续 daemon 能依赖的设备接口和控制组目录,再进入 binderfs、fusectl、存储视图等更大段落。这里的 copy 是把 boot 参数输入写入内核设备文件,不是普通配置文件复制;symlink 为使用标准 fd 路径的程序提供兼容入口。

3.2 Binder与存储 ​

源码文件:system/core/rootdir/init.rc

相关函数/类型:on init

text
    mount configfs none /config nodev noexec nosuid
    mkdir /dev/binderfs
    mount binder binder /dev/binderfs stats=global
    chmod 0755 /dev/binderfs

    mount fusectl none /sys/fs/fuse/connections
    symlink /dev/binderfs/binder /dev/binder
    symlink /dev/binderfs/hwbinder /dev/hwbinder
    symlink /dev/binderfs/vndbinder /dev/vndbinder

    mkdir /mnt/user 0755 root root
    mkdir /mnt/user/0 0755 root root
    mkdir /mnt/runtime 0700 root root
    mkdir /mnt/runtime/default 0755 root root

这些命令把 kernel filesystem、Binder device nodes 和多用户存储视图准备成服务可消费的资源。它们不等价于“Binder 服务已经注册”:servicemanager 进程的启动在同一 on init Action 后段,但 Binder 注册仍由服务自身完成。

3.3 核心服务 ​

源码文件:system/core/rootdir/init.rc

相关函数/类型:on init

text
    start logd
    start lmkd

    start servicemanager
    start hwservicemanager
    start vndservicemanager

    mkdir /mnt/vm 0755 root system
    mount tmpfs tmpfs /mnt/vm nosuid nodev noexec rw

start 的 owner 是 ServiceList,消费者是对应 Service 的 fork/exec 和后续进程状态;它不等待 Binder service registration。这个差异与上一篇 class_start 的结论一致,但本篇把它放回 root 配置的真实顺序:logd 和 lmkd 在其它 daemon 之前启动,是为了让后续进程拥有日志和内存压力消费者。

4. 阶段队列 ​

4.1 late-init配置 ​

源码文件:system/core/rootdir/init.rc

相关函数/类型:on late-init

text
on late-init
    trigger early-fs
    trigger fs
    trigger post-fs
    trigger late-fs
    trigger post-fs-data
    trigger load-bpf-programs
    trigger zygote-start
    trigger firmware_mounts_complete
    trigger early-boot
    trigger boot

这十条语句不是十次同步函数调用。do_trigger() 只把事件放进 ActionManager::event_queue_;当前 late-init Action 结束后,主循环才开始按 FIFO 取出 early-fs。事件名是调度标签,不是目标 Action 已完成的承诺。

4.2 消费边界 ​

源码文件:system/core/init/action_manager.cpp

相关函数/类型:ExecuteOneCommand

cpp
while (current_executing_actions_.empty() && !event_queue_.empty()) {
    for (const auto& action : actions_) {
        if (std::visit([&action](const auto& event) {
                return action->CheckEvent(event);
            }, event_queue_.front())) {
            current_executing_actions_.emplace(action.get());
        }
    }
    event_queue_.pop();
}

if (current_executing_actions_.empty()) return;

auto action = current_executing_actions_.front();
action->ExecuteOneCommand(current_command_);

匹配发生时,ActionManager 扫描已经解析的 Action;同一事件可匹配多个 Action,随后每轮只执行当前 Action 的一个 command。若某个 Action 在执行中 import 新 rc,新 Action 不会被已经取出的同一个 event 追溯匹配;后续 event 才可能看见它。这个边界由 init_test.cpp 的 lazy-load 测试直接验证。

4.3 属性触发 ​

源码文件:system/core/init/init.cpp

相关函数/类型:queue_property_triggers_action

cpp
static Result<void> queue_property_triggers_action(const BuiltinArguments&) {
    ActionManager::GetInstance().QueueBuiltinAction(
            property_enable_triggers_action,
            "enable_property_trigger");
    ActionManager::GetInstance().QueueAllPropertyActions();
    return {};
}

初始队列还会安排这个 builtin action。它先追加“启用 property trigger”的一次性动作,再追加空的 PropertyChange,用于按当前属性状态检查 property actions。因此 on property:... 并不是从 init 进程启动瞬间就无条件生效;它有一个由 C++ 显式安排的启用与全量检查边界。

5. 文件阶段 ​

5.1 early-fs ​

源码文件:system/core/rootdir/init.rc

相关函数/类型:on early-fs

text
on early-fs
    start vold

根配置只给出 vold 启动;设备或产品 rc 可以添加同名 early-fs Action。start vold 返回只表示启动请求已交给 ServiceList,后面的 fstab、加密和 userdata 结果仍由 mount_all、vold 属性及等待点共同决定。

5.2 post-fs ​

源码文件:system/core/rootdir/init.rc

相关函数/类型:on post-fs

text
on post-fs
    exec - system system -- /system/bin/vdc checkpoint markBootAttempt
    mount rootfs rootfs / remount bind ro nodev
    mount none /mnt/user/0 /storage bind rec
    mount none none /storage slave rec
    restorecon --recursive --cross-filesystems /sys/kernel/debug
    restorecon_recursive /metadata
    mkdir /metadata/vold
    mkdir /metadata/apex 0700 root system

post-fs 的关键不是“mount 已经结束”这句口号,而是可以观察到它消费早期挂载结果:rootfs remount、storage bind、跨文件系统 restorecon 和 metadata 目录创建都要求对应路径已经出现。exec 还会同步等待 checkpoint 命令;因此一条外部程序失败和一条普通 restorecon 失败的主循环影响不同。

5.3 late-fs ​

源码文件:system/core/rootdir/init.rc

相关函数/类型:on late-fs

text
on late-fs
    chmod 0755 /sys/kernel/tracing
    chmod 0755 /sys/kernel/debug/tracing
    class_start early_hal

on late-fs && property:ro.kernel.version=4.19
    setprop bootreceiver.enable 0

on late-fs
    setprop bootreceiver.enable ${bootreceiver.enable:-1}

late-fs 同时承载文件权限、早期 HAL 启动和 property 条件 Action。两个无条件 on late-fs 段按解析顺序成为不同 Action;属性条件段只在事件消费时检查,不会因为之后属性变化而重新绑定到旧的 late-fs 事件。

5.4 设备扩展 ​

标准根文件没有固定的 on fs 段,设备 rc 才是 fstab 的常见 owner。比如设备配置可以提供:

相关源码:

  • device/generic/goldfish/init/init.ranchu.rc
  • on fs
  • on late-fs
text
on fs
    mount_all /vendor/etc/fstab.ranchu --early

on late-fs
    mount_all /vendor/etc/fstab.ranchu --late

这是 goldfish 设备树中的真实配置,不是 system/core/rootdir/init.rc 的内容。分析真实设备时必须同时检查 init.${ro.hardware}.rc、/vendor/etc/init 和 fstab;只读 root init.rc 不能说明设备实际挂载了哪些分区,也不能把 ranchu 的 fstab 路径外推到实体设备。

6. Data屏障 ​

6.1 前段动作 ​

源码文件:system/core/rootdir/init.rc

相关函数/类型:on post-fs-data

text
on post-fs-data
    exec - system system -- /system/bin/vdc checkpoint prepareCheckpoint
    chown system system /data
    chmod 0771 /data
    restorecon /data
    installkey /data

    mkdir /data/tombstones 0775 system system encryption=Require
    start tombstoned
    enter_default_mount_ns

    mkdir /data/property 0700 root root encryption=Require
    load_persist_props
    trigger load_persist_props_action

这一段包含三类 owner:checkpoint/vold 相关外部命令、/data 文件系统元数据、property/APEX 后续状态。load_persist_props 负责发起属性加载并建立等待;紧随其后的 trigger load_persist_props_action 是兼容 rc 的事件,不应被误读成“属性加载从这条 trigger 才开始”。

6.2 等待点 ​

源码文件:system/core/rootdir/init.rc

相关函数/类型:on post-fs-data

text
    wait_for_prop apexd.status activated
    wait_for_prop keystore.module_hash.sent true
    perform_apex_config

    exec_start mainline_aconfigd_init
    exec_start derive_sdk
    exec_start derive_classpath
    exec_start art_boot

    start odsign
    wait_for_prop odsign.key.done 1

wait_for_prop 会让 init 主循环暂停普通 Action,直到属性回调匹配;exec_start 则等待 oneshot service 的执行状态。两者都表现为“下一条命令没有立刻执行”,但 owner 不同:前者是 PropWaiterState,后者是 Service/子进程回收。排查卡顿时不能只看最后一条 rc 命令名。

6.3 无回滚 ​

源码文件:system/core/init/action.cpp

相关函数/类型:Action::ExecuteCommand

cpp
void Action::ExecuteCommand(const Command& command) const {
    android::base::Timer t;
    auto result = command.InvokeFunc(subcontext_);
    auto duration = t.duration();

    if (!result.has_value() || duration > 50ms ||
        android::base::GetMinimumLogSeverity() <= android::base::DEBUG) {
        LOG(INFO) << "Command '" << command.BuildCommandString()
                  << "' action=" << BuildTriggersString() << " ("
                  << filename_ << ":" << command.line() << ") took "
                  << duration.count() << "ms and "
                  << (result.ok() ? "succeeded"
                                  : "failed: " + result.error().message());
    }
}

Action::ExecuteCommand() 记录错误和耗时,但不会因为 Result<void> 失败而回滚前面已执行的命令,也不会自动跳过 Action 的下一条 command。只有 builtin 自己实现的清理(例如 loop mount 失败时 detach)或更高层 fatal/recovery 路径,才有局部恢复语义。

图中的循环是等待状态,不是 rc 语法循环。属性变化由 PropertyChanged() 同时送入属性 Action 队列并检查 waiter;满足条件后才唤醒主线程。

7. Zygote启动 ​

7.1 独立事件 ​

源码文件:system/core/rootdir/init.rc

相关函数/类型:on zygote-start

text
on zygote-start
    wait_for_prop odsign.verification.done 1
    start statsd
    start zygote
    start zygote_secondary

Android 17 的根配置没有把 Zygote 简化成 on boot 中的 class_start main。它先等待 odsign verification,再直接启动两个 zygote service。start zygote 的完成边界仍只是 Service 启动请求;system_server 的 fork 和 framework ready 属于 Zygote/Java 侧消费者。

7.2 Boot阶段 ​

源码文件:system/core/rootdir/init.rc

相关函数/类型:on boot

text
on boot
    ifup lo
    hostname localhost
    domainname localdomain
    setprop net.tcp_def_init_rwnd 60
    class_start hal
    class_start core

on boot && property:ro.debuggable=1
    write /dev/sys/fs/by-name/userdata/iostat_period_ms 1000
    write /dev/sys/fs/by-name/userdata/iostat_enable 1

on boot && property:ro.config.low_ram=true
    write /proc/sys/vm/dirty_expire_centisecs 200
    write /proc/sys/vm/dirty_background_ratio 5

boot 是 root 配置的一个事件,不是“用户已经看到桌面”的标志。它配置网络、内核参数、HAL/core class,并按属性匹配 debug 或低内存分支。sys.boot_completed=1 是更晚由 framework 设置的 property,必须走 property trigger。

7.3 主服务 ​

源码文件:system/core/rootdir/init.rc

相关函数/类型:on nonencrypted

text
on nonencrypted
    class_start main
    class_start late_start

on property:sys.boot_completed=1
    bootchart stop
    exec - system system -- /bin/rm -rf /data/per_boot
    mkdir /data/per_boot 0710 system system encryption=Require key=per_boot_ref

nonencrypted 是 mount_all 根据 fs_mgr 返回码排入的历史事件名;file-encrypted 或 metadata-encrypted 且 vold 已准备完成时也会使用它。反过来,sys.boot_completed=1 的消费者是属性 Action,执行 bootchart 清理和 per-boot 目录操作。两个事件都可能出现在“启动后期”,但 owner、输入和证明范围完全不同。

下面的两个 Action 片段分别展示事件触发的 class 启动和 framework 后期的 property 消费,不能把它们拼成一条同步调用链。

8. 分支路径 ​

8.1 充电模式 ​

相关源码:

  • system/core/init/init.cpp
  • SecondStageMain
  • system/core/rootdir/init.rc
  • on charger
cpp
if (bootmode == BootMode::CHARGER_MODE) {
    am.QueueEventTrigger("charger");
} else {
    am.QueueEventTrigger("late-init");
}

C++ 先决定进入哪个事件;rc 再为该事件提供匹配的 Action。下面的 on charger 是事件消费者,sys.boot_from_charger_mode 则是从充电模式回到正常启动时的第二个入口。

text
on charger
    class_start charger

on property:sys.boot_from_charger_mode=1
    class_stop charger
    trigger late-init

charger 模式初始不进入完整文件系统启动链。healthd 后续通过 property 触发 late-init,恢复路径是从 late-init 继续,而不是重新执行整个 SecondStageMain。这个区别决定了 charger 阶段已创建资源能否被复用,也决定了排障时不能把“没有 early-init 日志”直接判为 init 崩溃。

8.2 GSI边界 ​

GSI 状态在脚本运行前由 C++ 写入属性,mount_all 的 recovery 分支还会拒绝在 GSI 中执行 wipe。标准 root init.rc 的 Action 不能外推为所有 GSI/vendor 行为;真实设备需要同时核对 GSI 属性、设备 fstab 和 vendor rc。

8.3 设备差异 ​

LoadBootScripts() 会根据 ro.boot.init_rc 选择脚本,也会记录尚未可读分区的 late_import_paths。因此缺少某个 on fs、zygote 变体或 vendor service 时,优先检查“哪个脚本被选中、哪个目录在当时可读”,不要先假设 root init.rc 缺少命令。

9. 测试验证 ​

9.1 Action顺序 ​

源码文件:system/core/init/init_test.cpp

相关函数/类型:EventTriggerOrder

测试输入是一段临时 rc:两个无条件 on boot,中间一个带 property:ro.hardware=*;三个替代 builtin 按执行序号断言 0、1、2。它证明同一事件匹配 Action 后保持解析顺序,不证明设备分区最终 import 顺序。

9.2 Import顺序 ​

源码文件:system/core/init/init_test.cpp

相关函数/类型:EventTriggerOrderMultipleFiles

测试构造一个直接 import 文件、一个目录 import 文件、目录内 a.rc 对另一个文件的递归 import 和 b.rc,最终断言六个 command 的执行顺序。它证明目录排序与递归导入会进入同一 Parser/ActionManager 体系;它不覆盖真实 /system_ext、/vendor 的权限和挂载时机。

9.3 延迟导入 ​

相关源码:

  • system/core/init/init_test.cpp
  • LazilyLoadedActionsCantBeTriggeredByTheSameTrigger
  • LazilyLoadedActionsCanBeTriggeredByTheNextTrigger

一个测试在 on boot 中加载新的 on boot,断言当前 boot 不追溯执行;另一个加载 on next 后执行 trigger next,断言新 Action 在下一个事件执行。这正是 mount_all --late、APEX rc 和分区 late import 需要明确触发时机的原因。

9.4 现场检查 ​

在 userdebug/eng 设备上可以只读观察:

bash
adb shell 'logcat -b all -d -s init:I | grep "processing action"'
adb shell getprop init.svc.ueventd
adb shell getprop init.svc.zygote
adb shell getprop init.svc.servicemanager
adb shell getprop ro.crypto.state
adb shell getprop sys.boot_completed
adb shell mount

processing action 的文件和行号能告诉你实际命中的 Action;init.svc.* 只表示 Service 状态;mount 只观察内核挂载表;sys.boot_completed 只证明 framework 写入了该属性。它们不能单独证明所有 init.rc 命令成功,也不能证明 system_server 业务 ready。

10. 触发追踪 ​

建议用一次“Zygote 没有启动”的问题完成闭环:

  1. 从 init.cpp::SecondStageMain() 判断当前是否走 charger 或 late-init;
  2. 从 LoadBootScripts() 判断实际 root script 和 zygote rc 路径;
  3. 在 init.rc 追踪 late-init → post-fs-data → zygote-start;
  4. 检查 wait_for_prop odsign.verification.done 1 的 producer,而不是只重复 start zygote;
  5. 查看 init.svc.zygote、processing action 和 property 日志;
  6. 对照 init_test.cpp,明确 host 测试覆盖的调用顺序,以及仍需结合设备配置和运行状态判断的部分。

最终要保留三条边界:root init.rc 是配置输入,不是完整设备启动图;event 名称是调度标签,不是消费者完成信号;普通 command error 没有全局 rollback。沿着 owner、consumer 和生效时机阅读,才能把一份很长的 rc 文件变成可验证的源码教材。