IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    [原]Android的init过程(二):初始化语言(init.rc)解析

    nokiaguy发表于 2013-06-17 09:36:09
    love 0

    Android的init过程(一)

    本文使用的软件版本

    Android:4.2.2

    Linux内核:3.1.10

    在上一篇文章中介绍了init的初始化第一阶段,也就是处理各种属性。在本文将会详细分析init最重要的一环:解析init.rc文件。

    init.rc文件并不是普通的配置文件,而是由一种被称为“Android初始化语言”(Android Init Language,这里简称为AIL)的脚本写成的文件。在了解init如何解析init.rc文件之前,先了解AIL非常必要,否则机械地分析init.c及其相关文件的源代码毫无意义。

    为了学习AIL,读者可以到自己Android手机的根目录寻找init.rc文件,最好下载到本地以便查看,如果有编译好的Android源代码,在out/target/product/generic/root目录也可找到init.rc文件。

    AIL由如下4部分组成。

    1. 动作(Actions)

    2. 命令(Commands)

    3. 服务(Services)

    4. 选项(Options)

    这4部分都是面向行的代码,也就是说用回车换行符作为每一条语句的分隔符。而每一行的代码由多个符号(Tokens)表示。可以使用反斜杠转义符在Token中插入空格。双引号可以将多个由空格分隔的Tokens合成一个Tokens。如果一行写不下,可以在行尾加上反斜杠,来连接下一行。也就是说,可以用反斜杠将多行代码连接成一行代码。

    AIL的注释与很多Shell脚本一行,以#开头。

    AIL在编写时需要分成多个部分(Section),而每一部分的开头需要指定Actions或Services。也就是说,每一个Actions或Services确定一个Section。而所有的Commands和Options只能属于最近定义的Section。如果Commands和Options在第一个Section之前被定义,它们将被忽略。

    Actions和Services的名称必须唯一。如果有两个或多个Action或Service拥有同样的名称,那么init在执行它们时将抛出错误,并忽略这些Action和Service。

    下面来看看Actions、Services、Commands和Options分别应如何设置。

    Actions的语法格式如下:

    on 
       
       
       
    

    也就是说Actions是以关键字on开头的,然后跟一个触发器,接下来是若干命令。例如,下面就是一个标准的Action

    on boot
        ifup lo
        hostname localhost
        domainname localdomain
    

    其中boot是触发器,下面三行是command

    那么init.rc到底支持哪些触发器呢?目前init.rc支持如下5类触发器。

    1. boot

    这是init执行后第一个被触发Trigger,也就是在 /init.rc被装载之后执行该Trigger

    2. =

    当属性被设置成时被触发。例如,

    on property:vold.decrypt=trigger_reset_main

    class_reset main

    3. device-added-

    当设备节点被添加时触发

    4. device-removed-

    当设备节点被移除时添加

    5. service-exited-

    会在一个特定的服务退出时触发

    Actions后需要跟若干个命令,这些命令如下:

    1. exec [ ]*

    创建和执行一个程序()。在程序完全执行前,init将会阻塞。由于它不是内置命令,应尽量避免使用exec ,它可能会引起init执行超时。

    2. export

    在全局环境中将 变量的值设为。(这将会被所有在这命令之后运行的进程所继承)

    3. ifup

    启动网络接口

    4. import

    指定要解析的其他配置文件。常被用于当前配置文件的扩展

    5. hostname

    设置主机名

    6. chdir

    改变工作目录

    7. chmod

    改变文件的访问权限

    8. chown

    更改文件的所有者和组

    9. chroot

    改变处理根目录

    10. class_start

    启动所有指定服务类下的未运行服务。

    11 class_stop

    停止指定服务类下的所有已运行的服务。

    12. domainname

    设置域名

    13. insmod

    加载指定的驱动模块

    14. mkdir [mode][owner] [group]

    创建一个目录 ,可以选择性地指定mode、owner以及group。如果没有指定,默认的权限为755,并属于root用户和 root组。

    15. mount

    [ ]*

    试图在目录

    挂载指定的设备。 可以是mtd@name的形式指定一个mtd块设备。包括 "ro"、"rw"、"re

    16. setkey

    保留,暂时未用

    17. setprop

    将系统属性的值设为。

    18. setrlimit

    设置的rlimit (资源限制)

    19. start

    启动指定服务(如果此服务还未运行)。

    20.stop

    停止指定服务(如果此服务在运行中)。

    21. symlink

    创建一个指向的软连接。

    22. sysclktz

    设置系统时钟基准(0代表时钟滴答以格林威治平均时(GMT)为准)

    23. trigger

    触发一个事件。用于Action排队

    24. wait [ ]

    等待一个文件是否存在,当文件存在时立即返回,或到指定的超时时间后返回,如果不指定,默认超时时间是5秒。

    25. write [ ]*

    向指定的文件写入一个或多个字符串。

    Services (服务)是一个程序,他在初始化时启动,并在退出时重启(可选)。Services (服务)的形式如下:

    service   [  ]*
          

    例如,下面是一个标准的Service用法

    service servicemanager /system/bin/servicemanager
        class core
        user system
        group system
        critical
        onrestart restart zygote
        onrestart restart media
        onrestart restart surfaceflinger
        onrestart restart drm
    
    Services的选项是服务的修饰符,可以影响服务如何以及怎样运行。服务支持的选项如下:

    1. critical

    表明这是一个非常重要的服务。如果该服务4分钟内退出大于4次,系统将会重启并进入 Recovery (恢复)模式。

    2. disabled

    表明这个服务不会同与他同trigger (触发器)下的服务自动启动。该服务必须被明确的按名启动。

    3. setenv

    在进程启动时将环境变量设置为。

    4. socket [ [ ] ]

    Create a unix domain socketnamed /dev/socket/ and pass

    its fd to the launchedprocess. must be"dgram", "stream" or "seqpacket".

    User and group default to0.

    创建一个unix域的名为/dev/socket/ 的套接字,并传递它的文件描述符给已启动的进程。 必须是 "dgram","stream" 或"seqpacket"。用户和组默认是0。

    5. user

    在启动这个服务前改变该服务的用户名。此时默认为 root。

    6. group [ ]*

    在启动这个服务前改变该服务的组名。除了(必需的)第一个组名,附加的组名通常被用于设置进程的补充组(通过setgroups函数),档案默认是root。

    7. oneshot

    服务退出时不重启。

    8. class

    指定一个服务类。所有同一类的服务可以同时启动和停止。如果不通过class选项指定一个类,则默认为"default"类服务。

    9. onrestart

    当服务重启,执行一个命令(下详)。

    现在接着分析一下init是如何解析init.rc的。现在打开system/core/init/init.c文件,找到main函数。在上一篇文章中分析了main函数的前一部分(初始化属性、处理内核命令行等),现在找到init_parse_config_file函数,调用代码如下:

    init_parse_config_file("/init.rc");

    这个方法主要负责初始化和分析init.rc文件。init_parse_config_file函数在init_parser.c文件中实现,代码如下:

    int init_parse_config_file(const char *fn)
    {
        char *data;
        data = read_file(fn, 0);
        if (!data) return -1;
        /*  实际分析init.rc文件的代码  */
        parse_config(fn, data);
        DUMP();
        return 0;
    }
    

    init_parse_config_file方法开始调用了read_file函数打开了/init.rc文件,并返回了文件的内容(char*类型),然后最核心的函数是parse_config。该函数也在init_parser.c文件中实现,代码如下:

    static void parse_config(const char *fn, char *s)
    {
        struct parse_state state;
        struct listnode import_list;
        struct listnode *node;
        char *args[INIT_PARSER_MAXARGS];
        int nargs;
    
        nargs = 0;
        state.filename = fn;
        state.line = 0;
        state.ptr = s;
        state.nexttoken = 0;
        state.parse_line = parse_line_no_op;
    
        list_init(&import;_list);
        state.priv = &import;_list;
        /*  开始获取每一个token,然后分析这些token,每一个token就是有空格、字表符和回车符分隔的字符串
       */
        for (;;) {
            /*  next_token函数相当于词法分析器  */
            switch (next_token(&state;)) {
            case T_EOF:  /*  init.rc文件分析完毕  */
                state.parse_line(&state;, 0, 0);
                goto parser_done;
            case T_NEWLINE:  /*  分析每一行的命令  */
                /*  下面的代码相当于语法分析器  */
                state.line++;
                if (nargs) {
                    int kw = lookup_keyword(args[0]);
                    if (kw_is(kw, SECTION)) {
                        state.parse_line(&state;, 0, 0);
                        parse_new_section(&state;, kw, nargs, args);
                    } else {
                        state.parse_line(&state;, nargs, args);
                    }
                    nargs = 0;
                }
                break;
            case T_TEXT:  /*  处理每一个token  */
                if (nargs < INIT_PARSER_MAXARGS) {
                    args[nargs++] = state.text;
                }
                break;
            }
        }
    
    parser_done:
        /*  最后处理由import导入的初始化文件  */
        list_for_each(node, &import;_list) {
             struct import *import = node_to_item(node, struct import, list);
             int ret;
    
             INFO("importing '%s'", import->filename);
             /*  递归调用  */ 
             ret = init_parse_config_file(import->filename);
             if (ret)
                 ERROR("could not import file '%s' from '%s'\n",
                       import->filename, fn);
        }
    }
    

    parse_config方法的代码就比较复杂了,现在先说说该方法的基本处理流程。首先会调用 list_init(&import;_list)初始化一个链表,该链表是用于存储通过import语句导入的初始化文件名。然后开始开始在for循环中分析init.rc文件中的每一行代码。最后将init.rc文件分析完后,就会进入parser_done部分,并递归调用init_parse_config_file方法分析通过import导入的初始化文件。

    通过分析parse_config方法的原理,感觉也并不是很复杂。不过分析parse_config方法的具体代码,还需要点编译原理的知识(只是概念上的就可以)。在for循环中调用了一个next_token方法不断从init.rc文件中获取token。这里的token,就是一种编程语言的最小单元,也就是不可再分。例如,对于传统的编程语言,if、then等关键字、变量名等标识符都属于一个token。而对于init.rc文件来说,import、on、以及触发器的参数值,都属于一个token。

    一个完整的编译器(或解析器)最开始需要进行词法和语法分析,词法分析就是在源代码文件中挑出一个个的Token,也就是说,词法分析器的返回值是Token,而语法分析器的输入就是词法分析器的输出。也就是说,语法分析器需要分析一个个的token,而不是一个个的字符。由于init解析语言很简单,所以就将词法和语法分析器放到了一起。词法分析器就是next_token函数,而语法分析器就是T_NEWLINE分支中的代码。这些就清楚多了。现在先看看next_token函数(在parser.c文件中实现)是如何获取每一个token的。

    int next_token(struct parse_state *state)
    {
        char *x = state->ptr;
        char *s;
    
        if (state->nexttoken) {
            int t = state->nexttoken;
            state->nexttoken = 0;
            return t;
        }
        /*  在这里开始一个字符一个字符地分析  */
        for (;;) {
            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;
                } else {
                    state->ptr = x;
                    return T_EOF;
                }
            default:
                goto text;
            }
        }
    
    textdone:
        state->ptr = x;
        *s = 0;
        return T_TEXT;
    text:
        state->text = s = x;
    textresume:
        for (;;) {
            switch (*x) {
            case 0:
                goto textdone;
            case ' ':
            case '\t':
            case '\r':
                x++;
                goto textdone;
            case '\n':
                state->nexttoken = T_NEWLINE;
                x++;
                goto textdone;
            case '"':
                x++;
                for (;;) {
                    switch (*x) {
                    case 0:
                            /* unterminated quoted thing */
                        state->ptr = x;
                        return T_EOF;
                    case '"':
                        x++;
                        goto textresume;
                    default:
                        *s++ = *x++;
                    }
                }
                break;
            case '\\':
                x++;
                switch (*x) {
                case 0:
                    goto textdone;
                case 'n':
                    *s++ = '\n';
                    break;
                case 'r':
                    *s++ = '\r';
                    break;
                case 't':
                    *s++ = '\t';
                    break;
                case '\\':
                    *s++ = '\\';
                    break;
                case '\r':
                        /* \   -> line continuation */
                    if (x[1] != '\n') {
                        x++;
                        continue;
                    }
                case '\n':
                        /* \  -> line continuation */
                    state->line++;
                    x++;
                        /* eat any extra whitespace */
                    while((*x == ' ') || (*x == '\t')) x++;
                    continue;
                default:
                        /* unknown escape -- just copy */
                    *s++ = *x++;
                }
                continue;
            default:
                *s++ = *x++;
            }
        }
        return T_EOF;
    }
    

    next_token函数的代码还是很多的,不过原理到很简单。就是逐一读取init.rc文件(还有import导入的初始化文件)的字符,并将由空格、“/t”和“/r”分隔的字符串挑出来,并通过state->text返回。如果返回了正常的token,next_token函数就返回T_TEXT。如果一行结束,就返回T_NEWLINE,如果init.rc文件的内容已读取完,就返回T_EOF。当返回T_NEWLINE时,开始语法分析(由于init初始化语言是基于行的,所以语言分析实际上就是分析init.rc文件的每一行,只是这些行已经被分解成一个个token了)。感兴趣的读者可以详细分析一下next_token函数的代码,尽管代码很多,但并不复杂。而且还很有意思。

    现在回到parse_config函数,先看一下T_TEXT分支。该分支将获得的每一行的token都存储在args数组中。现在来看T_NEWLINE分支。该分支的代码涉及到一个state.parse_line函数指针,该函数指针指向的函数负责具体的分析工作。但我们发现,一看是该函数指针指向了一个空函数parse_line_no_op,实际上,一开始该函数指针什么都不做,只是为了使该函数一开始不至于为null,否则调用出错。

    现在来回顾一下T_NEWLINE分支的完整代码。

    case T_NEWLINE:
        state.line++;
        if (nargs) {
            int kw = lookup_keyword(args[0]);
            if (kw_is(kw, SECTION)) {
                state.parse_line(&state;, 0, 0);
                parse_new_section(&state;, kw, nargs, args);
            } else {
                state.parse_line(&state;, nargs, args);
            }
            nargs = 0;
        }
        break;
    
    在上面的代码中首先调用了lookup_keyword方法搜索关键字。该方法的作用是判断当前行是否合法,也就是根据Init初始化语言预定义的关键字查询,如果未查到,返回K_UNKNOWN。lookup_keyword方法在init_parser.c文件中实现,代码如下:

    int lookup_keyword(const char *s)
    {
        switch (*s++) {
        case 'c':
        if (!strcmp(s, "opy")) return K_copy;
            if (!strcmp(s, "apability")) return K_capability;
            if (!strcmp(s, "hdir")) return K_chdir;
            if (!strcmp(s, "hroot")) return K_chroot;
            if (!strcmp(s, "lass")) return K_class;
            if (!strcmp(s, "lass_start")) return K_class_start;
            if (!strcmp(s, "lass_stop")) return K_class_stop;
            if (!strcmp(s, "lass_reset")) return K_class_reset;
            if (!strcmp(s, "onsole")) return K_console;
            if (!strcmp(s, "hown")) return K_chown;
            if (!strcmp(s, "hmod")) return K_chmod;
            if (!strcmp(s, "ritical")) return K_critical;
            break;
        case 'd':
            if (!strcmp(s, "isabled")) return K_disabled;
            if (!strcmp(s, "omainname")) return K_domainname;
            break;
         … …
        case 'o':
            if (!strcmp(s, "n")) return K_on;
            if (!strcmp(s, "neshot")) return K_oneshot;
            if (!strcmp(s, "nrestart")) return K_onrestart;
            break;
        case 'r':
            if (!strcmp(s, "estart")) return K_restart;
            if (!strcmp(s, "estorecon")) return K_restorecon;
            if (!strcmp(s, "mdir")) return K_rmdir;
            if (!strcmp(s, "m")) return K_rm;
            break;
        case 's':
            if (!strcmp(s, "eclabel")) return K_seclabel;
            if (!strcmp(s, "ervice")) return K_service;
            if (!strcmp(s, "etcon")) return K_setcon;
            if (!strcmp(s, "etenforce")) return K_setenforce;
            if (!strcmp(s, "etenv")) return K_setenv;
            if (!strcmp(s, "etkey")) return K_setkey;
            if (!strcmp(s, "etprop")) return K_setprop;
            if (!strcmp(s, "etrlimit")) return K_setrlimit;
            if (!strcmp(s, "etsebool")) return K_setsebool;
            if (!strcmp(s, "ocket")) return K_socket;
            if (!strcmp(s, "tart")) return K_start;
            if (!strcmp(s, "top")) return K_stop;
            if (!strcmp(s, "ymlink")) return K_symlink;
            if (!strcmp(s, "ysclktz")) return K_sysclktz;
            break;
        case 't':
            if (!strcmp(s, "rigger")) return K_trigger;
            break;
        case 'u':
            if (!strcmp(s, "ser")) return K_user;
            break;
        case 'w':
            if (!strcmp(s, "rite")) return K_write;
            if (!strcmp(s, "ait")) return K_wait;
            break;
        }
        return K_UNKNOWN;
    }
    

    lookup_keyword方法按26个字母顺序(关键字首字母)进行处理。

    现在回到parse_config方法的T_NEWLIEN分支,接下来调用了kw_is宏具体判断当前行是否合法,该宏以及SECTION宏的定义如下。根据这些代码。明显是keyword_info数组中的某个元素的flags成员变量的值取最后一位。

    #define SECTION 0x01
    #define kw_is(kw, type) (keyword_info[kw].flags & (type))
    

    现在问题又转到keyword_info数组了。该数组也在init_parser.c文件中定义,代码如下:

    #include "keywords.h"
    #define KEYWORD(symbol, flags, nargs, func) \
        [ K_##symbol ] = { #symbol, func, nargs + 1, flags, },
    struct {
        const char *name;
        int (*func)(int nargs, char **args);
        unsigned char nargs;
        unsigned char flags;
    } keyword_info[KEYWORD_COUNT] = {
        [ K_UNKNOWN ] = { "unknown", 0, 0, 0 },
    #include "keywords.h"
    };
    

    从表面上看,keyword_info数组是一个struct数组,但本质上,是一个map。为每一个数组元素设置了一个key,例如,数组元素{ "unknown", 0, 0,0 }的key是K_UNKNOWN,而#include “keywords.h”大有玄机。上面的代码中引用了两次keywords.h文件,现在可以看一下keywords.h文件的代码。

    #ifndef KEYWORD
    int do_chroot(int nargs, char **args);
    … …
    int do_export(int nargs, char **args);
    int do_hostname(int nargs, char **args);
    int do_rmdir(int nargs, char **args);
    int do_loglevel(int nargs, char **args);
    int do_load_persist_props(int nargs, char **args);
    int do_wait(int nargs, char **args);
    #define __MAKE_KEYWORD_ENUM__
    /*
    "K_chdir", ENUM
    */
    #define KEYWORD(symbol, flags, nargs, func) K_##symbol,
    enum {
        K_UNKNOWN,
    #endif
        KEYWORD(capability,  OPTION,  0, 0)
        KEYWORD(chdir,       COMMAND, 1, do_chdir)
        KEYWORD(chroot,      COMMAND, 1, do_chroot)
        KEYWORD(class,       OPTION,  0, 0)
        KEYWORD(class_start, COMMAND, 1, do_class_start)
        KEYWORD(class_stop,  COMMAND, 1, do_class_stop)
        KEYWORD(class_reset, COMMAND, 1, do_class_reset)
        KEYWORD(console,     OPTION,  0, 0)
        … …
        KEYWORD(critical,    OPTION,  0, 0)
        KEYWORD(load_persist_props,    COMMAND, 0, do_load_persist_props)
        KEYWORD(ioprio,      OPTION,  0, 0)
    #ifdef __MAKE_KEYWORD_ENUM__
        KEYWORD_COUNT,
    };
    #undef __MAKE_KEYWORD_ENUM__
    #undef KEYWORD
    #endif
    

    从keywords.h文件的代码可以看出,如果未定义KEYWORD宏,则在keywords.h文件中定义一个KEYWORD宏,以及一个枚举类型,其中K_##symbol的##表示连接的意思。而这个KEYWORD宏只用了第一个参数(symbol)。例如,KEYWORD(chdir, COMMAND, 1, do_chdir)就会生成K_chdir。

    而在keyword_info结构体数组中再次导入keywords.h文件,这是KEYWORD宏已经在init_parser.c文件中重新定义,所以第一次导入keywords.h文件使用的是如下的宏。

    #define KEYWORD(symbol, flags, nargs, func) \
        [ K_##symbol ] = { #symbol, func, nargs + 1, flags, },
    
    这下就明白了,如果不使用keywords.h文件,直接将所有的代码都写到init_parser.c文件中,就会有下面的代码。

    int do_chroot(int nargs, char **args);
    … …
    enum
    {
    K_UNKNOWN,
    K_ capability,
    K_ chdir,
    … …
    }
    #define KEYWORD(symbol, flags, nargs, func) \
        [ K_##symbol ] = { #symbol, func, nargs + 1, flags, },
    struct {
        const char *name;
        int (*func)(int nargs, char **args);
        unsigned char nargs;
        unsigned char flags;
    } keyword_info[KEYWORD_COUNT] = {
        [ K_UNKNOWN ] = { "unknown", 0, 0, 0 },
        [K_ capability] = {" capability ", 0, 1, OPTION },
        [K_ chdir] = {"chdir", do_chdir ,2, COMMAND},
        … …
    #include "keywords.h"
    };
    
    可能我们还记着lookup_keyword方法,该方法的返回值就是keyword_info数组的key。

    在keywords.h前面定义的函数指针都是处理init.rc文件中service、action和command的。现在就剩下一个问题了,在哪里为这些函数指针赋值呢,也就是说,具体处理每个部分的函数在哪里呢。现在回到前面的语法分析部分。如果当前行合法,则会执行parse_new_section函数(在init_parser.c文件中实现),该函数将为section和action设置处理这两部分的函数。parse_new_section函数的代码如下:

    void parse_new_section(struct parse_state *state, int kw,
                           int nargs, char **args)
    {
        printf("[ %s %s ]\n", args[0],
               nargs > 1 ? args[1] : "");
        switch(kw) {
        case K_service:  //  处理service
            state->context = parse_service(state, nargs, args);
            if (state->context) {
                state->parse_line = parse_line_service;
                return;
            }
            break;
        case K_on:  //  处理action
            state->context = parse_action(state, nargs, args);
            if (state->context) {
                state->parse_line = parse_line_action;
                return;
            }
            break;
        case K_import:   //  单独处理import导入的初始化文件。
            parse_import(state, nargs, args);
            break;
        }
        state->parse_line = parse_line_no_op;
    }
    

    现在看一下处理service的函数(parse_line_service)。

    static void parse_line_service(struct parse_state *state, int nargs, char **args)
    {
        struct service *svc = state->context;
        struct command *cmd;
        int i, kw, kw_nargs;
    
        if (nargs == 0) {
            return;
        }
    
        svc->ioprio_class = IoSchedClass_NONE;
    
        kw = lookup_keyword(args[0]);
        //  下面处理每一个option
        switch (kw) {
        case K_capability:
            break;
        … …
        case K_group:
            if (nargs < 2) {
                parse_error(state, "group option requires a group id\n");
            } else if (nargs > NR_SVC_SUPP_GIDS + 2) {
                parse_error(state, "group option accepts at most %d supp. groups\n",
                            NR_SVC_SUPP_GIDS);
            } else {
                int n;
                svc->gid = decode_uid(args[1]);
                for (n = 2; n < nargs; n++) {
                    svc->supp_gids[n-2] = decode_uid(args[n]);
                }
                svc->nr_supp_gids = n - 2;
            }
            break;
        case K_keycodes:
            if (nargs < 2) {
                parse_error(state, "keycodes option requires atleast one keycode\n");
            } else {
                svc->keycodes = malloc((nargs - 1) * sizeof(svc->keycodes[0]));
                if (!svc->keycodes) {
                    parse_error(state, "could not allocate keycodes\n");
                } else {
                    svc->nkeycodes = nargs - 1;
                    for (i = 1; i < nargs; i++) {
                        svc->keycodes[i - 1] = atoi(args[i]);
                    }
                }
            }
            break;
            … …
         }
        ……
    }
    

    Action的处理方式与service类似,读者可以自行查看相应的函数代码。现在一切都清楚了。处理service的函数是parse_line_service,处理action的函数是parse_line_action。而前面的state.parse_line根据当前是service还是action,指向这两个处理函数中的一个,并执行相应的函数处理actioncommand和serviceoption。

    综合上述,实际上分析init.rc文件的过程就是通过一系列地处理,最终转换为通过parse_line_service或parse_line_action函数分析Init.rc文件中每一行的行为。



沪ICP备19023445号-2号
友情链接