Skip to content

内置命令下

追踪 setprop、trigger、mount_all、symlink、restorecon 和 wait 的所有者、消费者、生效时机与失败路径。

基于android-17.0.0_r1
AndroidinitbuiltinspropertymountSELinux

内置命令下 ​

本文接着内置命令上,但不再按命令名罗列 API。读者要解决的是一个源码阅读问题:同样写在 on ... 段落里的命令,为什么有的立即调用系统调用,有的只是排队,有的会暂停 init 主循环,还有的会把挂载结果转换成后续 Action 和 recovery 决策?答案取决于命令的 owner、消费者和生效时机。

读完本文,你应能从真实代码回答四个问题:setprop 返回成功时谁已经收到属性;trigger 何时真正找到并执行 Action;mount 和 mount_all 的边界在哪里;symlink 与 restorecon 为什么都碰到 SELinux,却不能互相替代。教学示例会明确标注,不把简化代码冒充 AOSP 实现。

1. 四种生效 ​

先把命令按“结果何时生效”分类,而不是按表面语法分类。

类型命令owner直接消费者返回成功的含义
立即操作mount、symlink、restoreconinit builtin内核 syscall、SELinux label 服务本次操作返回成功
状态提交setpropproperty serviceproperty area、属性触发器builtin 已发起 SetProperty(),但未检查其布尔返回值
事件排队triggerActionManager匹配的 Action事件已入队,尚未执行目标 Action
主循环屏障wait_for_propPropWaiterState属性变化回调等待状态已建立或目标值已存在
阶段编排mount_allinit + fs_mgrfstab、分区、rc 导入、启动事件阶段处理完成,后续事件可能仍在队列中

这里的“立即”也不是“业务完成”。例如 mount 返回后,文件系统已经交给内核处理;但一个依赖该挂载的服务是否 ready,是另一个消费者的生命周期问题。反过来,trigger 返回时甚至还没有选择目标 Action。

图中最容易误读的是 P -> PA -> Q 和 T -> Q:两条路径最终都可能进入 ActionManager,但 setprop 还要经过属性系统和属性回调;trigger 直接提交事件。它们不是同一种同步关系。

2. 属性边界 ​

2.1 注册约束 ​

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

相关函数/类型:GetBuiltinFunctionMap

cpp
{"setprop", {2, 2, {true, do_setprop}}},
{"trigger", {1, 1, {false, do_trigger}}},
{"mount", {3, kMax, {false, do_mount}}},
{"mount_all", {0, kMax, {false, do_mount_all}}},
{"restorecon", {1, kMax, {true, do_restorecon}}},
{"restorecon_recursive", {1, kMax, {true, do_restorecon_recursive}}},
{"symlink", {2, 2, {true, do_symlink}}},
{"wait", {1, 2, {true, do_wait}}},
{"wait_for_prop", {2, 2, {false, do_wait_for_prop}}},

这里的布尔值不是“命令是否成功”,而是 builtin 是否允许在 subcontext 执行。mount_all、trigger 和 wait_for_prop 保持 init 主上下文,是因为它们触碰 ActionManager、阶段导入或 init 主循环状态;symlink、restorecon 等文件操作则注册为可在 subcontext 使用。

2.2 控制属性 ​

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

相关函数/类型:do_setprop

cpp
static Result<void> do_setprop(const BuiltinArguments& args) {
    if (StartsWith(args[1], "ctl.")) {
        return Error()
               << "Cannot set ctl. properties from init; call the Service functions directly";
    }
    if (args[1] == kRestoreconProperty) {
        return Error() << "Cannot set '" << kRestoreconProperty
                       << "' from init; use the restorecon builtin directly";
    }

    SetProperty(args[1], args[2]);
    return {};
}

ctl.* 是服务控制命令的属性接口,但 Android 17 的 init 不允许通过 setprop 绕过 Service 方法;selinux.restorecon_recursive 也被拒绝,必须走 restorecon builtin。这个拒绝发生在调用 SetProperty() 之前,因此不能把它解释为“属性服务写入后被异步拒绝”。

2.3 消费时机 ​

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

相关函数/类型:PropertyChanged

cpp
void PropertyChanged(const std::string& name, const std::string& value) {
    if (name == "sys.powerctl") {
        trigger_shutdown(value);
    } else if (name == "sys.shutdown.requested") {
        HandleShutdownRequestedMessage(value);
    }

    if (property_triggers_enabled) {
        ActionManager::GetInstance().QueuePropertyChange(name, value);
        WakeMainInitThread();
    }

    prop_waiter_state.CheckAndResetWait(name, value);
}

因此 setprop foo bar 至少有三条后续路径:属性本身由 property service 管理;属性触发器可能被排入 ActionManager;如果它是当前等待的目标值,PropWaiterState 会清除等待并唤醒主线程。还要注意,do_setprop() 没有检查 SetProperty() 的布尔返回值;它的空 Result 只证明 builtin 越过了自身的两项拒绝条件并发起调用,既不能单独证明属性最终值已经改变,也不能证明所有属性消费者已经运行完。

真实调用方可以在 system/core/rootdir/init.rc 找到:on late-fs 中根据 ro.kernel.version 设置 bootreceiver.enable,随后 on property:... Action 才会创建 tracing instance 并写入 sysfs。这个例子把“属性写入”和“属性触发 Action”清楚分成两个时刻。

3. 事件入队 ​

3.1 入队语义 ​

相关源码:

  • system/core/init/builtins.cpp
  • do_trigger
  • system/core/init/action_manager.cpp
  • QueueEventTrigger
cpp
static Result<void> do_trigger(const BuiltinArguments& args) {
    ActionManager::GetInstance().QueueEventTrigger(args[1]);
    return {};
}

void ActionManager::QueueEventTrigger(const std::string& trigger) {
    auto lock = std::lock_guard{event_queue_lock_};
    event_queue_.emplace(trigger);
}

trigger 没有调用 Action::Execute...,也没有在当前命令栈上递归执行 on <trigger>。它只在锁内把 EventTrigger 放进队列。锁保护的是事件队列数据结构,不是目标 Action 的业务逻辑。

3.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,并用 CheckEvent() 同时检查事件名和 property 条件;一个事件可以匹配多个 Action。然后每次只执行当前 Action 的一个 command,直到该 Action 的命令数耗尽。这解释了 trigger “看起来立即返回、实际稍后生效”的现象。

3.3 启动主线 ​

源码文件: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 zygote-start

这些命令构造的是启动阶段之间的队列关系。trigger early-fs 返回时,on early-fs 的命令还没有因为这条语句而同步执行;它们要等主循环下一次调用 ExecuteOneCommand(),再按 ActionManager 的规则逐条消费。

图后的关键点是 B-->>A 与 X 之间的间隔:builtin 成功和目标 Action 执行不是同一个调用栈。若 Action 中又执行 trigger next,next 仍然是新事件;它不会插入当前 Action 的剩余命令之前。

4. 挂载分流 ​

4.1 单次挂载 ​

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

相关函数/类型:do_mount

cpp
static Result<void> do_mount(const BuiltinArguments& args) {
    const char* options = nullptr;
    unsigned flags = 0;
    bool wait = false;

    for (size_t na = 4; na < args.size(); na++) {
        size_t i;
        for (i = 0; mount_flags[i].name; i++) {
            if (!args[na].compare(mount_flags[i].name)) {
                flags |= mount_flags[i].flag;
                break;
            }
        }
        if (!mount_flags[i].name) {
            if (!args[na].compare("wait")) wait = true;
            else if (na + 1 == args.size()) options = args[na].c_str();
        }
    }

    const char* system = args[1].c_str();
    const char* source = args[2].c_str();
    const char* target = args[3].c_str();

mount 的前三个参数分别是文件系统类型、source 和 target;后续 token 中被识别的名称累积为 mount flags,最后一个未识别 token 才可能成为 options。wait 是 init 自己的等待逻辑,不是传给内核的 mount option。

source 以 loop@ 开头时,源码会打开 backing file,使用 android::dm::LoopControl::Attach 获得 loop device,再调用 mount();mount 失败会 Detach loop device。普通 source 则直接调用 mount(),并对失败结果使用 ErrnoErrorIgnoreEnoent() 包装。这里的清理只覆盖 loop device attach 已经成功的分支,不能推导出所有失败都会自动回滚。

4.2 阶段挂载 ​

相关源码:

  • system/core/init/util.cpp
  • ParseMountAll
  • system/core/init/builtins.cpp
  • do_mount_all
cpp
Result<MountAllOptions> ParseMountAll(const std::vector<std::string>& args) {
    bool compat_mode = false;
    bool import_rc = false;
    if (SelinuxGetVendorAndroidVersion() <= __ANDROID_API_Q__) {
        if (args.size() <= 1) {
            return Error() << "mount_all requires at least 1 argument";
        }
        compat_mode = true;
        import_rc = true;
    }

    std::size_t first_option_arg = args.size();
    enum mount_mode mode = MOUNT_MODE_DEFAULT;

    for (std::size_t na = args.size() - 1; na > (compat_mode ? 1 : 0); --na) {
        if (args[na] == "--early") {
            first_option_arg = na;
            mode = MOUNT_MODE_EARLY;
        } else if (args[na] == "--late") {
            first_option_arg = na;
            mode = MOUNT_MODE_LATE;
            import_rc = false;
        }
    }

    std::string fstab_path;
    if (first_option_arg > 1) {
        fstab_path = args[1];
    } else if (compat_mode) {
        return Error() << "mount_all argument 1 must be the fstab path";
    }

    std::vector<std::string> rc_paths;
    for (std::size_t na = 2; na < first_option_arg; ++na) {
        rc_paths.push_back(args[na]);
    }

    return MountAllOptions{rc_paths, fstab_path, mode, import_rc};
}

完整函数先从参数尾部寻找 --early/--late,再用 first_option_arg 切开 fstab、rc paths 与 option。Android R 及以后 fstab 参数可以省略;--early 设置 MOUNT_MODE_EARLY,稍后的 do_mount_all() 不排队 fs event;--late 设置 MOUNT_MODE_LATE,同时关闭 rc 导入。Q 及以前的 vendor 兼容模式则要求显式 fstab,并允许导入后续 rc paths。

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

相关函数/类型:do_mount_all

cpp
auto mount_fstab_result = fs_mgr_mount_all(&fstab, mount_all->mode);
SetProperty(prop_name, std::to_string(t.duration().count()));

if (mount_all->import_rc) {
    import_late(mount_all->rc_paths);
}

if (queue_event) {
    auto queue_fs_result = queue_fs_event(mount_fstab_result);
    if (!queue_fs_result.ok()) {
        return Error() << "queue_fs_event() failed: " << queue_fs_result.error();
    }
}

所以 mount_all 不是“把若干条 mount 循环调用”:它读取 fstab,交给 fs_mgr_mount_all 按阶段挂载,记录 ro.boottime.init.mount_all.<early|late|default>,可导入新的 rc,最后依据返回码决定是否排队 nonencrypted 或进入 recovery。--early 的关键语义是“先挂载非 latemount 项,并延后事件决策”,不是一个普通 flag。

4.3 加密与恢复 ​

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

相关函数/类型:queue_fs_event

cpp
if (code == FS_MGR_MNTALL_DEV_NOT_ENCRYPTABLE) {
    SetProperty("ro.crypto.state", "unsupported");
    ActionManager::GetInstance().QueueEventTrigger("nonencrypted");
    return {};
} else if (code == FS_MGR_MNTALL_DEV_NEEDS_RECOVERY) {
    if (android::gsi::IsGsiRunning()) {
        return Error() << "cannot wipe within GSI";
    }
    return reboot_into_recovery({"--wipe_data", "--reason=fs_mgr_mount_all"});
} else if (code == FS_MGR_MNTALL_DEV_FILE_ENCRYPTED ||
           code == FS_MGR_MNTALL_DEV_IS_METADATA_ENCRYPTED ||
           code == FS_MGR_MNTALL_DEV_NEEDS_METADATA_ENCRYPTION) {
    SetProperty("ro.crypto.state", "encrypted");
    ActionManager::GetInstance().QueueEventTrigger("nonencrypted");
    return {};
}

这里的 nonencrypted 名称不能按字面理解成“设备没有加密”:源码在 file-encrypted、metadata-encrypted 分支也会排队它,因为 vold 已经完成设备准备,后续 Action 使用的是同一套继续启动路径。需要 recovery 时,正常分支不会返回到下一条 rc 命令;GSI 分支则明确返回错误,拒绝在 GSI 内 wipe。

5. 标签操作 ​

5.1 创建标签 ​

相关源码:

  • system/core/init/builtins.cpp
  • MakeSymlink
  • do_symlink
cpp
static int MakeSymlink(const std::string& target, const std::string& linkpath) {
    std::string secontext;
    if (SelabelLookupFileContext(linkpath, 0, &secontext) && !secontext.empty()) {
        setfscreatecon(secontext.c_str());
    }

    int rc = symlink(target.c_str(), linkpath.c_str());

    if (!secontext.empty()) {
        int save_errno = errno;
        setfscreatecon(nullptr);
        errno = save_errno;
    }
    return rc;
}

symlink 的 SELinux 操作发生在创建时:根据 link path 查询 file context,临时设置 fscreatecon,创建后恢复。它只创建链接,不打开或验证 target;目标不存在也不改变 symlink 系统调用的基本语义。do_symlink 对错误使用 ErrnoErrorIgnoreEnoent(),这与“文件已被别的启动路径删除或目录不存在时是否记录错误”有关,不能解释为覆盖已有链接。

5.2 路径重标 ​

相关源码:

  • system/core/init/util.cpp
  • ParseRestorecon
  • system/core/init/builtins.cpp
  • do_restorecon
cpp
static Result<void> do_restorecon(const BuiltinArguments& args) {
    auto restorecon_info = ParseRestorecon(args.args);
    if (!restorecon_info.ok()) return restorecon_info.error();

    const auto& [flag, paths] = *restorecon_info;
    int ret = 0;
    for (const auto& path : paths) {
        if (selinux_android_restorecon(path.c_str(), flag) < 0) {
            ret = errno;
        }
    }
    if (ret) return ErrnoErrorIgnoreEnoent()
                    << "selinux_android_restorecon() failed";
    return {};
}

ParseRestorecon 支持 --recursive、--skip-ce、--cross-filesystems、--force、--data-data,并要求所有 flag 出现在 path 之前。多路径循环不会在第一个失败处 return,而是继续处理后续 path,同时保留最后一次失败的 errno。restorecon_recursive 也不是另一套实现:它向参数插入 --recursive 后复用 do_restorecon。

两者的职责差异可以这样记:symlink 为“即将创建的 link inode”准备 context;restorecon 根据已有路径重新应用 file contexts。前者是创建时标签,后者是已有对象的重标;两者都不等于修改 SELinux policy,也不等于切换 enforcing。

6. 等待屏障 ​

6.1 文件等待 ​

相关源码:

  • system/core/init/util.cpp
  • wait_for_file
  • system/core/init/builtins.cpp
  • do_wait
cpp
int wait_for_file(const char* filename, std::chrono::nanoseconds timeout) {
    android::base::Timer t;
    while (t.duration() < timeout) {
        struct stat sb;
        if (stat(filename, &sb) != -1) return 0;
        std::this_thread::sleep_for(10ms);
    }
    return -1;
}

static Result<void> do_wait(const BuiltinArguments& args) {
    auto timeout = kCommandRetryTimeout;
    if (args.size() == 3) {
        double timeout_double;
        if (!android::base::ParseDouble(args[2], &timeout_double, 0)) {
            return Error() << "failed to parse timeout";
        }
        timeout = std::chrono::duration_cast<std::chrono::nanoseconds>(
                std::chrono::duration<double>(timeout_double));
    }
    if (wait_for_file(args[1].c_str(), timeout) != 0) {
        return Error() << "wait_for_file() failed";
    }
    return {};
}

Android 17 接受浮点秒并转换为纳秒;旧文章把它写成整数秒会丢失实际边界。wait 是当前 builtin 的同步阻塞,轮询 stat(),超时只返回错误,不会取消已经完成的前序 filesystem 操作。

6.2 属性等待 ​

相关源码:

  • system/core/init/builtins.cpp
  • do_wait_for_prop
  • system/core/init/init.cpp
  • PropWaiterState
cpp
static Result<void> do_wait_for_prop(const BuiltinArguments& args) {
    const char* name = args[1].c_str();
    const char* value = args[2].c_str();
    if (!IsLegalPropertyName(name)) {
        return Error() << "IsLegalPropertyName(" << name << ") failed";
    }
    if (strlen(value) >= PROP_VALUE_MAX) return Error() << "value too long";
    if (!start_waiting_for_property(name, value)) {
        return Error() << "already waiting for a property";
    }
    return {};
}

PropWaiterState::StartWaiting() 同一时刻只允许一个 waiter;如果当前值已经等于目标值,就不建立计时器,直接成功。否则保存 name/value 和 Timer。PropertyChanged() 收到匹配变化后清除 waiter、调用 WakeMainInitThread();主循环在 MightBeWaiting() 为真时跳过普通 ExecuteOneCommand()。

6.3 等待差异 ​

这形成一条很重要的反差:trigger 把工作放入队列但不等目标 Action;wait_for_prop 反过来建立屏障,让 init 暂停消费普通 Action,直到属性回调匹配。等待没有通用取消命令;超时只存在于文件 wait,属性 waiter 的完成条件是目标属性变化或已经满足。恢复路径也不是 rollback:唤醒后 init 继续处理队列,之前已经执行的命令不会被撤销。

图中 QueuePropertyChange 和 CheckAndResetWait 是同一次属性变化的两条消费者路径;属性 waiter 被满足,不代表属性触发 Action 已经执行完,恢复后仍要经过 ActionManager 的正常调度。

7. 测试边界 ​

7.1 事件测试 ​

相关源码:

  • system/core/init/init_test.cpp
  • SimpleEventTrigger
  • EventTriggerOrder
  • LazilyLoadedActionsCanBeTriggeredByTheNextTrigger

这些 host 测试用临时 rc 文本、替代 builtin map 和断言计数器验证:事件能匹配 Action;同一事件的多个 Action 保持解析顺序;当前事件已经开始消费后新导入的同名 Action 不会被当前事件回头执行,而下一个 trigger 可以执行新导入 Action。它们证明 ActionManager 的队列和匹配规则,不证明真实设备上的 mount()、SELinux allow 或 fstab 内容。

7.2 静态检查 ​

相关源码:

  • system/core/init/check_builtins.cpp
  • check_mount_all
  • check_restorecon
  • check_setprop

check_builtins 会检查 rc 的参数格式、property 名和值类型、mount_all/restorecon 选项以及 ctl.* 拒绝规则。源码明确说明 property 展开值在 host 检查时未知,因此空字符串会被忽略。这个工具是解析期检查,不是 runtime 行为测试;它不能证明设备真的存在 source、挂载成功或标签已经改变。

7.3 可执行验证 ​

在 userdebug/eng 设备上,读者可用以下只读命令把源码主线与运行结果对照:

bash
adb shell getprop ro.boottime.init.mount_all.early
adb shell getprop ro.crypto.state
adb shell getprop init.svc.zygote
adb shell mount
adb shell ls -Z /metadata /dev/sys/fs/by-name
adb shell logcat -b all -d | grep -E 'processing action|mount_all|restorecon|wait for property'

这些命令分别观察阶段计时、加密状态、服务状态、内核挂载表、SELinux 标签和 init 日志。它们不能单独证明某个 setprop 的所有消费者都已完成,也不能安全地在生产设备上复现 recovery wipe 分支。

8. 命令验证 ​

建议读者选 on late-init 中的 trigger early-fs,完成一次源码回溯:

  1. 在 GetBuiltinFunctionMap() 找到参数范围和 subcontext 标志;
  2. 在 do_trigger() 找到 QueueEventTrigger();
  3. 在 ActionManager::ExecuteOneCommand() 找到事件匹配和逐命令执行;
  4. 在 init_test.cpp 的 SimpleEventTrigger 或 EventTriggerOrder 找到对应断言;
  5. 在设备日志中确认 processing action (early-fs),再检查后续 mount 阶段的属性和挂载表。

如果换成 mount_all,则应额外检查 ParseMountAll() 的 --early/--late 分支、fs_mgr_mount_all() 返回码和 queue_fs_event()。如果换成 restorecon_recursive,应验证它只是给 do_restorecon 增加 --recursive,而不是凭命令名假设存在另一套递归实现。

本文覆盖 init 内置命令的调度边界、挂载阶段编排、SELinux 标签处理和等待屏障;没有覆盖厂商 fstab、设备 SELinux policy、具体 kernel 文件系统实现或服务业务 ready。继续阅读源码时,应把这些对象视为独立消费者。