[Android的init過程(一)](http://blog.csdn.net/nokiaguy/article/details/8800962)
本文使用的軟件版本
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 <trigger>
<command>
<command>
<command>
~~~
也就是說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 <name> <pathname> [ <argument> ]*
<option>
<option>
~~~
例如,下面是一個標準的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':
/* \ <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。當返回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文件中每一行的行為。