Android的init过程:init.rc解析流程
這幾天打算看下安卓的代碼,看優(yōu)秀的源代碼也是一種學(xué)習(xí)過程,看源代碼的過程就感覺到,安卓確實(shí)是深受linux內(nèi)核的影響,不少數(shù)據(jù)結(jié)構(gòu)的使用方法全然一致。花了一中午時(shí)間,研究了下init.rc解析過程,做個(gè)記錄。
init.rc 文件并非普通的配置文件。而是由一種被稱為“Android初始化語言”(Android Init Language。這里簡(jiǎn)稱為AIL)的腳本寫成的文件。在了解init怎樣解析init.rc文件之前,先了解AIL很必要。否則機(jī)械地分析 init.c及其相關(guān)文件的源碼毫無意義。
???? 為了學(xué)習(xí)AIL,讀者能夠到自己Android手機(jī)的根文件夾尋找init.rc文件。最好下載到本地以便查看,假設(shè)有編譯好的Android源碼。 在<Android源碼根文件夾>out/target/product/generic/root文件夾也可找到init.rc文件。
AIL由例如以下4部分組成。
1.? 動(dòng)作(Actions)
2.? 命令(Commands)
3. 服務(wù)(Services)
4.? 選項(xiàng)(Options)
這4部分都是面向行的代碼,也就是說用回車換行符作為每一條語句的分隔符。而每一行的代碼由多個(gè)符號(hào)(Tokens)表示。能夠使用反斜杠轉(zhuǎn)義符在 Token中插入空格。雙引號(hào)能夠?qū)⒍鄠€(gè)由空格分隔的Tokens合成一個(gè)Tokens。假設(shè)一行寫不下,能夠在行尾加上反斜杠。來連接下一行。也就是 說,能夠用反斜杠將多行代碼連接成一行代碼。
???? AIL的凝視與非常多Shell腳本一行。以#開頭。
???? AIL在編寫時(shí)須要分成多個(gè)部分(Section)。而每一部分的開頭須要指定Actions或Services。也就是說。每個(gè)Actions或 Services確定一個(gè)Section。
而全部的Commands和Options僅僅能屬于近期定義的Section。假設(shè)Commands和 Options在第一個(gè)Section之前被定義,它們將被忽略。
Actions和Services的名稱必須唯一。假設(shè)有兩個(gè)或多個(gè)Action或Service擁有相同的名稱。那么init在運(yùn)行它們時(shí)將拋出錯(cuò)誤,并忽略這些Action和Service。
以下來看看Actions、Services、Commands和Options分別應(yīng)怎樣設(shè)置。
Actions的語法格式例如以下:
on <trigger> <command> <command> <command>?也就是說Actions是以keywordon開頭的。然后跟一個(gè)觸發(fā)器,接下來是若干命令。比如。以下就是一個(gè)標(biāo)準(zhǔn)的Action。
on boot ifup lo hostname localhost domainname localdomain
Services (服務(wù))是一個(gè)程序,他在初始化時(shí)啟動(dòng),并在退出時(shí)重新啟動(dòng)(可選)。Services (服務(wù))的形式例如以下:
service <name> <pathname> [ <argument> ]* <option> <option>
比如,以下是一個(gè)標(biāo)準(zhǔn)的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函數(shù)。
在上一篇文章中 分析了main函數(shù)的前一部分(初始化屬性、處理內(nèi)核命令行等),如今找到init_parse_config_file函數(shù),調(diào)用代碼例如以下:
init_parse_config_file("/init.rc");
這種方法主要負(fù)責(zé)初始化和分析init.rc文件。init_parse_config_file函數(shù)在init_parser.c文件里實(shí)現(xiàn),代碼例如以下:
int init_parse_config_file(const char *fn) { char *data; data = read_file(fn, 0); if (!data) return -1; /* 實(shí)際分析init.rc文件的代碼 */ parse_config(fn, data); DUMP(); return 0; }讀取文件read_file有個(gè)地方須要注意:它把init.rc內(nèi)容讀取到data指向的buffer其中。它會(huì)在buffer最后追加兩個(gè)字符:\n和\0。而且在linux系統(tǒng)須要注意的是,每行的結(jié)束唯獨(dú)一個(gè)字符\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; /* 開始獲取每個(gè)token,然后分析這些token,每個(gè)token就是有空格、字表符和回車符分隔的字符串 */ for (;;) { /* next_token函數(shù)相當(dāng)于詞法分析器 */ switch (next_token(&state)) { case T_EOF: /* init.rc文件分析完成 */ state.parse_line(&state, 0, 0); goto parser_done; case T_NEWLINE: /* 分析每一行的命令 */ /* 以下的代碼相當(dāng)于語法分析器 */ 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: /* 處理每個(gè)token */ if (nargs < INIT_PARSER_MAXARGS) { args[nargs++] = state.text; } break; } } parser_done: /* 最后處理由import導(dǎo)入的初始化文件 */ list_for_each(node, &import_list) { struct import *import = node_to_item(node, struct import, list); int ret; INFO("importing '%s'", import->filename); /* 遞歸調(diào)用 */ ret = init_parse_config_file(import->filename); if (ret) ERROR("could not import file '%s' from '%s'\n", import->filename, fn); } }
parse_config的代碼比較復(fù)雜了。如今先說說該方法的基本處理流程。首先會(huì)調(diào)用list_init(&import_list)初始化一個(gè)鏈表。該鏈表用于存儲(chǔ)通過import語句導(dǎo)入的初始化文件名稱。
然后開始在for循環(huán)中分析init.rc文件里的每一行代碼。最后init.rc分析完之后,就會(huì)進(jìn)入parse_done部分,并遞歸調(diào)用init_parse_config_file方法分析通過import導(dǎo)入的初始化文件。
for循環(huán)中調(diào)用next_token不斷從init.rc文件里獲取token,這里的token,就是一種編程語言的最小單位,也就是不可再分。比如,對(duì)于傳統(tǒng)的編程語言的if、then等keyword、變量名等標(biāo)識(shí)符都屬于一個(gè)token。而對(duì)于init.rc文件來說,import、on以及觸發(fā)器的參數(shù)值都是屬于一個(gè)token。一個(gè)解析器要進(jìn)行語法和詞法的分析,詞法分析就是在文件里找出一個(gè)個(gè)的token。也就是說,詞法分析器的返回值是token,而語法分析器的輸入就是詞法分析器的輸出。也就是說,語法分析器就須要分析一個(gè)個(gè)的token,而不是一個(gè)個(gè)的字符。詞法分析器就是next_token,而語法分析器就是T_NEWLINE分支中的代碼。以下我們來看看next_token是怎么獲取一個(gè)個(gè)的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; } /* 在這里開始一個(gè)字符一個(gè)字符地分析 */ 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的代碼還是蠻多的,只是原理到非常easy。就是逐一讀取init.rc文件的字符。并將由空格、/t分隔的字符串挑出來。并通過state_text返回。并通過state->text返回。假設(shè)返回正常的token,next_token就返回T_TEXT。
假設(shè)一行結(jié)束,就返回T_NEWLINE。并開始語法分析,
特別注意:init初始化語言是基于行的,所以語言分析實(shí)際上就是分析init.rc的每一行,僅僅是這些行已經(jīng)被分解成一個(gè)個(gè)的token并保存在args數(shù)組其中。如今回到parse_config函數(shù)。先看一下T_TEXT分支。
該分支講獲得每一行的token都存儲(chǔ)在args數(shù)組中。如今來看T_NEWLINE分支。
該分支的代碼涉及到一個(gè)state.parse_line函數(shù)指針,該函數(shù)指針指向的函數(shù)負(fù)責(zé)詳細(xì)的分析工作。但我們發(fā)現(xiàn),一看是該函數(shù)指針指向了一個(gè)空函數(shù)。實(shí)際上一開始該函數(shù)什么都不做。
如今來回想一下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;上面的代碼首先調(diào)用lookup_keyword搜索關(guān)鍵字,該方法的作用是判定當(dāng)前行是否合法:也就是依據(jù)init初始化提前定義的關(guān)鍵字查詢。假設(shè)沒有查到返回K_UNKNOWN。假設(shè)當(dāng)前行合法,則會(huì)運(yùn)行parse_new_section函數(shù),該函數(shù)將為section和action設(shè)置處理函數(shù)。代碼例如以下: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: // 單獨(dú)處理import導(dǎo)入的初始化文件。
parse_import(state, nargs, args); break; } state->parse_line = parse_line_no_op; }
我們拿case K_service舉例:首先調(diào)用parse_service函數(shù),該函數(shù)代碼例如以下: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; }該函數(shù)先判定當(dāng)前行參數(shù)個(gè)數(shù)。比方service daemon /system/bin/daemon,此時(shí)剛好滿足條件,參數(shù)剛剛是三個(gè)。第一個(gè)是servicekeyword。第二個(gè)參數(shù)是服務(wù)名。第三個(gè)參數(shù)是服務(wù)所在的路徑。然后調(diào)用service_find_by_name在serivce_list隊(duì)列查找當(dāng)前行的服務(wù)是否已經(jīng)加入過隊(duì)列,假設(shè)加入過即svc!=NULL,那么就報(bào)錯(cuò)。最后最重要的一點(diǎn),填充svc結(jié)構(gòu)體的內(nèi)容,并將其加入到service_list雙向鏈表其中。在填充結(jié)構(gòu)體的內(nèi)容的時(shí)候須要注意的點(diǎn)是:srv->args[]數(shù)組的內(nèi)容,僅僅保存參數(shù),什么意思呢?舉個(gè)樣例。比方init.rc中有這么一行代碼:service dumpstate /system/bin/dumpstate -s,那么剛進(jìn)入到parse_service函數(shù)的時(shí)候,nargs=4。
可是svc的args數(shù)組僅僅須要保存/system/bin/dumpstate -s這兩個(gè)參數(shù)就好了!!
然后會(huì)又一次設(shè)置state->parse_line,比方對(duì)于service的section解析來說,state->parse_line = parse_line_service;這樣就會(huì)調(diào)用parse_line_service解析services的options。
沒有圖像的分析總顯得不夠直觀,以下使用詳細(xì)樣例說明在運(yùn)行完成parse_service和parse_line_service時(shí)的組織結(jié)構(gòu)圖:
service zygote ....
onrestart write /sys/android..
onrestart write /sys/power..
onrestart restart media
圖片取自《深入理解安卓》一書。
從上圖可知:
1)service_list鏈表解說析之后的service所有鏈接到一起。而且是雙向鏈表
2)onrestart通過commands也構(gòu)造一個(gè)雙向鏈表,假設(shè)service以下具有onrestart的option,那么會(huì)將選項(xiàng)掛接到onrestart其中的鏈表其中。
總結(jié)
以上是生活随笔為你收集整理的Android的init过程:init.rc解析流程的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: IoC-spring 的灵魂(带你轻松理
- 下一篇: Spring 的3种依赖注入方式