Skip to content

内置命令上

追踪 class_start、mkdir、chmod、chown、write 从 builtin 注册、参数校验到系统资源变化的真实路径。

基于android-17.0.0_r1
Androidinitbuiltinsclass_startmkdirwrite

内置命令上 ​

本文面向已经读过 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.cpp
  • system/core/init/builtins.h
  • system/core/init/keyword_map.h
cpp
{"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

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

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

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

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

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

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

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 源码断言 ​

读者可以对每条命令完成四项断言:

text
注册表参数范围
  -> 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 设备上可检查:

bash
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 等未展开命令。