分区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
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
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.cppParseConfigParseConfigDir
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.cppParseSectionEndFile
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
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.cppPathMatchesSubcontextPartitionMatchesSubcontext
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.cppParseSectionsystem/core/init/action_parser.cppParseSection
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。
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
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.cppForkSubcontextProcess::RunCommand
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 查找、执行和结果回传。
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
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
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
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.cppActionParser::ParseSectionsystem/core/init/init.cppCreateApexConfigParser
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.cppIgnoreDuplicateServiceOverrideService
IgnoreDuplicateService 证明同名 Service 没有 override 时保留先定义并记录重复错误;OverrideService 证明显式 override 才会替换同名对象。这些是 host 解析测试,不证明跨 subcontext override 可以成功;跨 Treble context 的拒绝逻辑仍需结合 service_parser.cpp 直接阅读。
7.3 Subcontext故障
相关源码:
system/core/init/subcontext.cppTransmitMessageSubcontextChildReap
subcontext socket 发送失败或回复解析失败会触发 Restart();子进程被 reap 且不是 shutdown 结束时也会重启 subcontext。重启的是 vendor_init 执行器,不是重新解析全部 boot scripts;已经发生的 builtin 副作用不会自动回滚。
7.4 现场检查
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/init | init 尝试扫描该目录 | 所有 rc 均无语法错误 |
Parsing file ...foo.rc | 某文件进入 Parser | 其 Action 已执行 |
init.svc.vendor.foo=running | Service 父进程登记 running | vendor 业务 ready |
| subcontext restart 日志 | vendor_init 执行器重启 | 之前命令已回滚 |
| 同名 Service 被 override | parser 接受替换条件 | SELinux 业务权限正确 |
8. 加载验证
用“vendor service 没有启动”完成一次源码验证:
- 确认
ro.boot.init_rc,排除自定义 bootscript; - 检查
/vendor/etc/init是否在第一次LoadBootScripts()时可见; - 若不可见,沿
late_import_paths → mount_all → import_late查找第二次解析; - 查看 service rc 的真实文件路径,判断
PathMatchesSubcontext是否绑定 vendor_init; - 对每个命令检查
BuiltinFunctionMap的execute_in_subcontext_标志; - 追踪
Service::Start()的可执行文件、SELinux context、socket 和 namespace 检查; - 用
init.svc.*、init parser 日志和进程/文件现场结果分别验证,不用单个状态 property 代替整条链。
本文证明了 Android 17 分区 rc 的输入选择、目录排序、延迟导入、Service/Action subcontext 绑定、命令 IPC 和 override/property 边界;没有证明具体厂商的 SELinux allow、fstab 挂载成功、vendor HAL 的 Binder 注册或业务 ready。下一篇将进入条件表达式,解释 property trigger 如何在这些分区边界上决定 Action 是否可见和可执行。
