Android的init過程:init.rc解析流程
這幾天打算看下安卓的程式碼,看優秀的原始碼也是一種學習過程,看原始碼的過程就感覺到,安卓確實是深受linux核心的影響,不少資料結構的用法完全一致。花了一中午時間,研究了下init.rc解析過程,做個記錄。
init.rc 檔案並不是普通的配置檔案,而是由一種被稱為“Android初始化語言”(Android Init Language,這裡簡稱為AIL)的指令碼寫成的檔案。在瞭解init如何解析init.rc檔案之前,先了解AIL非常必要,否則機械地分析 init.c及其相關檔案的原始碼毫無意義。
為了學習AIL,讀者可以到自己Android手機的根目錄尋找init.rc檔案,最好下載到本地以便檢視,如果有編譯好的Android原始碼, 在<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
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
現在接著分析一下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;
}
讀取檔案read_file有個地方需要注意:它把init.rc內容讀取到data指向的buffer當中,它會在buffer最後追加兩個字元:\n和\0。並且在linux系統需要注意的是,每行的結束僅僅有一個字元\n。
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分析完之後,就會進入parse_done部分,並遞迴呼叫init_parse_config_file方法分析通過import匯入的初始化檔案。
for迴圈中呼叫next_token不斷從init.rc檔案中獲取token,這裡的token,就是一種程式語言的最小單位,也就是不可再分。例如,對於傳統的程式語言的if、then等關鍵字、變數名等識別符號都屬於一個token。而對於init.rc檔案來說,import、on以及觸發器的引數值都是屬於一個token。一個解析器要進行語法和詞法的分析,詞法分析就是在檔案中找出一個個的token,也就是說,詞法分析器的返回值是token,而語法分析器的輸入就是詞法分析器的輸出。也就是說,語法分析器就需要分析一個個的token,而不是一個個的字元。詞法分析器就是next_token,而語法分析器就是T_NEWLINE分支中的程式碼。下面我們來看看next_token是怎麼獲取一個個的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檔案的字元,並將由空格、/t分隔的字串挑出來,並通過state_text返回,並通過state->text返回。如果返回正常的token,next_token就返回T_TEXT。如果一行結束,就返回T_NEWLINE,並開始語法分析,特別注意:init初始化語言是基於行的,所以語言分析實際上就是分析init.rc的每一行,只是這些行已經被分解成一個個的token並儲存在args陣列當中。現在回到parse_config函式,先看一下T_TEXT分支。該分支講獲得每一行的token都儲存在args陣列中。現在來看T_NEWLINE分支。該分支的程式碼涉及到一個state.parse_line函式指標,該函式指標指向的函式負責具體的分析工作。但我們發現,一看是該函式指標指向了一個空函式,實際上一開始該函式什麼都不做。
現在來回顧一下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。如果當前行合法,則會執行parse_new_section函式,該函式將為section和action設定處理函式。程式碼如下: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;
}
我們拿case K_service舉例:首先呼叫parse_service函式,該函式程式碼如下:
static void *parse_service(struct parse_state *state, int nargs, char **args)
{
struct service *svc;
if (nargs < 3) {
parse_error(state, "services must have a name and a program\n");
return 0;
}
if (!valid_name(args[1])) {
parse_error(state, "invalid service name '%s'\n", args[1]);
return 0;
}
svc = service_find_by_name(args[1]);
if (svc) {
parse_error(state, "ignored duplicate definition of service '%s'\n", args[1]);
return 0;
}
nargs -= 2;
svc = calloc(1, sizeof(*svc) + sizeof(char*) * nargs);
if (!svc) {
parse_error(state, "out of memory\n");
return 0;
}
svc->name = args[1];
svc->classname = "default";
memcpy(svc->args, args + 2, sizeof(char*) * nargs);
svc->args[nargs] = 0;
svc->nargs = nargs;
svc->onrestart.name = "onrestart";
list_init(&svc->onrestart.commands);
list_add_tail(&service_list, &svc->slist);
return svc;
}
該函式先判定當前行引數個數,比如service daemon /system/bin/daemon,此時剛好滿足條件,引數剛剛是三個,第一個是service關鍵字,第二個引數是服務名,第三個引數是服務所在的路徑。然後呼叫service_find_by_name在serivce_list佇列查詢當前行的服務是否已經新增過佇列,如果新增過即svc!=NULL,那麼就報錯;最後最重要的一點,填充svc結構體的內容,並將其新增到service_list雙向連結串列當中。在填充結構體的內容的時候需要注意的點是:srv->args[]陣列的內容,只儲存引數,什麼意思呢?舉個例子,比如init.rc中有這麼一行程式碼:service
dumpstate /system/bin/dumpstate -s,那麼剛進入到parse_service函式的時候,nargs=4。但是svc的args陣列只需要儲存/system/bin/dumpstate -s這兩個引數就好了!!
然後會重新設定state->parse_line,比如對於service的section解析來說,state->parse_line = parse_line_service;這樣就會呼叫parse_line_service解析services的options。
沒有影象的分析總顯得不夠直觀,下面使用具體例子說明在執行完畢parse_service和parse_line_service時的組織結構圖:
service zygote ....
onrestart write /sys/android..
onrestart write /sys/power..
onrestart restart media
圖片取自《深入理解安卓》一書。
從上圖可知:
1)service_list連結串列講解析之後的service全部連結到一起,並且是雙向連結串列
2)onrestart通過commands也構造一個雙向連結串列,如果service下面具有onrestart的option,那麼會將選項掛接到onrestart其中的連結串列當中。