init进程解析rc文件的相关函数分析
2015-10-20 17:31 cascle 阅读(540) 评论(0) 编辑 收藏 举报init进程的源码文件位于system/core/init,其中解析rc文件语法的代码放在五个函数中, init_parse_config_file (init_parser.c), read_file (util.c), parse_config (init_parser.c), next_token (parser.c), lookup_keyword (init_parser.c)。下面一个一个看这几个函数的具体实现。
首先是 init_parser_config_file 函数在init的main函数中被调用
1 init_parse_config_file("/init.rc");
可以看到直接解析init.rc文件,其会import其他rc文件
函数内容如下
1 int init_parse_config_file(const char *fn) 2 { 3 char *data; 4 data = read_file(fn, 0); 5 if (!data) return -1; 6 7 parse_config(fn, data); 8 DUMP(); 9 return 0; 10 }
可以看到非常简单,先把rc文件读到内存里,再调用 parse_config 函数解析读到内存中的文件内容,最后把接触出来的数据结构dump出来。
先看读取文件内容部分
1 /* reads a file, making sure it is terminated with \n \0 */ 2 void *read_file(const char *fn, unsigned *_sz) 3 { 4 char *data; 5 int sz; 6 int fd; 7 struct stat sb; 8 9 data = 0; 10 fd = open(fn, O_RDONLY); 11 if(fd < 0) return 0; 12 13 // for security reasons, disallow world-writable 14 // or group-writable files 15 if (fstat(fd, &sb) < 0) { 16 ERROR("fstat failed for '%s'\n", fn); 17 goto oops; 18 } 19 if ((sb.st_mode & (S_IWGRP | S_IWOTH)) != 0) { 20 ERROR("skipping insecure file '%s'\n", fn); 21 goto oops; 22 } 23 24 sz = lseek(fd, 0, SEEK_END); 25 if(sz < 0) goto oops; 26 27 if(lseek(fd, 0, SEEK_SET) != 0) goto oops; 28 29 data = (char*) malloc(sz + 2); 30 if(data == 0) goto oops; 31 32 if(read(fd, data, sz) != sz) goto oops; 33 close(fd); 34 data[sz] = '\n'; 35 data[sz+1] = 0; 36 if(_sz) *_sz = sz; 37 return data; 38 39 oops: 40 close(fd); 41 if(data != 0) free(data); 42 return 0; 43 }
内容很简单,要注意两点,一是权限问题,对于可以对其他用户随意写(S_IWOTH)以及组内可写的文件,不会读取其内容;二是读出来的数据在比文件附加了两个字节,一个是换行 '\n' ,一个是NULL '\0' ,以便随后的解析中可以判断文件结束——EOF。
把文件内容读到内存之后就可以用函数 parse_config 解析了,其内容如下
1 static void parse_config(const char *fn, char *s) 2 { 3 struct parse_state state; 4 struct listnode import_list; 5 struct listnode *node; 6 char *args[INIT_PARSER_MAXARGS]; 7 int nargs; 8 9 #ifdef MTK_INIT 10 NOTICE("<<Parsing %s>>\n", fn); 11 #endif 12 13 nargs = 0; 14 state.filename = fn; 15 state.line = 0; 16 state.ptr = s; 17 state.nexttoken = 0; 18 state.parse_line = parse_line_no_op; 19 20 list_init(&import_list); 21 state.priv = &import_list; 22 23 for (;;) { 24 switch (next_token(&state)) { 25 case T_EOF: 26 state.parse_line(&state, 0, 0); 27 goto parser_done; 28 case T_NEWLINE: 29 state.line++; 30 if (nargs) { 31 int kw = lookup_keyword(args[0]); 32 if (kw_is(kw, SECTION)) { 33 state.parse_line(&state, 0, 0); 34 parse_new_section(&state, kw, nargs, args); 35 } else { 36 state.parse_line(&state, nargs, args); 37 } 38 nargs = 0; 39 } 40 break; 41 case T_TEXT: 42 if (nargs < INIT_PARSER_MAXARGS) { 43 args[nargs++] = state.text; 44 } 45 break; 46 } 47 } 48 49 parser_done: 50 list_for_each(node, &import_list) { 51 struct import *import = node_to_item(node, struct import, list); 52 int ret; 53 54 INFO("importing '%s'", import->filename); 55 ret = init_parse_config_file(import->filename); 56 if (ret) 57 ERROR("could not import file '%s' from '%s'\n", 58 import->filename, fn); 59 } 60 }
做语法解析一般分两步,一是进行词法分析,二是进行语法分析。词法分析会把一个一个字符聚合成词法单元,比如if,for等,这里是on,service等。随后将一组按语法规则分割好的词法单元送给语法分析部分。这里做词法分析的是 next_token 函数,根据返回值决定解析出的词法单元性质。rc文件是以一行为单位分割词法单元,所以解析到新行( '\n' )词法单元后,之前已经聚合好的词法单元会被解析语义,即 case T_NEWLINE 内的部分,生成内部相依的数据结构,比如action,service等。在此之前会把收集到的一个个词法单元放进 args字符数组里。若是检测到文件结束,则该文件解析结束,进入 parse_done标签部分,递归解析被import进来的其他rc文件。
记录解析状态的结构体是 parse_state ,其定义与parser.h文件中,内容如下
1 struct parse_state 2 { 3 char *ptr; 4 char *text; 5 int line; 6 int nexttoken; 7 void *context; 8 void (*parse_line)(struct parse_state *state, int nargs, char **args); 9 const char *filename; 10 void *priv; 11 };
其中, ptr 为内容buffer的游标,记录解析到哪里了; text 为解析出来的词法单元; line 为解析到的行数; nexttoken 记录语法解析器下一个要处理的词法单元的性质; context 存放接触出来后映射到的具体内容,比如service,action等; parse_line 为用来进行单行解析的函数; filename 为解析的rc文件的名称; priv 为import进来的rc文件的 listnode 结构体。
记录解析到的词法单元的性质的三个宏也定义与parser.h文件中,如下
1 #define T_EOF 0 2 #define T_TEXT 1 3 #define T_NEWLINE 2
语法分析部分会根据返回的词法单元的性质,做不同的动作,或者解析结束,或者继续聚合词法单元,或者进行语法解析,映射相依内容到内部表示的数据结构。
所以再看 parse_config 函数,先初始化 parse_state 相关的字段, nexttoken 为0,其值代表需要当前正在进行解析的词法单元要继续解析,没遇到换行符。这个字段的唯一一种应用就是在解析出换行符后,设置其为 T_NEWLINE ,词法扫描器 next_token 的返回值为 T_TEXT ,这样再聚合完最后一个词法单元后,再次进入 next_token 函数之后就可以直接返回 T_NEWLINE ,进入解析单行的函数中去。
随后进入死循环,利用词法扫描器扫描rc文件内容,扫到词法单元进行聚合,扫到换行符解析聚合好的词法单元,扫到文件结尾结束解析当前rc文件,继续递归解析被import进来的rc文件。
下面看看词法扫描器 next_token ,其内容如下
1 int next_token(struct parse_state *state) 2 { 3 char *x = state->ptr; 4 char *s; 5 6 if (state->nexttoken) { 7 int t = state->nexttoken; 8 state->nexttoken = 0; 9 return t; 10 } 11 12 for (;;) { 13 switch (*x) { 14 case 0: 15 state->ptr = x; 16 return T_EOF; 17 case '\n': 18 x++; 19 state->ptr = x; 20 return T_NEWLINE; 21 case ' ': 22 case '\t': 23 case '\r': 24 x++; 25 continue; 26 case '#': 27 while (*x && (*x != '\n')) x++; 28 if (*x == '\n') { 29 state->ptr = x+1; 30 return T_NEWLINE; 31 } else { 32 state->ptr = x; 33 return T_EOF; 34 } 35 default: 36 goto text; 37 } 38 } 39 40 textdone: 41 state->ptr = x; 42 *s = 0; 43 return T_TEXT; 44 text: 45 state->text = s = x; 46 textresume: 47 for (;;) { 48 switch (*x) { 49 case 0: 50 goto textdone; 51 case ' ': 52 case '\t': 53 case '\r': 54 x++; 55 goto textdone; 56 case '\n': 57 state->nexttoken = T_NEWLINE; 58 x++; 59 goto textdone; 60 case '"': 61 x++; 62 for (;;) { 63 switch (*x) { 64 case 0: 65 /* unterminated quoted thing */ 66 state->ptr = x; 67 return T_EOF; 68 case '"': 69 x++; 70 goto textresume; 71 default: 72 *s++ = *x++; 73 } 74 } 75 break; 76 case '\\': 77 x++; 78 switch (*x) { 79 case 0: 80 goto textdone; 81 case 'n': 82 *s++ = '\n'; 83 break; 84 case 'r': 85 *s++ = '\r'; 86 break; 87 case 't': 88 *s++ = '\t'; 89 break; 90 case '\\': 91 *s++ = '\\'; 92 break; 93 case '\r': 94 /* \ <cr> <lf> -> line continuation */ 95 if (x[1] != '\n') { 96 x++; 97 continue; 98 } 99 case '\n': 100 /* \ <lf> -> line continuation */ 101 state->line++; 102 x++; 103 /* eat any extra whitespace */ 104 while((*x == ' ') || (*x == '\t')) x++; 105 continue; 106 default: 107 /* unknown escape -- just copy */ 108 *s++ = *x++; 109 } 110 continue; 111 default: 112 *s++ = *x++; 113 } 114 } 115 return T_EOF; 116 }
首先初始化游标x,其指向当前扫描的缓存中要解析的字符
第一部分,要判断 nexttoken ,如果不为0,及代表词法单元中有新行,要重新初始化为0并直接返回,再做语法解析。
第二部分,要跳过空格,制表,回车,注释,空的换行,并向前移动缓存指针,继续解析新的内容。若是空的换行,返回之后会发现 nargs 为0,不做语法解析。
三部分,如果遇到了字符串,则有三个标签对应不同的情况:text,textresume,textdone。遇到换行符跳到textdone标签下执行代码,遇到字符跳到text标签下执行代码,遇到双引号,要原样返回双引号之内的内容,即不跳过空格,回车等字符,则在遇到右边的双引号后再跳到textresume标签下继续执行解析。
先看text标签下的内容。首先初始化词法单元指针 text 和执行词法单元尾部的指针s,为x,x为游标,指向当前扫描位置。随后扫描每个字符,,遇到字符串则继续向前移动s和x,遇到空格,制表,回车,换行符,代表词法单元构建完成,则把游标x向前移动到下一个要解析的字符,然后跳转到textdone标签,换行符还会设置 nexttoken 为T_NEWLINE,代表遇到换行,下次解析可以直接返回遇到了换行,开始单行解析。
在textdone标签,会设置parse state的ptr指针为当前x,然后在s执行的地方(内容为换行符,空格,制表,回车),将其设为0,代表字符串结束。最后返回T_TEXT,表明进行词法单元聚合。
解析的字符有两种特殊情况。
一是双引号字符,在其后遇到0代表文件解析结束,直接返回T_EOF;遇到其他字符继续扫描,遇到另一个双引号代表被扫描的字符串结束,跳转到textresume标签继续扫描其他字符。
二是转义字符 '\\' ,先把游标x向前移动一个字符,下一个字符如果是:遇到0,则什么也不做,跳转到textdone标签,\被替换为\0,代表字符串结束,然后返回词法单元;遇到 'n' , 'r' , 't' , '\\'将相依字符转义,将原来位置的\字符置换为相依功能的转义字符,然后继续解析下一个字符;遇到回车符,若下一个字符为换行符,则与换行符处的处理一致,若下一个字符不是换行符,则将游标x向前移动一下,将转义字符替换为\r之后的一个字符(若为空格之类的在下次此处理时自动判断词法单元结束),代表之后的字符接着上一行;遇到换行字符,代表之后的字符接着上一行,不是新行,则跳过空格和制表符,继续下边的扫描,即没有textdone;遇到其他的字符不管,原样送回给语法解析器解析。