1. 程式人生 > >Android的init過程:init.rc解析流程

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其中的連結串列當中。