本文使用的软件版本
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源代码,在
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
创建和执行一个程序(
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
创建一个目录
15. mount
试图在目录
16. setkey
保留,暂时未用
17. setprop
将系统属性
18. setrlimit
设置
19. start
启动指定服务(如果此服务还未运行)。
20.stop
停止指定服务(如果此服务在运行中)。
21. symlink
创建一个指向
22. sysclktz
设置系统时钟基准(0代表时钟滴答以格林威治平均时(GMT)为准)
23. trigger
触发一个事件。用于Action排队
24. wait
等待一个文件是否存在,当文件存在时立即返回,或到
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 drmServices的选项是服务的修饰符,可以影响服务如何以及怎样运行。服务支持的选项如下:
1. critical
表明这是一个非常重要的服务。如果该服务4分钟内退出大于4次,系统将会重启并进入 Recovery (恢复)模式。
2. disabled
表明这个服务不会同与他同trigger (触发器)下的服务自动启动。该服务必须被明确的按名启动。
3. setenv
在进程启动时将环境变量
4. socket
Create a unix domain socketnamed /dev/socket/
its fd to the launchedprocess.
User and group default to0.
创建一个unix域的名为/dev/socket/
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文件中每一行的行为。