Skip to content

rc语法

追踪 init.rc 从字符流、section 分派到 Action 与 Service 对象提交的完整解析路径。

基于android-17.0.0_r1
Androidinitinit.rcParsertokenizer

rc语法 ​

本文面向已经读过 启动阶段 的读者。前文说明 LoadBootScripts() 会按分区装载 rc 文件,但没有展开“一行文本怎样变成 Action、Service 或 import 请求”。本文从 system/core/init/parser.cpp 的 Parser::ParseData() 出发,追踪 tokenizer、段解析器、属性展开、段结束提交和错误恢复。

本文解决的是 rc 的语法和解析边界,不讲 ActionManager 如何执行命令,也不讲 Service::Start() 如何 fork/exec;这两个消费者分别由 Init主循环 和后续服务专题负责。读完后,读者应能解释一行 on、service 或 import 的 owner,判断反斜杠、引号、注释和 ${property} 如何改变 token,并能从错误日志反推解析循环停在哪个边界。

1. 语法对象 ​

1.1 三类入口 ​

Android init 的通用 Parser 注册三个 section parser:service 交给 ServiceParser,on 交给 ActionParser,import 交给 ImportParser。它们不是三个独立的文件格式,而是同一字符流在行结束时根据首 token 分派到不同 owner。

相关源码:

  • system/core/init/init.cpp
  • system/core/init/parser.cpp
  • system/core/init/parser.h
cpp
Parser CreateParser(ActionManager& action_manager, ServiceList& service_list) {
    Parser parser;
    parser.AddSectionParser(
            "service", std::make_unique<ServiceParser>(&service_list, GetSubcontext()));
    parser.AddSectionParser(
            "on", std::make_unique<ActionParser>(&action_manager, GetSubcontext()));
    parser.AddSectionParser("import", std::make_unique<ImportParser>(&parser));
    return parser;
}
首 token段 owner解析结果结束时消费者
onActionParser事件/属性触发器和命令列表ActionManager
serviceServiceParser服务名、argv、进程属性和 flagsServiceList
importImportParser延迟展开的文件路径Parser::ParseConfig

1.2 单行入口 ​

Parser 还维护 line_callbacks_。解析到一行后,callback 会先于 section parser 检查首 token 前缀,并结束当前 section。这个分支主要给 ueventd 的 /sys/、/dev/ 规则使用,因此“所有 rc 行都属于 on 或 service”并不成立。

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

cpp
auto line_callback = std::find_if(
        line_callbacks_.begin(), line_callbacks_.end(),
        [&args](const auto& c) {
            return android::base::StartsWith(args[0], c.first);
        });
if (line_callback != line_callbacks_.end()) {
    end_section();
    if (auto result = line_callback->second(std::move(args)); !result.ok()) {
        parse_error_count_++;
        LOG(ERROR) << filename << ": " << state.line << ": " << result.error();
    }
}

1.3 文件边界 ​

ParseConfig() 根据路径是文件还是目录选择 ParseConfigFile() 或 ParseConfigDir()。目录只收集普通文件,随后按完整路径排序;不存在的目录返回 false,由调用方决定是否延迟导入或继续启动。

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

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) {
            files.emplace_back(android::base::StringPrintf(
                    "%s/%s", path.c_str(), current_file->d_name));
        }
    }
    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;
}

2. 字符切词 ​

2.1 基本字符 ​

next_token() 并不返回一个抽象的“语法树 token”。它返回 T_TEXT、T_NEWLINE 或 T_EOF,并把文本首地址写入 parse_state::text。空格、制表符和回车只作为分隔符;换行则保留为独立 token,让上层知道一行何时结束。

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

cpp
switch (*x) {
case 0:
    state->ptr = x;
    return T_EOF;
case '\n':
    x++;
    state->ptr = x;
    return T_NEWLINE;
case ' ':
case '\t':
case '\r':
    x++;
    continue;
case '#':
    while (*x && (*x != '\n')) x++;
    if (*x == '\n') {
        state->ptr = x + 1;
        return T_NEWLINE;
    }
    state->ptr = x;
    return T_EOF;
}

注释从 # 开始直到行尾;"#" 处于引号内部时不会进入这个分支,因为 tokenizer 已经在 quoted text 分支中逐字符复制内容。

2.2 引号规则 ​

双引号用于把空格保留在同一个 token 内。引号本身不会出现在结果中,连续引号可以拼接相邻文本;反斜杠并不是“转义引号”的通用机制,源码对 \" 的处理会让引号状态以未闭合方式结束,测试明确覆盖了这一点。

text
service demo "/system/bin/name with space"
    user "system"

上例的 argv 中程序路径是一个 token,user 的值也是一个 token。不要把 shell 的引号规则移植到 init:tokenizer 支持的是源码中列明的字符处理,不支持 shell 命令替换、单引号或反斜杠引号转义。

2.3 反斜杠 ​

反斜杠有两种教材上必须区分的用途。反斜杠后跟 n、r、t 或反斜杠会写入对应控制字符;反斜杠后跟真实换行(包括 CRLF)则合并两行,并吞掉下一行开头的空格和制表符。

text
service demo /system/bin/demo \
    --mode "long value" \
    --flag
    class main

前三行最终组成一组 service 首行 argv。class main 仍是下一条独立行,因为前一行没有反斜杠。tokenizer 测试 cr_lf 验证了 LF、CRLF 和单独 CR 的继续规则;测试还证明未知转义会去掉反斜杠并保留后一个字符,而不是报错。

3. 行分派 ​

3.1 缓冲行 ​

Parser::ParseData() 先给输入追加一个换行和空字符,保证没有尾部换行的文件也能触发一次正常的行结束路径。每个 T_TEXT 追加到 args;只有拿到 T_NEWLINE 才决定这一行属于哪个 parser。

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

cpp
data->push_back('\n');
data->push_back('\0');
parse_state state;
state.line = 0;
state.ptr = data->data();
state.nexttoken = 0;
SectionParser* section_parser = nullptr;
int section_start_line = -1;
std::vector<std::string> args;

这意味着 parser 不会在读到 on 的第一个 token 时立即创建 Action;它必须等整行 token 完成,才能验证触发器参数或 service 的程序路径。

3.2 段切换 ​

遇到合法 section 首 token 时,解析器先调用旧 parser 的 EndSection(),再把 section_parser 指向新 parser,最后调用新 parser 的 ParseSection()。所以前一个对象的提交时机是“下一段开始”或 EOF,而不是每条 option 结束时。

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

cpp
} else if (section_parsers_.count(args[0])) {
    end_section();
    section_parser = section_parsers_[args[0]].get();
    section_start_line = state.line;
    if (auto result = section_parser->ParseSection(
                std::move(args), filename, state.line); !result.ok()) {
        parse_error_count_++;
        LOG(ERROR) << filename << ": " << state.line << ": " << result.error();
        section_parser = nullptr;
        bad_section_found = true;
    }
}

3.3 错误恢复 ​

section 首行失败后,section_parser 被清空,bad_section_found 置为 true。后续普通行不会被错误地交给刚刚失败的 parser;直到下一个合法 section 出现,才恢复可解析状态。错误计数增加,但整个文件扫描仍可继续。

4. 触发段 ​

4.1 触发解析 ​

ActionParser::ParseSection() 去掉首 token on 后调用 ParseTriggers()。触发器可以是一个事件、一个或多个 property 条件,条件之间只能使用 &&;同一 property 不能出现两次。

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

cpp
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();
}
action_ = std::make_unique<Action>(
        false, action_subcontext, filename, line, event_trigger, property_triggers);

on boot && property:sys.usb.config=adb 的 event owner 是 Action,property map 也是 Action 的状态;触发时机和队列消费则属于后续 ActionManager。文章只负责证明解析阶段保存了什么,不把它误写成已经执行。

4.2 命令提交 ​

每一行 body 由 Action::AddCommand() 接收。只有当前段结束且命令数量大于零时,EndSection() 才把 Action 移交给 ActionManager。

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

cpp
Result<void> ActionParser::ParseLineSection(
        std::vector<std::string>&& args, int line) {
    return action_ ? action_->AddCommand(std::move(args), line) : Result<void>{};
}

Result<void> ActionParser::EndSection() {
    if (action_ && action_->NumCommands() > 0) {
        action_manager_->AddAction(std::move(action_));
    }
    return {};
}

没有命令的 on 段不会进入 ActionManager。命令参数错误会在 AddCommand() 阶段被记录,但已提交的其他 Action 不会因为它自动回滚。

4.3 条件边界 ​

Android 17 还会检查 property trigger 的可读性。在启用 actionable-compatible property 且存在 vendor subcontext 时,未授权读取的属性会被拒绝;Vendor APEX 之外的 APEX 不能使用 on。这不是普通语法的装饰,而是解析时的安全边界。

5. 服务段 ​

5.1 首行 ​

ServiceParser::ParseSection() 要求至少包含服务名和程序路径,并用 IsValidName() 检查服务名,因为 init 后续会把它拼进 init.svc.<name> 属性。

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

cpp
if (args.size() < 3) {
    return Error() << "services must have a name and a program";
}
const std::string& name = args[1];
if (!IsValidName(name)) {
    return Error() << "invalid service name '" << name << "'";
}
std::vector<std::string> str_args(args.begin() + 2, args.end());
service_ = std::make_unique<Service>(
        name, restart_action_subcontext, filename, str_args);

5.2 选项表 ​

服务 body 不是一串 if/else,而是一个带参数个数约束的 keyword map。以 Android 17 为例,class 至少一个参数,disabled 不带参数,socket 需要三到六个参数,未知 option 由 map 查找直接报错。

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

相关函数/类型:ServiceParser::GetParserMap

cpp
{"class",         {1, kMax, &ServiceParser::ParseClass}},
{"disabled",      {0, 0,    &ServiceParser::ParseDisabled}},
{"onrestart",     {1, kMax, &ServiceParser::ParseOnrestart}},
{"socket",        {3, 6,    &ServiceParser::ParseSocket}},
{"user",           {1, 1,    &ServiceParser::ParseUser}},
{"task_profiles", {1, kMax, &ServiceParser::ParseTaskProfiles}},

ParseLineSection() 先用 GetParserMap().Find(args) 验证关键字和参数范围,再调用对应成员函数。option 的所有权仍在 Service;真正应用到进程要等后续 Service::Start()。

5.3 提交规则 ​

服务段结束时会检查 user、critical/oneshot 组合和重名覆盖。重复 service 若没有 override 会报错;有 override 时还要检查 APEX 可更新属性和 Treble subcontext 边界,随后从 ServiceList 移除旧对象并添加新对象。

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

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 (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_));

6. 导入段 ​

6.1 路径展开 ​

ImportParser::ParseSection() 只接受一个参数,并立即调用 ExpandProps() 展开 ${property}。它保存的是展开后的路径和源行号,而不是马上读取文件。

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

cpp
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();
}
imports_.emplace_back(std::move(*conf_file), line);

6.2 文件结束 ​

import 的真正消费者是 ImportParser::EndFile()。当前文件所有行解析完成后,它移动出 imports 列表,依次调用父 Parser 的 ParseConfig()。如果导入的是目录,目录仍会排序;如果导入文件,文件的 Action/Service 会在当前文件提交后进入全局列表。

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

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

6.3 缺失路径 ​

ParseConfigDir() 打不开目录时返回 false;单文件读取失败则返回错误结果。LoadBootScripts() 对若干分区目录根据返回值记录 late_import_paths,因此一个可选目录缺失不等于整个 init 解析失败。读者排查导入问题时必须区分“路径展开失败”“目录不存在”“文件读取失败”和“文件内容语法错误”。

7. 加载顺序 ​

7.1 启动脚本 ​

Android 17 的 LoadBootScripts() 在没有 ro.boot.init_rc 时依次解析主脚本、system、system_ext、vendor、odm、product 路径;若属性非空,则只解析指定 bootscript。

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

相关函数/类型:LoadBootScripts

cpp
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");
    }
    parser.ParseConfig("/odm/etc/init");
    parser.ParseConfig("/product/etc/init");
} else {
    parser.ParseConfig(bootscript);
}

7.2 顺序实验 ​

init_test.cpp 的 EventTriggerOrderMultipleFiles 构造六个临时输入:主文件、主文件 import、目录中的 a.rc/b.rc、目录文件的 import 和最后 import。测试把每个 on boot 命令编号,断言执行顺序为 1,2,3,4,5,6。这证明的是 parser 的文件/导入排序如何影响已注册 Action 的顺序,不证明设备厂商所有 rc 都具备相同文件集合。

7.3 实验方法 ​

可以不启动完整 Android,直接使用 system/core/init 的 host 测试思路构造最小 rc:一个 import、两个 on boot、一个非法 section。观察 action 计数、错误计数和最终顺序,即可分别验证“提交时机”“import 展开时机”和“错误后恢复”。不要用 shell 的 source 或 bash 解释器替代 init parser,因为两者的引号、反斜杠和 section 语义不同。

8. 诊断边界 ​

8.1 语法错误 ​

看到 Invalid section keyword found,先检查该行是否位于合法 section 之外;看到 Unexpected line found after import statement,说明 import 被当作不允许有 body 的独立段;看到 ignored duplicate definition of service,要检查是否真的需要 override,以及 override 是否跨越 Treble boundary。

8.2 顺序错误 ​

若两个 on boot 的命令顺序与预期不同,按三层定位:目录内文件名排序、主文件 EOF 后的 import 展开、ActionManager 后续事件队列。只看 rc 文件中 import 行的视觉位置,会漏掉 ImportParser::EndFile() 这个提交屏障。

8.3 解析边界 ​

本文证明了 Android 17 通用 parser 的字符、行、段和文件边界;没有证明厂商 subcontext 的全部 SELinux 权限、recovery 专用 parser、APEX 之外的加载时序,也没有把 parser 接受的配置误写成一定会成功执行的命令。读者可以选择一个真实文件,沿“字符 → token → section parser → 对象提交 → 后续消费者”写出五段链路,再用 init_test.cpp 的顺序断言和设备上的 init 日志交叉验证。