Skip to content

分区rc加载

追踪 system、system_ext、vendor、odm 和 product 的 init rc 如何被扫描、延迟导入、解析并路由到不同执行上下文。

基于android-17.0.0_r1
AndroidinitvendorodmTreblesubcontext

分区rc加载 ​

本文面向已经读过ImportParser、init.rc主配置和Zygote服务的读者。主题不是把五个目录列成一张表,而是回答一个源码问题:同一个 on 或 service 文本,为什么在 /system/etc/init、/vendor/etc/init 和 /odm/etc/init 的导入时机、执行上下文、可见属性和覆盖规则都不同?

先给出边界:分区目录“被扫描”不等于目录内每条命令已经执行;vendor/odm 进入 vendor_init subcontext 不等于所有命令都在子进程中运行;override 也不等于可以跨 Treble 边界替换任意 Service。下面沿着真实调用链拆开这些容易混淆的判断。

1. 加载入口 ​

1.1 根文件入口 ​

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

这里的 /vendor/etc/init/hw/init.${ro.hardware}.rc 是显式文件 import;它和后面的 /vendor/etc/init 目录扫描不是同一个入口。一个设备可能同时拥有硬件选择 rc、vendor 目录 rc、odm 目录 rc 和 product 目录 rc,它们的解析记录来自不同的 ParseConfig 调用。

1.2 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);
    }
}

加载顺序是源码中的顺序:标准 system root 文件、system 目录、system_ext 目录、vendor 目录、odm 目录、product 目录;但 system_ext 的失败不会进入 late_import_paths,而其他三个目录的失败会记录待后续导入。这个差异是 Android 17 代码直接写出的兼容策略,不能概括成“所有分区都延迟导入”。

1.3 属性替代入口 ​

当 ro.boot.init_rc 非空时,函数不会继续执行默认的五条输入路径,而是只调用 parser.ParseConfig(bootscript)。因此现场排查“vendor rc 为什么没有生效”时,第一步不是 grep 目录,而是确认是否存在自定义 bootscript;否则你可能一直追踪一条根本没有被选中的配置链。

图中的虚线不是“目录稍后一定成功”的保证,只表示 ParseConfig 返回 false 后路径被记录。真正重试发生在 mount_all 的 import_late(),并且可能因为设备 fstab、阶段模式或后续 Action 失败而没有到达。

2. 文件扫描 ​

2.1 单文件与目录 ​

相关源码:

  • system/core/init/parser.cpp
  • ParseConfig
  • ParseConfigDir
cpp
bool Parser::ParseConfigDir(const std::string& path) {
    std::unique_ptr<DIR, decltype(&closedir)> config_dir(opendir(path.c_str()), closedir);
    if (!config_dir) {
        PLOG(INFO) << "Could not import directory '" << path << "'";
        return false;
    }

    std::vector<std::string> files;
    dirent* current_file;
    while ((current_file = readdir(config_dir.get()))) {
        if (current_file->d_type == DT_REG) {
            std::string current_path = android::base::StringPrintf(
                    "%s/%s", path.c_str(), current_file->d_name);
            files.emplace_back(current_path);
        }
    }

    std::sort(files.begin(), files.end());
    for (const auto& file : files) {
        if (auto result = ParseConfigFile(file); !result.ok()) {
            LOG(ERROR) << "could not import file '" << file << "': " << result.error();
        }
    }
    return true;
}

bool Parser::ParseConfig(const std::string& path) {
    if (is_dir(path.c_str())) return ParseConfigDir(path);
    auto result = ParseConfigFile(path);
    if (!result.ok()) LOG(INFO) << result.error();
    return result.ok();
}

目录扫描只接收 DT_REG 普通文件,并先按完整路径排序;它不递归扫描子目录,也不按文件修改时间排序。单个 rc 文件解析失败会记录错误但不让目录扫描函数整体返回 false。于是“目录存在”与“目录内某文件解析成功”是两层结果。

2.2 import延迟 ​

相关源码:

  • system/core/init/import_parser.cpp
  • ParseSection
  • EndFile
cpp
Result<void> ImportParser::ParseSection(std::vector<std::string>&& args,
                                        const std::string& filename, int line) {
    if (args.size() != 2) {
        return Error() << "single argument needed for import\n";
    }

    auto conf_file = ExpandProps(args[1]);
    if (!conf_file.ok()) {
        return Error() << "Could not expand import: " << conf_file.error();
    }

    if (filename_.empty()) filename_ = filename;
    imports_.emplace_back(std::move(*conf_file), line);
    return {};
}

void ImportParser::EndFile() {
    auto current_imports = std::move(imports_);
    imports_.clear();
    for (const auto& [import, line_num] : current_imports) {
        parser_->ParseConfig(import);
    }
}

import 不在读到这一行时立即递归调用 parser,而是在当前文件结束时消费 imports_。因此同一文件中的 import 仍按 import 行出现顺序解析;line_num 会保存在 imports_ 中,但 EndFile() 当前调用的是 ParseConfig(import),不会把行号继续传入单文件解析接口。目录扫描和文件内 import 是两种嵌套关系:前者按文件名排序,后者按 import 行出现顺序。

2.3 不存在的文件 ​

ParseConfig 对不存在的单文件返回 false;而 ImportParser::EndFile() 不会把每个 import 的 false 继续向上传递为整个父文件失败。实际排障必须读 init 日志中的 Parsing file、Could not import 和 parser error,不能只看最终 ServiceList 是否有某个服务。

3. 执行上下文 ​

3.1 subcontext初始化 ​

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

相关函数/类型:InitializeSubcontext

cpp
void InitializeSubcontext() {
    if (IsMicrodroid()) {
        LOG(INFO) << "Not using subcontext for microdroid";
        return;
    }

    if (SelinuxGetVendorAndroidVersion() >= __ANDROID_API_P__) {
        subcontext.reset(new Subcontext(
                std::vector<std::string>{"/vendor", "/odm"},
                std::vector<std::string>{"VENDOR", "ODM"},
                kVendorContext));
    }
}

Android 17 普通设备把 /vendor 和 /odm 作为路径前缀,把 VENDOR、ODM 作为 partition 名加入 subcontext。Microdroid 明确跳过这套初始化;所以“vendor rc 一定由 vendor_init 执行”不能外推到 Microdroid。

3.2 路径匹配 ​

相关源码:

  • system/core/init/subcontext.cpp
  • PathMatchesSubcontext
  • PartitionMatchesSubcontext
cpp
bool Subcontext::PathMatchesSubcontext(const std::string& path) const {
    auto apex_name = GetApexNameFromFileName(path);
    if (!apex_name.empty()) {
        return std::find(apex_list_.begin(), apex_list_.end(), apex_name) != apex_list_.end();
    }
    for (const auto& prefix : path_prefixes_) {
        if (StartsWith(path, prefix)) return true;
    }
    return false;
}

bool Subcontext::PartitionMatchesSubcontext(const std::string& partition) const {
    return std::find(partitions_.begin(), partitions_.end(), partition) != partitions_.end();
}

路径匹配是字符串前缀/已登记 APEX 名称判断,不是“文件来自哪个 Git project”的判断。/vendor/etc/init/foo.rc 匹配;/system/etc/init/foo.rc 不匹配;vendor APEX 是否匹配还依赖 apex_list_ 是否由 apex-info-list 登记。

3.3 对象归属 ​

相关源码:

  • system/core/init/service_parser.cpp
  • ParseSection
  • system/core/init/action_parser.cpp
  • ParseSection
cpp
Subcontext* restart_action_subcontext = nullptr;
if (subcontext_ && subcontext_->PathMatchesSubcontext(filename)) {
    restart_action_subcontext = subcontext_;
}

service_ = std::make_unique<Service>(name, restart_action_subcontext,
                                     filename, str_args);

Service parser 把路径匹配结果保存到新建 Service;Action parser 使用相同的路径判断,但把结果交给 Action。

cpp
Subcontext* action_subcontext = nullptr;
if (subcontext_ && subcontext_->PathMatchesSubcontext(filename)) {
    action_subcontext = subcontext_;
}

if (StartsWith(filename, "/apex/") && !action_subcontext) {
    return Error() << "'on' is supported for only Vendor APEXes.";
}

Service 的 subcontext_ 主要决定其 onrestart Action 如何执行;Action 的 subcontext_ 则决定该 Action 中每条 builtin 的执行路由。二者都根据 rc 文件路径计算,但不是把整个 Parser 进程搬到 vendor_init。

图中最重要的分叉是 execute_in_subcontext_:文件路径决定“该 Action 绑定哪个 subcontext”,builtin 注册表再决定具体命令是否真的通过 IPC 发给 vendor_init。文件来源和单条命令执行者不是一一对应关系。

4. 命令路由 ​

4.1 InvokeFunc ​

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

相关函数/类型:Command::InvokeFunc

cpp
Result<void> Command::InvokeFunc(Subcontext* subcontext) const {
    if (subcontext) {
        if (execute_in_subcontext_) {
            return subcontext->Execute(args_);
        }

        auto expanded_args = subcontext->ExpandArgs(args_);
        if (!expanded_args.ok()) return expanded_args.error();
        return RunBuiltinFunction(func_, *expanded_args, subcontext->context());
    }

    return RunBuiltinFunction(func_, args_, kInitContext);
}

当命令允许 subcontext 执行时,init 通过 protobuf/socketpair 发送命令;不允许时,subcontext 只负责 property 展开,真正 builtin 仍在 init 上下文执行。比如涉及 ServiceList 或全局 ActionManager 的命令必须留在 init,不能看到 vendor rc 就认为它会在 vendor_init 中执行。

4.2 子进程IPC ​

相关源码:

  • system/core/init/subcontext.cpp
  • Fork
  • SubcontextProcess::RunCommand
cpp
void Subcontext::Fork() {
    unique_fd subcontext_socket;
    Socketpair(AF_UNIX, SOCK_SEQPACKET | SOCK_CLOEXEC, 0,
                &socket_, &subcontext_socket);

    auto result = fork();
    if (result == 0) {
        socket_.reset();
        int child_fd = dup(subcontext_socket.get());
        setexeccon(context_.c_str());
        auto init_path = GetExecutablePath();
        auto child_fd_string = std::to_string(child_fd);
        const char* args[] = {init_path.c_str(), "subcontext",
                              context_.c_str(), child_fd_string.c_str(), nullptr};
        execv(init_path.data(), const_cast<char**>(args));
    }
}

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

相关函数/类型:SubcontextProcess::RunCommand

父进程完成 exec 后,子进程主循环接收 protobuf 命令;下面的第二段代码展示 builtin 查找、执行和结果回传。

cpp
void SubcontextProcess::RunCommand(
        const SubcontextCommand::ExecuteCommand& execute_command,
        SubcontextReply* reply) const {
    std::vector<std::string> args;
    for (const auto& string : execute_command.args()) args.emplace_back(string);

    auto map_result = function_map_->Find(args);
    if (!map_result.ok()) {
        reply->mutable_failure()->set_error_string(map_result.error().message());
        return;
    }
    auto result = RunBuiltinFunction(map_result->function, args, context_);
    if (result.ok()) reply->set_success(true);
    else reply->mutable_failure()->set_error_string(result.error().message());
}

上面是教学摘录,保留了真实 socketpair、setexeccon、exec 和命令回传关系;完整源码还包含 fd 错误、切换默认 mount namespace、protobuf 序列化、shutdown 通知和子进程重启。subcontext 失败时,TransmitMessage() 会重启子进程并把错误返回给 init,不能把 vendor 命令失败隐藏成“平台 init 自动重试”。

4.3 builtin权限 ​

权限边界最终由 builtin 注册表中的 run_in_subcontext 标志和 SELinux policy 共同决定。mkdir、write、restorecon 等资源操作可能允许 subcontext;class_start、trigger、mount_all 等涉及全局调度或阶段导入的命令注册为 init 上下文。真正结论必须回到 GetBuiltinFunctionMap() 和 policy,不能按命令名字猜。

5. 覆盖边界 ​

5.1 override解析 ​

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

相关函数/类型:EndSection

cpp
Service* old_service = service_list_->FindService(service_->name());
if (old_service) {
    if (!service_->is_override()) {
        return Error() << "ignored duplicate definition of service '"
                       << service_->name() << "'";
    }

    if (StartsWith(filename_, "/apex/") && !old_service->is_updatable()) {
        return Error() << "cannot update a non-updatable service '"
                       << service_->name() << "' with a config in APEX";
    }

    std::string context = service_->subcontext()
            ? service_->subcontext()->context() : "";
    std::string old_context = old_service->subcontext()
            ? old_service->subcontext()->context() : "";
    if (context != old_context) {
        return Error() << "service '" << service_->name()
                       << "' overrides another service across the Treble boundary.";
    }

    service_list_->RemoveService(*old_service);
}
service_list_->AddService(std::move(service_));

override 只绕过普通重复定义错误,仍需满足 APEX updatable 和 subcontext context 相同等条件。vendor service 不能借 override 替换 platform context 中的同名 Service;这不是一条“SELinux 之后再决定”的宽松机制,而是 parser 在加入 ServiceList 前直接拒绝。

5.2 Action叠加 ​

Action 并没有与 Service override 相同的替换语义。多个文件中的同一 on boot 或同一 property trigger 会形成多个 Action,之后由 ActionManager 按解析顺序匹配执行。vendor rc 可以添加同名 event action,但不能因此理解为修改了 root action 的 command 列表。

5.3 属性触发边界 ​

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

相关函数/类型:IsActionableProperty

cpp
static constexpr const char* kPartnerPrefixes[] = {
    "init.svc.vendor.", "ro.vendor.", "persist.vendor.",
    "vendor.", "init.svc.odm.", "ro.odm.",
    "persist.odm.", "odm.", "ro.boot.",
};

if (!IsActionableProperty(subcontext, prop_name)) {
    return Error() << "unexported property trigger found: " << prop_name;
}

启用 ro.actionable_compatible_property.enabled 时,vendor/odm 合作属性前缀可以直接进入 partner trigger 白名单;其他属性需要 CanReadProperty(subcontext->context(), prop_name) 证明 vendor_init 可读。这个检查发生在 rc 解析阶段,所以失败表现为 parser error,不是运行时 PropertyChanged 没有触发。

6. 分区与APEX ​

6.1 vendor/odm目录 ​

/vendor/etc/init 和 /odm/etc/init 在目录成功打开时按文件名排序解析;若它们在 SecondStageMain 时不可见,路径进入 late_import_paths,由后续 mount_all 调用 import_late()。

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

相关函数/类型:import_late

cpp
static void import_late(const std::vector<std::string>& rc_paths) {
    Parser parser = CreateParser(ActionManager::GetInstance(),
                                 ServiceList::GetInstance());
    if (rc_paths.empty()) {
        for (const auto& path : late_import_paths) parser.ParseConfig(path);
        late_import_paths.clear();
    } else {
        for (const auto& rc_path : rc_paths) parser.ParseConfig(rc_path);
    }
}

mount_all 自己还可以携带 rc paths;若参数明确给出路径,就解析这些路径,而不是消费全局 late_import_paths。这解释了为什么设备 mount_all /vendor/etc/fstab.ranchu --early 和默认 late import 不是同一机制。

6.2 Vendor APEX ​

相关源码:

  • system/core/init/action_parser.cpp
  • ActionParser::ParseSection
  • system/core/init/init.cpp
  • CreateApexConfigParser
cpp
if (StartsWith(filename, "/apex/") && !action_subcontext) {
    return Error() << "ParseSection() failed: 'on' is supported for only Vendor APEXes.";
}

APEX rc 不是一律等于 platform rc。CreateApexConfigParser() 会读取 /apex/apex-info-list.xml,把与 vendor/odm partition 匹配的 APEX 名称交给 subcontext;只有这些 vendor APEX 的 on Action 才能通过路径匹配进入 vendor_init。mainline APEX 不能任意注册稳定的 init event/property trigger。

6.3 资源可见时机 ​

Vendor/odm rc 里的可执行文件和 /sys 节点是否存在,取决于分区挂载、APEX namespace 和 init 阶段;Parser 只负责读文本和建立对象。一个 service definition 已进入 ServiceList,不等于它现在可以 execv();Service::Start() 仍会检查路径、SELinux context、namespace 和 socket。

7. 测试与排查 ​

7.1 Parser输入 ​

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

相关函数/类型:EventTriggerOrderMultipleFiles

测试构造直接 import、目录 import、目录内递归 import 和多个 rc 文件,断言六个 command 的执行序号。这证明 Parser 的文件排序和 import 递归进入同一个 ActionManager,不证明真实 vendor 分区的 mount 时机或 SELinux domain。

7.2 Service边界 ​

相关源码:

  • system/core/init/init_test.cpp
  • IgnoreDuplicateService
  • OverrideService

IgnoreDuplicateService 证明同名 Service 没有 override 时保留先定义并记录重复错误;OverrideService 证明显式 override 才会替换同名对象。这些是 host 解析测试,不证明跨 subcontext override 可以成功;跨 Treble context 的拒绝逻辑仍需结合 service_parser.cpp 直接阅读。

7.3 Subcontext故障 ​

相关源码:

  • system/core/init/subcontext.cpp
  • TransmitMessage
  • SubcontextChildReap

subcontext socket 发送失败或回复解析失败会触发 Restart();子进程被 reap 且不是 shutdown 结束时也会重启 subcontext。重启的是 vendor_init 执行器,不是重新解析全部 boot scripts;已经发生的 builtin 副作用不会自动回滚。

7.4 现场检查 ​

bash
adb shell getprop ro.boot.init_rc
adb shell getprop ro.hardware
adb shell ls -ld /system/etc/init /system_ext/etc/init /vendor/etc/init /odm/etc/init /product/etc/init
adb shell logcat -b all -d -s init:I | grep -E 'Parsing (file|directory)|Could not import|ParseSection|subcontext'

ls 只能说明路径当前可见;Parser 日志才能说明是否尝试读取、是否因目录不可见进入延迟列表、是否发生 parser error。不要把 find /vendor/etc/init 的文件清单当作“全部已加载”。

7.5 边界表 ​

观察可以证明不能证明
Parsing directory /vendor/etc/initinit 尝试扫描该目录所有 rc 均无语法错误
Parsing file ...foo.rc某文件进入 Parser其 Action 已执行
init.svc.vendor.foo=runningService 父进程登记 runningvendor 业务 ready
subcontext restart 日志vendor_init 执行器重启之前命令已回滚
同名 Service 被 overrideparser 接受替换条件SELinux 业务权限正确

8. 加载验证 ​

用“vendor service 没有启动”完成一次源码验证:

  1. 确认 ro.boot.init_rc,排除自定义 bootscript;
  2. 检查 /vendor/etc/init 是否在第一次 LoadBootScripts() 时可见;
  3. 若不可见,沿 late_import_paths → mount_all → import_late 查找第二次解析;
  4. 查看 service rc 的真实文件路径,判断 PathMatchesSubcontext 是否绑定 vendor_init;
  5. 对每个命令检查 BuiltinFunctionMap 的 execute_in_subcontext_ 标志;
  6. 追踪 Service::Start() 的可执行文件、SELinux context、socket 和 namespace 检查;
  7. 用 init.svc.*、init parser 日志和进程/文件现场结果分别验证,不用单个状态 property 代替整条链。

本文证明了 Android 17 分区 rc 的输入选择、目录排序、延迟导入、Service/Action subcontext 绑定、命令 IPC 和 override/property 边界;没有证明具体厂商的 SELinux allow、fstab 挂载成功、vendor HAL 的 Binder 注册或业务 ready。下一篇将进入条件表达式,解释 property trigger 如何在这些分区边界上决定 Action 是否可见和可执行。