转载注明出处

原文链接:http://blog.csdn.net/nokiaguy/article/details/9109491

简介init.rc

init.rc文件并不是普通的配置文件,而是由一种被称为“Android初始化语言”(Android Init Language,AIL)编写的脚本文件。在了解init如何解析init.rc文件之前,先了解AIL非常必要,否则机械地分析init.rc及其相关文件的源代码也毫无意义。 init.rc可以在编译好的源代码里找到,目录为<Android源代码根目录>out/target/product/<编译的产品name>/root目录下。 AIL由以下4部分组成:
  1. 动作(Actions)
  2. 命令(Commands)
  3. 服务(Services)
  4. 选项(Options)
这四部分都是面向行的代码,也就是说用回车换行符作为每一条语句的分隔符。而每一行的代码由多个符号(Tokens)表示。可以使用反斜杠转义符在Token中插入空格。双引号可以将多个由空格分隔的Tokens合成一个Tokens。如果一行写不下,可以在行尾加上反斜杠,来连接下一行。也就是说,可以用反斜杠将多行代码连接成一行代码。 AIL的注释与很多Shell脚本一行,以#开头。 AIL在编写时需要分为多个部分(Section),而每一部分的开头需要指定Actions或Services。也就是说,每一个Actions或Services确定一个Section。而所有的Commands和Options只能属于最近定义的Section。而所有的Commands和Options只能属于最近定义的Section。如果Commands和Options在第一个Section之前被定义,它们将被忽略。 Actions和Services的名称必须唯一。如果有两个或多个Action或Service拥有同样的名称,那么init在执行它们的时候将抛出错误,并忽略这些Action和Service。 下面来看看Actions、Services、Commands和Options分别应如何设置。

Actions


Actions的语法格式如下:
on <trigger>    <command>    <command>    <command>
也就是说Actions是以关键字on开头的,然后跟一个触发器,接下来是若干命令。例如,下面就是一个标准的Action
on early-init    # Set init and its forked children's oom_adj.    write /proc/1/oom_adj -16    # Set the security context for the init process.    # This should occur before anything else (e.g. ueventd) is started.    setcon u:r:init:s0    start ueventd# create mountpoints    mkdir /mnt 0775 root system
其中early-init是触发器,下面几行是command。那么init.rc到底支持哪些触发器呢?

init.rc支持各种触发器

这里列出init.rc支持的常见的5种触发器: 1. boot
这是init执行后第一个被触发Trigger,也就是在 /init.rc被装载之后执行该Trigger
2. <name>=<value>
当属性<name>被设置成<value>时被触发。例如,
on property:vold.decrypt=trigger_reset_main
3. device-added-<path>
当设备节点被添加时触发
4. device-removed-<path>
当设备节点被移除时添加
5. service-exited-<name>
会在一个特定的服务退出时触发

Actions 命令

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

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

3. ifup <interface>
启动网络接口

4. import <filename>
指定要解析的其他配置文件。常被用于当前配置文件的扩展

5. hostname <name>
设置主机名

6. chdir <directory>
改变工作目录

7. chmod <octal-mode><path>
改变文件的访问权限

8. chown <owner><group> <path>
更改文件的所有者和组

9. chroot <directory>
改变处理根目录

10. class_start<serviceclass>
启动所有指定服务类下的未运行服务。

11 class_stop<serviceclass>
停止指定服务类下的所有已运行的服务。

12. domainname <name>
设置域名

13. insmod <path>
加载<path>指定的驱动模块

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

15. mount <type> <device> <dir> [<mountoption> ]*
试图在目录<dir>挂载指定的设备。<device> 可以是mtd@name的形式指定一个mtd块设备。<mountoption>包括 "ro"、"rw"、"re

16. setkey
保留,暂时未用

17. setprop <name><value>
将系统属性<name>的值设为<value>。

18. setrlimit <resource> <cur> <max>
设置<resource>的rlimit (资源限制)

19. start <service>
启动指定服务(如果此服务还未运行)。

20.stop<service>
停止指定服务(如果此服务在运行中)。

21. symlink <target> <path> 建立一个path指向target的符号链接。
22. sysclktz <mins_west_of_gmt>
设置系统时钟基准(0代表时钟滴答以格林威治平均时(GMT)为准)

23. trigger <event>
触发一个事件。用于Action排队

24. wait <path> [<timeout> ]
等待一个文件是否存在,当文件存在时立即返回,或到<timeout>指定的超时时间后返回,如果不指定<timeout>,默认超时时间是5秒。

25. write <path> <string> [ <string> ]*
向<path>指定的文件写入一个或多个字符串。

Services

Services的选项是服务的修饰符,可以影响服务如何以及怎样运行。服务支持的选项如下:
1. critical
表明这是一个非常重要的服务。如果该服务4分钟内退出大于4次,系统将会重启并进入 Recovery (恢复)模式。

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

3. setenv <name><value>
在进程启动时将环境变量<name>设置为<value>。

4. socket <name><type> <perm> [ <user> [ <group> ] ]
Create a unix domain socketnamed /dev/socket/<name> and pass
its fd to the launchedprocess. <type> must be"dgram", "stream" or "seqpacket".
User and group default to0.
创建一个unix域的名为/dev/socket/<name> 的套接字,并传递它的文件描述符给已启动的进程。<type> 必须是 "dgram","stream" 或"seqpacket"。用户和组默认是0。

5. user <username>
在启动这个服务前改变该服务的用户名。此时默认为 root。

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

7. oneshot
服务退出时不重启。

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

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

源码分析

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函数(在praser.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':                    /* \ <cr> <lf> -> line continuation */                if (x[1] != '\n') {                    x++;                    continue;                }            case '\n':                    /* \ <lf> -> 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。
现在回到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_NEWLINE分支,接下来调用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。例如,数组元素{”unknown“, 0, 0, 0}的key是K_UNKNOWN,而#include ”keywords.h“大有玄机。上面的代码中引用了两次keywords.h文件,现在可以看一下keywords.h文件的代码。
#ifndef KEYWORDint 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]);    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;        … …     }    ……}

ction的处理方式与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文件中每一行的行为。

更多相关文章

  1. Android(安卓)WebView的loadData方法注意事项
  2. Android问题之res/raw和assets文件大小限制
  3. android图书管理系统+javaweb后台服务器代码
  4. android实现手机截屏并保存截图功能
  5. Android(安卓)Studio打包签名全过程
  6. UBI文件系统
  7. Android(安卓)NFS 文件系统
  8. 使用drawable资源
  9. android读取原始的xml文件

随机推荐

  1. Linux socket通信——并发服务器(fork)
  2. ubuntu下的Samba配置:使每个用户可以用自
  3. linux c (4) 进程终止-exit和_exit函数
  4. linux上/proc/cpuinfo中的bug是什么意思?
  5. 导出内存(linux中)
  6. 詹金斯死了,但是pid文件存在
  7. Linux BT下载(3)-与Tracker交互
  8. 【linux】下的mkfifo 命令 和【C语言】中
  9. Linux下的文件时间
  10. 第五章 个人优化vim第二步: 开启自带辅助