内置命令上
本文面向已经读过 Action解析 的读者。上一篇说明 Action 如何保存命令并在事件中调用 builtin;本文沿着五个真实命令继续向下:class_start 改变的是 Service 生命周期,mkdir、chmod、chown 改变文件系统元数据,write 把字节写给 procfs/sysfs 或普通文件。
本文不把 builtin 当作 shell 命令,也不只列出函数名。每个命令都按“注册参数约束 → BuiltinArguments → owner/消费者 → 失败和幂等边界”阅读。后续 mount、property、SELinux 命令另有专题。
1. 调用入口
1.1 注册表
GetBuiltinFunctionMap() 为每个关键字记录最小/最大参数数,以及是否在 subcontext 中直接执行。参数数量先由 KeywordMap::Find() 检查,函数体不会收到明显错误的 argv。
相关源码:
system/core/init/builtins.cppsystem/core/init/builtins.hsystem/core/init/keyword_map.h
{"chmod", {2, 2, {true, do_chmod}}},
{"chown", {2, 3, {true, do_chown}}},
{"class_start", {1, 1, {false, do_class_start}}},
{"mkdir", {1, 6, {true, do_mkdir}}},
{"write", {2, 2, {true, do_write}}},1.2 参数展开
Action 执行时,普通 builtin 的参数由 RunBuiltinFunction() 展开 ${property},再构造 BuiltinArguments。因此 write /proc/x ${prop} 的 path/value 可能在执行时才确定;注册表中的数量约束针对展开前的 token 数量。
源码文件:system/core/init/action.cpp
BuiltinArguments builtin_arguments{.context = context};
builtin_arguments.args.resize(args.size());
builtin_arguments.args[0] = args[0];
for (std::size_t i = 1; i < args.size(); ++i) {
auto expanded_arg = ExpandProps(args[i]);
if (!expanded_arg.ok()) return expanded_arg.error();
builtin_arguments.args[i] = std::move(*expanded_arg);
}
return function(builtin_arguments);1.3 两类消费者
2. class_start
2.1 遍历者
do_class_start() 不解析 rc,也不直接 fork。它读取 class 名,遍历全局 ServiceList,对匹配的 Service 调用 StartIfNotDisabled()。
源码文件:system/core/init/builtins.cpp
static Result<void> do_class_start(const BuiltinArguments& args) {
if (android::base::GetBoolProperty(
"persist.init.dont_start_class." + args[1], false)) {
return {};
}
for (const auto& service : ServiceList::GetInstance()) {
if (service->classnames().count(args[1])) {
if (auto result = service->StartIfNotDisabled(); !result.ok()) {
LOG(ERROR) << "Could not start service '" << service->name()
<< "' as part of class '" << args[1]
<< "': " << result.error();
}
}
}
return {};
}2.2 条件边界
有三个容易混淆的条件:class 不匹配的 Service 跳过;显式 disabled 的 Service 也跳过;persist.init.dont_start_class.<name> 为 true 时整个 class 静默跳过。单个启动失败只记录错误,循环继续,不是事务回滚。
2.3 生效时机
class_start 成功表示遍历动作完成,不表示所有子进程已 ready。真正的进程创建、init.svc.* 更新和 Binder 发布由 Service::Start() 及目标进程负责。这个命令的消费者是 ServiceList,不是文件系统。
3. mkdir
3.1 参数解析
do_mkdir() 先把原始 argv 交给 ParseMkdir(),再调用 make_dir_with_options()。Android 17 的选项还包含 fscrypt 相关字段,因此旧稿把它简化成单次 mkdir() 并不完整。
源码文件:system/core/init/builtins.cpp
// mkdir <path> [mode] [owner] [group] [<option> ...]
static Result<void> do_mkdir(const BuiltinArguments& args) {
auto options = ParseMkdir(args.args);
if (!options.ok()) return options.error();
return make_dir_with_options(*options);
}3.2 幂等更新
make_dir_with_options() 先 lstat()。目标不存在时递归创建;目标已存在时要求它确实是目录,然后比较 owner/mode,必要时 lchown()、fchmodat(AT_SYMLINK_NOFOLLOW)。所以 mkdir 的“已存在”不是简单忽略,而是会校正元数据。
源码文件:system/core/init/builtins.cpp
struct stat mstat;
if (lstat(options.target.c_str(), &mstat) != 0) {
if (errno != ENOENT) return ErrnoError() << "lstat() failed";
if (!make_dir(options.target, options.mode)) {
return ErrnoErrorIgnoreEnoent() << "mkdir() failed";
}
if (lstat(options.target.c_str(), &mstat) != 0) {
return ErrnoError() << "lstat() failed on new directory";
}
}
if (!S_ISDIR(mstat.st_mode)) return Error() << "Not a directory";3.3 加密边界
如果 FBE 开启,函数还会按 ref 或 per_boot_ref 设置目录策略;策略失败会进入 recovery reboot 路径。这个副作用超出普通目录权限,不能把 mkdir 视为只改变 mode 的命令。
4. chmod与chown
4.1 chmod
Android 17 用本地 get_mode() 逐字符解析八进制字符串,再调用 fchmodat,并带 AT_SYMLINK_NOFOLLOW。非法数字会得到 -1 mode,系统调用失败。
源码文件:system/core/init/builtins.cpp
static mode_t get_mode(const char* s) {
mode_t mode = 0;
while (*s) {
if (*s >= '0' && *s <= '7') mode = (mode << 3) | (*s - '0');
else return -1;
s++;
}
return mode;
}
static Result<void> do_chmod(const BuiltinArguments& args) {
mode_t mode = get_mode(args[1].c_str());
if (fchmodat(AT_FDCWD, args[2].c_str(), mode, AT_SYMLINK_NOFOLLOW) < 0) {
return ErrnoErrorIgnoreEnoent() << "fchmodat() failed";
}
return {};
}4.2 chown
chown user [group] path 用 DecodeUid() 解析 uid/gid,最终调用 lchown(),不跟随符号链接。没有 group 时 gid 保持 -1,让内核不改变原 gid。
源码文件:system/core/init/builtins.cpp
static Result<void> do_chown(const BuiltinArguments& args) {
auto uid = DecodeUid(args[1]);
if (!uid.ok()) return Error() << "Unable to decode UID";
const std::string& path = (args.size() == 4) ? args[3] : args[2];
Result<gid_t> gid = -1;
if (args.size() == 4) {
gid = DecodeUid(args[2]);
if (!gid.ok()) return Error() << "Unable to decode GID";
}
if (lchown(path.c_str(), *uid, *gid) == -1) {
return ErrnoErrorIgnoreEnoent() << "lchown() failed";
}
return {};
}4.3 差异
chmod 改 mode,chown 改 owner;两者都对 dangling/missing 路径使用 ErrorIgnoreEnoent() 形式的错误包装。它们不会自动创建目标,也不会替代 SELinux restorecon。
5. write
5.1 写入者
do_write() 把 path 和 content 交给 WriteFile(),不自己打开 fd,也不解释 procfs/sysfs 的语义。目标文件的消费者是内核虚拟文件系统或普通文件实现。
源码文件:system/core/init/builtins.cpp
static Result<void> do_write(const BuiltinArguments& args) {
if (auto result = WriteFile(args[1], args[2]); !result.ok()) {
return ErrorIgnoreEnoent()
<< "Unable to write to file '" << args[1] << "': " << result.error();
}
return {};
}5.2 内容边界
content 已经经过 Action 执行时的 property 展开,但不会经过 shell 解析。空格、引号和 $ 是否写入,取决于 tokenizer 和 ExpandProps 的结果;write 不会自动追加 shell 风格换行。
5.3 错误语义
目标不存在的错误被 ErrorIgnoreEnoent() 包装,具体是否忽略由该错误类型和调用链处理;权限、只读文件系统和内核拒绝仍会返回失败。命令返回成功只表示 WriteFile() 接受写入,不表示 sysfs 改变达到了更高层业务状态。
6. 组合路径
6.1 目录到服务
典型启动 action 可能先 mkdir 创建运行目录,再 chown/chmod 校正访问权限,最后 class_start 启动引用该目录的服务。命令之间由 Action 的顺序保证,但每条 builtin 的错误不会自动撤销前面已经发生的系统调用。
6.2 失败后续
如果 mkdir 失败,后续 command 仍由 ActionManager 按序调用;是否继续产生有意义结果取决于下一个 builtin。若 class_start 中某个服务启动失败,其他匹配 Service 仍会被遍历。这是两个不同的“继续”边界,不能统称为命令成功。
6.3 subcontext
注册表中的 true 表示命令允许/需要在 subcontext 侧执行;class_start 是 false,因为它操作 init 所有的 ServiceList。这个标志影响 Command::InvokeFunc() 的执行者,不改变 builtin 参数校验。
7. 验证方法
7.1 源码断言
读者可以对每条命令完成四项断言:
注册表参数范围
-> BuiltinArguments 下标
-> 具体 syscall 或 ServiceList 消费者
-> Result<void> 错误包装例如 chown system system /path 应映射到 args[1]/args[2]/args[3];chown system /path 则 gid 为 -1,路径下标变成 args[2]。
7.2 只读检查
在 userdebug/eng 设备上可检查:
adb shell ls -ld /path
adb shell stat -c '%a %u %g %n' /path
adb shell getprop init.svc.example
adb shell logcat -b all -d | grep -E 'processing action|Command .*failed'这些命令能验证文件元数据、Service 状态和 init 日志,不能证明 SELinux allow、内核 sysfs 语义或服务业务 ready。
7.3 命令边界
本文覆盖 builtin 注册、参数路由和五个实现;util_test.cpp 的 WriteFile/目录辅助测试只涉及底层写入和目录工具,不能替代设备权限、FBE 和 class 启动实验。后续命令专题负责 mount、property、restorecon 等未展开命令。
