Action解析
本文面向已经读过 rc语法 和 Init主循环 的读者。前文解释了 tokenizer 如何把一行切成参数;本文把范围收窄到 on 段:ActionParser 如何建立事件和属性条件,Action 如何保存命令,ActionManager 如何在事件队列中找到消费者。
本文不重复 tokenizer,也不展开每个 builtin 的实现。重点是一个可追踪问题:给定 on boot && property:ro.hardware=*,哪些字段在解析时被拥有,什么时候才匹配,命令失败后 Action 是否消失,以及新增的 rc 文件为什么可能错过当前事件。
1. 对象分工
1.1 三个所有者
| 对象 | 解析阶段职责 | 运行阶段职责 |
|---|---|---|
ActionParser | 读取 on 首行和 body,创建/提交 Action | 不执行命令 |
Action | 保存 event、property、命令和源位置 | 判断事件、逐条调用 Command |
ActionManager | 接收已完成的 Action | 保存 Action 列表、消费事件队列 |
ActionParser 自己只持有当前段的 action_;ActionManager 才拥有提交后的长期列表。这个边界解释了为什么 EndSection() 是关键生效点:在它执行前,后续事件看不到这条 Action。
1.2 数据形状
源码文件:system/core/init/action.h
相关函数/类型:Action
class Action {
private:
std::map<std::string, std::string> property_triggers_;
std::string event_trigger_;
std::vector<Command> commands_;
bool oneshot_;
Subcontext* subcontext_;
std::string filename_;
int line_;
};一个 Action 最多有一个 event trigger,可以有多个不同名称的 property trigger;命令顺序由 commands_ 保留。filename_ 和 line_ 不是装饰信息,执行日志和故障定位依赖它们。
2. 首行解析
2.1 触发列表
ActionParser::ParseSection() 收到的 args 包含首 token on,因此先从下标 1 开始复制 trigger。空 trigger 直接失败;解析成功后才创建 Action。
源码文件:system/core/init/action_parser.cpp
Result<void> ActionParser::ParseSection(
std::vector<std::string>&& args,
const std::string& filename, int line) {
std::vector<std::string> triggers(args.begin() + 1, args.end());
if (triggers.size() < 1) {
return Error() << "Actions must have a trigger";
}
std::string event_trigger;
std::map<std::string, std::string> property_triggers;
if (auto result = ParseTriggers(
triggers, action_subcontext, &event_trigger, &property_triggers);
!result.ok()) {
return Error() << "ParseTriggers() failed: " << result.error();
}
auto action = std::make_unique<Action>(
false, action_subcontext, filename, line,
event_trigger, property_triggers);
action_ = std::move(action);
return {};
}旧稿把 Action 先构造、再“设置 trigger”描述成可变对象;Android 17 的构造函数直接接收解析结果,避免出现一个已经暴露但触发条件还未完成的中间 Action。
2.2 事件名
ParseTriggers() 将不以 property: 开头的 token 视为 event trigger。event trigger 只能出现一个,并在 Android R 及以后通过 ValidateEventTrigger() 限制为字母数字、下划线和连字符。
源码文件:system/core/init/action_parser.cpp
if (!event_trigger->empty()) {
return Error() << "multiple event triggers are not allowed";
}
if (auto result = ValidateEventTrigger(args[i]); !result.ok()) {
return result;
}
*event_trigger = args[i];因此 on boot && early-init 不是两个事件的“同时触发”,而是解析错误。on boot: 也会因冒号不符合字符规则而增加 parse error;init_test.cpp 的 WrongEventTrigger 对此有反向断言。
2.3 连接符
触发器 token 按奇偶位置解析:偶数位置必须是 trigger,奇数位置必须是 &&。其他连接符不会被当作逻辑运算符,也没有 OR 语义。
源码文件:system/core/init/action_parser.cpp
for (std::size_t i = 0; i < args.size(); ++i) {
if (args[i].empty()) {
return Error() << "empty trigger is not valid";
}
if (i % 2) {
if (args[i] != "&&") {
return Error() << "&& is the only symbol allowed to concatenate actions";
}
continue;
}
// 处理 property 或 event
}3. 属性条件
3.1 键值分割
ParsePropertyTrigger() 去掉 property: 前缀后寻找第一个 =。左侧是 property 名,右侧是完整 value;缺少 = 的 token 失败。value 中后续的 = 不会被当成新的分隔符。
源码文件:system/core/init/action_parser.cpp
std::string prop_name(trigger.substr(prop_str.length()));
size_t equal_pos = prop_name.find('=');
if (equal_pos == std::string::npos) {
return Error() << "property trigger found without matching '='";
}
std::string prop_value(prop_name.substr(equal_pos + 1));
prop_name.erase(equal_pos);3.2 重复键
property 条件保存于 std::map。使用 emplace() 的插入结果拒绝同一个 property 的第二次出现;这比后写覆盖前写更严格,也避免 on property:x=1 && property:x=2 产生无法解释的条件。
源码文件:system/core/init/action_parser.cpp
if (auto [it, inserted] = property_triggers->emplace(prop_name, prop_value);
!inserted) {
return Error() << "multiple property triggers found for same property";
}3.3 读取边界
当 actionable-compatible property 开关启用且 Action 运行在 subcontext 中时,IsActionableProperty() 允许一组 partner 前缀,其他 property 必须通过 CanReadProperty()。这是 parse-time 的安全检查,不是事件发生时才检查的权限。
源码文件: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.",
};4. 段上下文
4.1 Subcontext选择
ActionParser 收到的 Subcontext* 并不会无条件挂到所有 Action。只有源文件路径匹配该 subcontext 时,才把它保存为 action_subcontext;否则使用 null,命令在 init context 运行。
源码文件:system/core/init/action_parser.cpp
Subcontext* action_subcontext = nullptr;
if (subcontext_ && subcontext_->PathMatchesSubcontext(filename)) {
action_subcontext = subcontext_;
}4.2 APEX限制
源码还限制 /apex/ 文件的 on 段:如果文件不属于可识别的 Vendor APEX subcontext,就直接拒绝。原因不是路径格式,而是 mainline APEX 不应依赖不稳定的 init 事件和 property。
源码文件:system/core/init/action_parser.cpp
if (StartsWith(filename, "/apex/") && !action_subcontext) {
return Error() << "ParseSection() failed: 'on' is supported for only Vendor APEXes.";
}4.3 命令执行者
subcontext 存在时,Command::InvokeFunc() 根据 run_in_subcontext 决定两条路径:需要隔离执行的 builtin 交给 Subcontext::Execute();普通 builtin 先由 subcontext 展开参数,再在对应 context 调用函数。
源码文件:system/core/init/action.cpp
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);这一步发生在 Action 已匹配并开始执行之后,不是 ActionParser 的解析动作。把 subcontext 的执行隔离误写成“解析器创建了一个新进程”会混淆两个生命周期。
5. 命令绑定
5.1 关键字查找
ParseLineSection() 将 body 行交给 Action::AddCommand()。Action 的静态 function_map_ 必须已经由 init 设置;未设置时返回 no function map available。设置后,BuiltinFunctionMap::Find() 同时验证命令名和参数数量。
源码文件:system/core/init/action.cpp
Result<void> Action::AddCommand(
std::vector<std::string>&& args, int line) {
if (!function_map_) {
return Error() << "no function map available";
}
auto map_result = function_map_->Find(args);
if (!map_result.ok()) {
return Error() << map_result.error();
}
commands_.emplace_back(
map_result->function, map_result->run_in_subcontext,
std::move(args), line);
return {};
}5.2 参数延迟
解析时命令参数仍保存在 Command::args_;${property} 的普通 builtin 参数在 RunBuiltinFunction() 执行时展开。这样同一条 Action 可以在 property 改变后使用新值,而不是把解析瞬间的 property 快照写死。
源码文件:system/core/init/action.cpp
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);5.3 无效命令
命令关键字未知、参数个数不符或属性展开失败,都会让该命令返回错误。解析阶段已加入的其他 Action 不会因为这条命令失败而回滚;执行阶段的日志包含命令文本、Action trigger、文件和行号,消费者可以据此定位源头。
源码文件:system/core/init/action.cpp
LOG(INFO) << "Command '" << cmd_str << "' action=" << trigger_name
<< " (" << filename_ << ":" << command.line() << ") took "
<< duration.count() << "ms and "
<< (result.ok() ? "succeeded" : "failed: " + result.error().message());6. 段提交
6.1 EndSection
只有当前 Action 有至少一条命令时,EndSection() 才调用 ActionManager::AddAction()。空的 on boot 段不会进入 actions_,因此不会在事件匹配中留下一个“什么也不做”的对象。
源码文件:system/core/init/action_parser.cpp
Result<void> ActionParser::EndSection() {
if (action_ && action_->NumCommands() > 0) {
action_manager_->AddAction(std::move(action_));
}
return {};
}6.2 EOF提交
解析器在 EOF 也会调用 EndSection(),所以文件最后一个 on 段不需要额外空行才能提交;这是 Parser::ParseData() 追加换行的直接结果。若 body 命令全部无效,NumCommands() 仍为零,该 Action 不会提交。
6.3 顺序保留
ActionManager::AddAction() 只是将 unique pointer 追加到 actions_。同一事件匹配多个 Action 时,ExecuteOneCommand() 按 actions_ 的顺序把它们放入 current_executing_actions_,因此解析文件/导入顺序最终会影响同一 trigger 的执行顺序。
源码文件:system/core/init/action_manager.cpp
void ActionManager::AddAction(std::unique_ptr<Action> action) {
actions_.emplace_back(std::move(action));
}下面的时序图把“段内暂存”和“事件消费”分成两个阶段。EndSection 是二者之间的所有权转移点;事件入队时,Parser 已经不再参与。
7. 匹配执行
7.1 事件匹配
事件触发调用 Action::CheckEvent(const EventTrigger&)。它要求 event 字符串相等,并且所有 property 条件都满足当前属性值。没有 event 的纯 property Action 不会被事件分支匹配。
源码文件:system/core/init/action.cpp
bool Action::CheckEvent(const EventTrigger& event_trigger) const {
return event_trigger == event_trigger_ && CheckPropertyTriggers();
}7.2 属性匹配
属性变化调用另一重载,要求 Action 没有 event trigger,并把刚变化的 (name, value) 作为快速检查项;其他 property 仍从 property service 读取。* 只对非空属性值匹配,空值不会满足通配条件。
源码文件:system/core/init/action.cpp
bool Action::CheckEvent(const PropertyChange& property_change) const {
const auto& [name, value] = property_change;
return event_trigger_.empty() && CheckPropertyTriggers(name, value);
}重载只负责限定候选 Action,真正的多属性合取发生在 CheckPropertyTriggers()。当前变化的键值直接使用事件携带值,其余键则读取当前 property 状态。
源码文件:system/core/init/action.cpp
for (const auto& [trigger_name, trigger_value] : property_triggers_) {
if (trigger_name != name) {
std::string prop_value = android::base::GetProperty(trigger_name, "");
if (trigger_value == "*" && !prop_value.empty()) continue;
if (trigger_value != prop_value) return false;
}
}
return true;7.3 运行循环
ActionManager::ExecuteOneCommand() 先在锁内消费事件队列,按 actions_ 顺序将匹配 Action 放入执行队列;真正的 InvokeFunc() 在释放锁后发生。这样 builtin 可以设置 property 或导入 rc,而不会在持有 event_queue_lock_ 时递归制造死锁。
源码文件:system/core/init/action_manager.cpp
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();
}8. 失败与恢复
8.1 命令失败
命令失败由 Command::InvokeFunc() 返回 Result<void>,Action::ExecuteCommand() 记录失败并继续由 ActionManager 推进下一个命令。源码没有因为一条 builtin 失败就自动撤销之前命令的事务语义;是否停止或重试取决于具体 builtin。
8.2 动态导入
如果一个 Action 执行 import,新 Action 会在执行过程中加入 ActionManager,但不会 retroactively 匹配已经从 event queue 取出的当前事件。LazilyLoadedActionsCantBeTriggeredByTheSameTrigger 测试输入一个 on boot 加载另一个 on boot,断言后者本轮不执行;下一次显式 trigger 才能消费它。
8.3 oneshot清理
普通 rc Action 的 oneshot_ 为 false,会留在 actions_ 供未来事件再次匹配。内置 Action 由 QueueBuiltinAction() 创建为 oneshot;最后一条命令执行完成后,ActionManager 才从 actions_ 移除它。清理发生在消费完成之后,不是在入队时。
源码文件:system/core/init/action_manager.cpp
if (current_command_ == action->NumCommands()) {
current_executing_actions_.pop();
current_command_ = 0;
if (action->oneshot()) {
actions_.erase(std::remove_if(actions_.begin(), actions_.end(), eraser),
actions_.end());
}
}9. 触发测试
9.1 触发测试
SimpleEventTrigger 的输入是一个 on boot 和 pass_test builtin,测试动作是向 ActionManager 入队 boot,断言 callback 被执行。它证明了“解析 → AddAction → 事件匹配 → 命令调用”这条最小正向路径,不证明属性权限或 subcontext。
EventTriggerOrder 再加入两个同名 on boot,其中一个附带 property:ro.hardware=*,断言三条命令按源码注册顺序执行。这证明 property 条件满足时不会改变 actions_ 的相对顺序。
9.2 失败测试
WrongEventTrigger 使用 on boot:,随后断言 parse_error_count() 为 1,证明非法 event 字符在解析阶段被拒绝。这个断言不涉及设备上的 SELinux policy,也不证明所有厂商扩展都会用相同 vendor API level。
9.3 可执行检查
读者可以从一个真实 init.rc 的 on 段开始,依次写出:
首行 token
-> event_trigger / property_triggers
-> Action::commands_
-> EndSection / ActionManager::actions_
-> CheckEvent
-> Command::InvokeFunc然后选择一个带 property: 的 Action,分别改变匹配 property 和无关 property,预测哪次 QueuePropertyChange 会进入执行队列。最后检查 processing action 日志中的文件与行号,确认运行消费者仍指向源 rc。
9.4 边界
本文没有把 trigger 解析等同于命令执行,也没有覆盖 ActionManager 全部时间戳、shutdown 清队列和所有 property service 细节;这些是相邻机制的边界。结论只适用于 Android 17 通用 init 源码,设备厂商的 subcontext、APEX 清单和 rc 加载集合仍需回到对应源码核验。
