安卓启动流程(二) - Parser解析器

Parser是rc文件解析成执行逻辑的核心工具。
内部通过tokenizer分词器对rc文件的字符流进行解析,转换成单词(参数)和对应的token令牌。根据token令牌,派分到不同的解析器实现进行的处理。
通过模板模式,Parser将实际的解析逻辑解耦到解析器模板的实现类中进行。


Parser定义

/system/core/init/parser.h

class Parser {
    public:
        using LineCallback = std::function(std::vector&&)>;
        Parser();
        bool ParseConfig(const std::string& path);
        void AddSectionParser(const std::string& name, std::unique_ptr parser);
        void AddSingleLineParser(const std::string& prefix, LineCallback callback);

    private:
        std::map> section_parsers_;
        std::vector> line_callbacks_;
};
  • bool ParseConfig(const std::string& path);
    解析指定的rc文件
    path可以是rc文件路径,也可以是一个目录路径。

  • void AddSectionParser(const std::string& name, std::unique_ptr parser);
    添加 SectionParser 解析器
    添加段落解析器,name为一行命令中,首个参数的完全匹配值。

  • void AddSingleLineParser(const std::string& prefix, LineCallback callback);
    添加 LineCallback 解析器
    添加单行命令解析器。prefix为一行命令中,首个参数的前序匹配值。


单行命令解析器模板:LineCallback
class Parser {
    public:
        using LineCallback = std::function(std::vector&&)>;
}

本质是一个函数。
解析过程中,对一行命令的首个参数进行匹配,当匹配成功时,马上调用对应的单行命令解析器(即回调函数)。
从源码上看,只有在ueventd进程解析ueventd.rc时,才有该类型解析器的应用。

段落命令解析器模板:SectionParser
class SectionParser {
    public:
        virtual ~SectionParser() {}
        virtual Result ParseSection(std::vector&& args,
                                             const std::string& filename, int line) = 0;
        virtual Result ParseLineSection(std::vector&&, int) { return Success(); };
        virtual Result EndSection() { return Success(); };
        virtual void EndFile(){};
};

解析过程中,对一行命令的首个参数进行匹配,当匹配成功时,即认为该行时一个命令段落的开始,并使用匹配到的解析器解析段落中的命令内容。

主要定义了几个函数:

  • ParseSection()
    处理段落首行命令
    行命令处理时,匹配到段落解析器的关键字,则视为一个段落的首行,段落的后续命令交由该段落解析器处理,并调用该解析器的ParseSection()函数。
  • ParseLineSection()
    处理段落内容命令
    对匹配不到关键字的行命令,视为段落的内容,假如存在解析中的段落解析器,则调用该解析器的ParseLineSection()函数。

  • EndSection()
    处理段落结束
    段落结束时,假如存在解析中的段落解析器,则调用该解析器的EndSection()函数。

  • EndFile()
    处理文件结束
    文件解析结束时,调用所有已添加解析器中的EndFile()函数。


Parser实现

  • AddSectionParser / AddSingleLineParser

    void Parser::AddSectionParser(const std::string& name,   std::unique_ptr parser) {
        section_parsers_[name] = std::move(parser);
    }
    
    void Parser::AddSingleLineParser(const std::string& prefix, LineCallback callback) {
        line_callbacks_.emplace_back(prefix, callback);
    }
    

    把关键字和解析器添加到对应的容器中。

  • ParseConfig

    Parser::ParseConfig(const std::string& path) {
        size_t parse_errors;
        return ParseConfig(path, &parse_errors);
    }
    
    bool Parser::ParseConfig(const std::string& path, size_t* parse_errors) {
        *parse_errors = 0;
        if (is_dir(path.c_str())) {
            return ParseConfigDir(path, parse_errors);
        }
        return ParseConfigFile(path, parse_errors);
    }
    

    判断path是否文件夹,选择直接解析文件,还是解析目录下的所有rc文件。
    ParseConfigDir()的内部遍历出的文件,最终同样调用ParseConfigFile()进行解析。

  • ParseConfigFile

      bool Parser::ParseConfigFile(const std::string& path, size_t* parse_errors) {
        auto config_contents = ReadFile(path);
        if (!config_contents) {
            return false;
        }
        config_contents->push_back('\n'); 
        ParseData(path, *config_contents, parse_errors);
        for (const auto& [section_name, section_parser] : section_parsers_) {
            section_parser->EndFile();
        }
        return true;
    }
    

    通过ReadFile()函数,将文件数据读取为字符串数据。然后通过ParseData()函数执行解析和处理。
    文件解析完成时,调用所有添加的解析器的EndFile()函数。


ParseData() 核心解析函数

Parse解析器的核心解析函数。
通过tokenizer分词器解析字符串获取token令牌,根据token令牌的类型,执行处理逻辑,或派分事件到具体解析器实现进行处理。

解析过程相关变量:
void Parser::ParseData(const std::string& filename, const std::string& data, size_t* parse_errors) {
    std::vector data_copy(data.begin(), data.end());
    data_copy.push_back('\0');
    ...
}
  • data_copy
    存放rc文件字符流的字符容器
    把传入数据复制到名为data_copy的vector容器,并在末尾补充结束字符'\0'(NULL字符)。
struct parse_state
{
    char *ptr;        // 当前解析中字符的指针,即解析进度
    char *text;       // 当前检出的单词,即参数
    int line;         // 当前解析中的行号
    int nexttoken;    // 令牌缓存,用于性能优化
};

ParseData() {
    ...
    parse_state state;
    state.line = 0;
    state.ptr = &data_copy[0];
    state.nexttoken = 0;
    ...
}
  • state
    解析数据结构体
    创建parse_state结构体state并初始化。用于存放 解析过程的状态 和 产生的临时数据。
ParseData() {
    ...
    SectionParser* section_parser = nullptr;
    int section_start_line = -1;
    std::vector args;
    ...
}
  • section_parser
    段落目标解析器
    当解析到新行时,假如新行首个参数命中解析器关键字 (即段落的首行),则把命中的解析器设置为段落目标解析器 (即当前段落的解析器)
    没有命中解析器关键字时,则尝试使用段落目标解析器对行进行解析(即解析段落非首行数据,即命令数据)

  • section_start_line
    当前段落开始行号
    如果当前解析的行,是一个段落的首行,则记录该行行号。

  • args
    当前行解析出的参数链表
    参数链表,存放一行命令解析出的所有参数。

ParseData() {
    ...
    auto end_section = [&] {
        if (section_parser == nullptr) return;
            if (auto result = section_parser->EndSection(); !result) {
                (*parse_errors)++;
            }
        section_parser = nullptr;
        section_start_line = -1;
    };
    ...
}
  • end_section
    Lambda表达式,用于结束段落解析和重置解析相关数据
    假如当前段落目标解析器不为空,则调用其EndSection()函数,通知解析器该段落解析结束,并重置段落目标解析器的指针section_parser和当前段落开始行号section_start_line
解析过程逻辑阶段:

tokenizer分词器的实现分析:安卓启动流程(三) - tokenizer分词器

ParseData() {
    ...
    for (;;) {
        switch (next_token(&state)) {
            case T_EOF: ...
            case T_NEWLINE: ...
            case T_TEXT: ...
        }
    }
    ...
}

死循环,通过next_token(&state)函数获取token令牌和检出单词参数。

  • case T_TEXT

    case T_TEXT:
        args.emplace_back(state.text);
        break;
    

    检出了一个单词
    args插入单词,state.text指针指向单词的首字符。

  • case T_EOF

    case T_EOF:
        end_section();
        return;
    

    表示rc文本解析结束
    调用end_section表达式,通知当前段落目标解析器该段落解析结束,并退出ParseData函数。

  • case T_NEWLINE

    case T_NEWLINE:
        // 当前解析的行号 +1
        state.line++;
    
        // 检出的参数为空,不执行处理
        if (args.empty()) break;
    
        // 该行是否匹配到单行命令解析器
        // 假如匹配到单行命令解析器,尝试通过end_section函数结束当前段落解析, 
        // 然后调用对应的单行命令解析器
        for (const auto& [prefix, callback] : line_callbacks_) {
            if (android::base::StartsWith(args[0], prefix)) {
                // 尝试通过end_section函数结束当前段落解析
                end_section();
                // 调用对应的单行命令解析器
                if (auto result = callback(std::move(args)); !result) {
                    (*parse_errors)++;
                }
                break;
            }
        }
        
        // 该行是否匹配到段落解析器
        if (section_parsers_.count(args[0])) {
            // 当匹配到解析器时, 尝试通过end_section函数结束上一个段落解析
            end_section();
            // 记录当前解析器
            section_parser = section_parsers_[args[0]].get();
            // 记录段落行号
            section_start_line = state.line;
            // 调用解析器的ParseSection函数
            if (auto result = section_parser->ParseSection(std::move(args), filename, state.line); !result) {
                (*parse_errors)++;
                section_parser = nullptr;
            }
        } else if (section_parser) {
            // 假如存在段落目标解析器,则调用解析器的ParseLineSection函数
            if (auto result = section_parser->ParseLineSection(std::move(args), state.line); !result) {
                (*parse_errors)++;
            }
        }
    
        // 清空参数链表
        args.clear();
        break;
    }
    

    检测到换行,该行结束,处理行命令
    该行参数已解析完成,在解析下一行前,处理该行的命令。


大致流程总结

  1. 使用ParseData()对路径参数path下的所有rc文件,进行解析。
  2. ParseData()中命令的单位为每一行的文本。一行命令中检出的所有参数都会放入args链表,在下一行解析开始前,处理命令,并在命令处理完成时清空args链表。
  3. 命令处理开始时,先进行解析器匹配。当args链表数量大于0时,进入匹配阶段,首先匹配单行命令解析器,然后匹配段落命令解析器。
  4. 匹配阶段无法匹配到解析器时,则该行命令视为段落命令,进入段落命令处理阶段,尝试使用已设置为处理中的段落解析器进行处理,无法找到解析器,则跳过该行命令的处理。

下一篇:安卓启动流程(三) - tokenizer分词器

你可能感兴趣的:(安卓启动流程(二) - Parser解析器)