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

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


Parser定义

/system/core/init/parser.h

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

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

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

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


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

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

段落命令解析器模板:SectionParser
class SectionParser {
    public:
        virtual ~SectionParser() {}
        virtual Result<Success> ParseSection(std::vector<std::string>&& args,
                                             const std::string& filename, int line) = 0;
        virtual Result<Success> ParseLineSection(std::vector<std::string>&&, int) { return Success(); };
        virtual Result<Success> 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<SectionParser> 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<char> data_copy(data.begin(), data.end());
    data_copy.push_back('\0');
    ...
}
  • data_copy
    存放rc文件字符流的字符容器
    把传入数据复制到名为data_copy的vector<char>容器,并在末尾补充结束字符'\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<std::string> 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分词器

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342

推荐阅读更多精彩内容