内置命令下
本文接着内置命令上,但不再按命令名罗列 API。读者要解决的是一个源码阅读问题:同样写在 on ... 段落里的命令,为什么有的立即调用系统调用,有的只是排队,有的会暂停 init 主循环,还有的会把挂载结果转换成后续 Action 和 recovery 决策?答案取决于命令的 owner、消费者和生效时机。
读完本文,你应能从真实代码回答四个问题:setprop 返回成功时谁已经收到属性;trigger 何时真正找到并执行 Action;mount 和 mount_all 的边界在哪里;symlink 与 restorecon 为什么都碰到 SELinux,却不能互相替代。教学示例会明确标注,不把简化代码冒充 AOSP 实现。
1. 四种生效
先把命令按“结果何时生效”分类,而不是按表面语法分类。
| 类型 | 命令 | owner | 直接消费者 | 返回成功的含义 |
|---|---|---|---|---|
| 立即操作 | mount、symlink、restorecon | init builtin | 内核 syscall、SELinux label 服务 | 本次操作返回成功 |
| 状态提交 | setprop | property service | property area、属性触发器 | builtin 已发起 SetProperty(),但未检查其布尔返回值 |
| 事件排队 | trigger | ActionManager | 匹配的 Action | 事件已入队,尚未执行目标 Action |
| 主循环屏障 | wait_for_prop | PropWaiterState | 属性变化回调 | 等待状态已建立或目标值已存在 |
| 阶段编排 | mount_all | init + fs_mgr | fstab、分区、rc 导入、启动事件 | 阶段处理完成,后续事件可能仍在队列中 |
这里的“立即”也不是“业务完成”。例如 mount 返回后,文件系统已经交给内核处理;但一个依赖该挂载的服务是否 ready,是另一个消费者的生命周期问题。反过来,trigger 返回时甚至还没有选择目标 Action。
图中最容易误读的是 P -> PA -> Q 和 T -> Q:两条路径最终都可能进入 ActionManager,但 setprop 还要经过属性系统和属性回调;trigger 直接提交事件。它们不是同一种同步关系。
2. 属性边界
2.1 注册约束
源码文件:system/core/init/builtins.cpp
相关函数/类型:GetBuiltinFunctionMap
{"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
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
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.cppdo_triggersystem/core/init/action_manager.cppQueueEventTrigger
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
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
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
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.cppParseMountAllsystem/core/init/builtins.cppdo_mount_all
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
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
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.cppMakeSymlinkdo_symlink
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.cppParseRestoreconsystem/core/init/builtins.cppdo_restorecon
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.cppwait_for_filesystem/core/init/builtins.cppdo_wait
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.cppdo_wait_for_propsystem/core/init/init.cppPropWaiterState
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.cppSimpleEventTriggerEventTriggerOrderLazilyLoadedActionsCanBeTriggeredByTheNextTrigger
这些 host 测试用临时 rc 文本、替代 builtin map 和断言计数器验证:事件能匹配 Action;同一事件的多个 Action 保持解析顺序;当前事件已经开始消费后新导入的同名 Action 不会被当前事件回头执行,而下一个 trigger 可以执行新导入 Action。它们证明 ActionManager 的队列和匹配规则,不证明真实设备上的 mount()、SELinux allow 或 fstab 内容。
7.2 静态检查
相关源码:
system/core/init/check_builtins.cppcheck_mount_allcheck_restoreconcheck_setprop
check_builtins 会检查 rc 的参数格式、property 名和值类型、mount_all/restorecon 选项以及 ctl.* 拒绝规则。源码明确说明 property 展开值在 host 检查时未知,因此空字符串会被忽略。这个工具是解析期检查,不是 runtime 行为测试;它不能证明设备真的存在 source、挂载成功或标签已经改变。
7.3 可执行验证
在 userdebug/eng 设备上,读者可用以下只读命令把源码主线与运行结果对照:
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,完成一次源码回溯:
- 在
GetBuiltinFunctionMap()找到参数范围和 subcontext 标志; - 在
do_trigger()找到QueueEventTrigger(); - 在
ActionManager::ExecuteOneCommand()找到事件匹配和逐命令执行; - 在
init_test.cpp的SimpleEventTrigger或EventTriggerOrder找到对应断言; - 在设备日志中确认
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。继续阅读源码时,应把这些对象视为独立消费者。
