Android 4.4 的 init 程式詳解

悠然紅茶的部落格發表於2015-06-22

1背景

前些日子需要在科室內做關於Android系統啟動流程的培訓。為此,我在幾年前的技術手記的基礎上,重新改了一份培訓文件。在重新整理文件期間,我也重讀了一下Android 4.4的相關程式碼,發現還有一些東西是我以前一直沒重視過的,所以打算寫下來總結一二。

我以前之所以沒有把關於Android系統啟動方面的手記整理成博文,主要是因為網上已經有許多類似的文章了,再說一遍好像也沒什麼意思。但這次的培訓既然已迫使我重整了一份文件,那麼倒也不妨貼出來供大家參考。文中的某些細節是我最近新補充的內容,這樣或許能和網上其他文章有所區別吧。

2概述init程式

我們先概述一下Android的init程式。init是Linux系統中,使用者空間的第一個程式。它負責建立系統中最關鍵的幾個子程式,尤其是zygote。另外,init還提供了property service(屬性服務),類似於windows系統的登錄檔服務。有關屬性服務的細節,大家可參考我寫的《Android Property機制》一文,本文就不多說了。

在Android系統中,會有個init.rc指令碼。Init程式一啟動就會讀取並解析這個指令碼檔案,把其中的元素整理成自己的資料結構(連結串列)。具體情況可參考system\core\init\init.c檔案,它的main()函式會先呼叫init_parse_config_file(“/init.rc”)來解析init.rc指令碼,分析出應該執行的語義,並且把指令碼中描述的action和service資訊分別組織成雙向連結串列,然後執行之。示意圖如下:

3解析init.rc指令碼

3.1介紹init.rc指令碼

Init.rc指令碼使用的是一種初始化語言,其中包含了4類宣告:
1)Action
2)Command
3)Service
4)Option
該語言規定,Action和Service是以一種“小節”(Section)的形式出現的,其中每個Action小節可以含有若干Command,而每個Service小節可以含有若干Option。小節只有起始標記,卻沒有明確的結束標記,也就是說,是用“後一個小節”的起始來結束“前一個小節”的。

指令碼中的Action大體上表示一個“行動”,它用一系列Command共同完成該“行動”。Action需要有一個觸發器(trigger)來觸發它,一旦滿足了觸發條件,這個Action就會被加到執行佇列的末尾。Action的形式如下:

on  <trigger>
 <command1>
 <command2>
    ......

Service表示一個服務程式,會在初始化時啟動。因為init.rc指令碼中描述的服務往往都是核心服務,所以(基本上所有的)服務會在退出時自動重啟。Service的形式如下:

service <name> <pathname> [<arguments>]*
 <option>
 <option>
  ......

Init.rc中的Service截選如下:

service servicemanager /system/bin/servicemanager
    class core
    user system
    group system
    critical
    onrestart restart healthd
    onrestart restart zygote
    onrestart restart media
    onrestart restart surfaceflinger
    onrestart restart drm

service vold /system/bin/vold
    class core
    socket vold stream 0660 root mount
    ioprio be 2

service netd /system/bin/netd
    class main
    socket netd stream 0660 root system
    socket dnsproxyd stream 0660 root inet
    socket mdns stream 0660 root system

請大家留心service裡的class選項,比如上面的class core和class main。它表示該service是屬於哪種型別的服務。在後文的闡述boot子階段時,會用到這個概念。

其實,除了Action和Service,Init.rc中還有一種小節,就是Import小節。該小節表達的意思有點兒像java中的import,也就是說,Init.rc中還可以匯入其他.rc指令碼檔案的內容。在早期的Android中,好像並不支援import語句,不過至少從Android4.0開始,新增了import語句。至於import最早出現在哪個版本,我沒有考證過。import句子截選如下:

import /init.environ.rc
import /init.usb.rc
import /init.${ro.hardware}.rc
import /init.trace.rc

3.2解析

在init程式的main()函式裡,會呼叫init_parse_config_file(“/init.rc”)一句來解析init.rc指令碼。init_parse_config_file()的程式碼如下:
【system/core/init/Init_parser.c】

int init_parse_config_file(const char *fn)
{
    char *data;
    data = read_file(fn, 0);
    if (!data) return -1;

    parse_config(fn, data);
    DUMP();
    return 0;
}

先用read_file()把指令碼內容讀入一塊記憶體,而後呼叫parse_config()解析這塊記憶體。

parse_config()的程式碼截選如下:

static void parse_config(const char *fn, char *s)
{
    . . . . . .
    for (;;) {
        switch (next_token(&state)) {
        . . . . . .
        case T_NEWLINE:   // 遇到折行
            state.line++;
            if (nargs) {
                int kw = lookup_keyword(args[0]);
                if (kw_is(kw, SECTION)) {
                    state.parse_line(&state, 0, 0);  // 不同section的parse_line也不同噢
                    parse_new_section(&state, kw, nargs, args);
                } else {
                    state.parse_line(&state, nargs, args);
                }
                nargs = 0;
            }
            break;
        . . . . . .
    . . . . . .
}

它在逐行分析init.rc指令碼,判斷每一行的第一個引數是什麼型別的,如果是action或service型別的,就表示要建立一個新的section節點了,此時它會設定一下解析後續行的解析函式,也就是給state->parse_line賦值啦。針對service型別,解析後續行的函式是parse_line_service(),而針對action型別,解析後續行的函式則是parse_line_action()。

這麼看來,parse_config()裡有3個地方值得我們注意:

  • lookup_keyword()和kw_is()
  • parse_new_section()
  • state.parse_line()

3.2.1查詢指令碼關鍵字

我們先介紹關於關鍵字查詢方面的知識,在這裡主要看lookup_keyword()和kw_is()。

lookup_keyword()的定義截選如下:
【system/core/init/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;
    . . . . . .
    . . . . . .

kw_is()巨集的定義如下:

    #define kw_is(kw, type) (keyword_info[kw].flags & (type))

基本上是查表的過程,而lookup_keyword()返回的那些K_copy、K_capability值,其實就是表項的索引號。這張關鍵字表的技術細節如下。

在init_parser.c檔案中有下面這樣的程式碼:

【system/core/init/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"
};
#undef KEYWORD

這裡用到了一點兒小技巧,兩次include了keywords.h標頭檔案,其實keywords.h中會先定義一次KEYWORD巨集,其主要目的是為了形成一個順序排列的enum,而後就#undef KEYWORD了。接著上面程式碼中再次定義了KEYWORD巨集,這次的主要目的是為了形成一個struct陣列,即keyword_info陣列。

keywords.h的部分截選如下:
【system/core/init/Keywords.h】

#ifndef KEYWORD
int do_chroot(int nargs, char **args);
int do_chdir(int nargs, char **args);
int do_class_start(int nargs, char **args);
. . . . . .
. . . . . .
#define __MAKE_KEYWORD_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)
    . . . . . .
    . . . . . .
#ifdef __MAKE_KEYWORD_ENUM__
    KEYWORD_COUNT,
};
#undef __MAKE_KEYWORD_ENUM__
#undef KEYWORD
#endif

其中的#define KEYWORD是第一次定義KEYWORD,我們比對一下這兩次定義:

// 第一次
#define KEYWORD(symbol, flags, nargs, func) K_##symbol,
// 第二次
#define KEYWORD(symbol, flags, nargs, func) \
    [ K_##symbol ] = { #symbol, func, nargs + 1, flags, },

總之,最後形成了如下陣列:

表中只有3個表項的flag是SECTION,表示這是個小節,我用黃色框表示。

3.2.2解析section小節

一旦分析出某句指令碼是以on或者service或者import開始,就說明一個新的小節要開始了。此時,會呼叫到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:
        state->context = parse_service(state, nargs, args);
        if (state->context) {
            state->parse_line = parse_line_service;
            return;
        }
        break;
    case K_on:
        state->context = parse_action(state, nargs, args);
        if (state->context) {
            state->parse_line = parse_line_action;
            return;
        }
        break;
    case K_import:
        parse_import(state, nargs, args);
        break;
    }
    state->parse_line = parse_line_no_op;
}

很明顯,解析的小節就是那三類:action小節(以on開頭的),service小節和import小節。最核心的部分當然是service小節和action小節,具體解析的地方在上面程式碼中的parse_service()和parse_action()函式裡。至於import小節,parse_import()函式只是把指令碼中的所有import語句先彙總成一個連結串列,記入state結構中,待回到parse_config()後再做處理。

3.2.2.1解析service小節

parse_service()的程式碼如下:

【system/core/init/Init_parser.c】

static void *parse_service(struct parse_state *state, int nargs, char **args)
{
    struct service *svc;
    . . . . . .
    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段時,會用calloc()申請一個service節點,填入service名等資訊,並連入service_list總表中。注意,此時該service節點的onrestart.commands部分還是個空連結串列,因為我們還沒有分析該service的後續指令碼行呢。
parse_new_section()中為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;
    . . . . . .
    kw = lookup_keyword(args[0]);   // 解析具體的service option也是要查關鍵字表的
    switch (kw) {
    case K_capability:
        break;
    case K_class:
        if (nargs != 2) {
            parse_error(state, "class option requires a classname\n");
        } else {
            svc->classname = args[1];
        }
        break;
    case K_console:
        svc->flags |= SVC_CONSOLE;
        break;
case K_disabled:
. . . . . .
. . . . . .

service的各個option會影響service節點的不同域,比如flags域、classname域、onrestart域等等。比較麻煩的是onrestart域,因為它本身又是個action節點,可攜帶若干個子command。

下面是service中常見的option:
1)K_capability
2)K_class
3)K_console
4)K_disabled
5)K_ioprio
6)K_group
7)K_user
8)K_keycodes
9)K_oneshot
10)K_onrestart
11)K_critical
12)K_setenv
13)K_socket
14)K_seclabel

在service小節解析完畢後,我們應該能得到類似下圖這樣的service節點:

3.2.2.2解析action小節

另一方面,解析action小節時的動作也很簡單,會用calloc()申請一個action節點,填入action名等資訊,然後連入action_list總表中。當然,此時action的commands部分也是空的。

static void *parse_action(struct parse_state *state, int nargs, char **args)
{
struct action *act;
    . . . . . .
    act = calloc(1, sizeof(*act));
    act->name = args[1];
    list_init(&act->commands);
    list_init(&act->qlist);
    list_add_tail(&action_list, &act->alist);
    return act;
}

對於action小節而言,我們指定了不同的解析後續行的函式,也就是parse_line_action()。該函式的程式碼截選如下:

static void parse_line_action(struct parse_state* state, int nargs, char **args)
{
    struct command *cmd;
    struct action *act = state->context;
    . . . . . .
    kw = lookup_keyword(args[0]);   // 解析具體的action command也是要查關鍵字表的
    if (!kw_is(kw, COMMAND)) {
        parse_error(state, "invalid command '%s'\n", args[0]);
        return;
    }

    n = kw_nargs(kw);
    if (nargs < n) {
        parse_error(state, "%s requires %d %s\n", args[0], n - 1,
            n > 2 ? "arguments" : "argument");
        return;
    }
    cmd = malloc(sizeof(*cmd) + sizeof(char*) * nargs);
    cmd->func = kw_func(kw);
    cmd->nargs = nargs;
    memcpy(cmd->args, args, sizeof(char*) * nargs);
    list_add_tail(&act->commands, &cmd->clist);
}

既然action的後續行可以包含多條command,那麼parse_line_action()就必須先確定出當前分析的是什麼command,這一點和parse_line_service()是一致的,都是通過呼叫lookup_keyword()來查詢關鍵字的。另外,command子行的所有引數其實已被記入傳進來的args引數,現在這些引數會記入command節點的args域中,而且這個command節點會鏈入action節點的commands連結串列尾部。

在action小節解析完畢後,我們應該能得到類似下圖這樣的action節點:

3.2.3主要形成兩個雙向連結串列

我們畫了一張關於parse_config()的呼叫關係圖,如下:

init_parse_config_file()函式會將Init.rc指令碼解析成兩個雙向連結串列,對應的表頭分別是service_list和action_list。雙向連結串列示意圖如下:

3.3具體執行那些action

經過解析一步,init.rc指令碼中的actions被整理成雙向連結串列了,但是這些action並沒有被實際執行。現在我們就來看下一步具體執行action的流程。

在init程式的main()函式中,我們可以看到如下句子:

int main(int argc, char **argv)
{
    . . . . . .
    . . . . . .
    init_parse_config_file("/init.rc");  // 內部將指令碼內容轉換成action連結串列了

    action_for_each_trigger("early-init", action_add_queue_tail);

    queue_builtin_action(wait_for_coldboot_done_action, 
                         "wait_for_coldboot_done");
    queue_builtin_action(mix_hwrng_into_linux_rng_action, 
                         "mix_hwrng_into_linux_rng");
    queue_builtin_action(keychord_init_action, "keychord_init");
    queue_builtin_action(console_init_action, "console_init");

    /* execute all the boot actions to get us started */
    action_for_each_trigger("init", action_add_queue_tail);
    . . . . . .
    . . . . . .
}

首先,init_parse_config_file()已經把init.rc指令碼里的內容轉換成action連結串列了,接著程式碼執行到action_for_each_trigger(“early-init”…)一句,這一句會把action_list列表中匹配的action節點,連入action_queue佇列。

3.3.1整理action_queue佇列

init程式希望把系統初始化過程分割成若干“子階段”,action_for_each_trigger()的意思就是“觸發某個子階段裡的所有action”。在早期的Android中,大概就只有4、5個子階段,現在隨著Android的不斷升級,子階段也變得越來越多了。

action_for_each_trigger()的程式碼如下:

void action_for_each_trigger(const char *trigger,
                             void (*func)(struct action *act))
{
    struct listnode *node;
    struct action *act;
    list_for_each(node, &action_list) {
        act = node_to_item(node, struct action, alist);
        if (!strcmp(act->name, trigger)) {
            func(act);  // 只要匹配,就回撥func
        }
    }
}

可以看到是在遍歷action_list連結串列,找尋所有“action名”和“引數trigger”匹配的節點,並回撥“引數func所指的回撥函式”。在前面的程式碼中,回撥函式就是action_add_queue_tail()。

void action_add_queue_tail(struct action *act)
{
    if (list_empty(&act->qlist)) {
        list_add_tail(&action_queue, &act->qlist);
    }
}

嗯,這裡又出現了個action_queue佇列!它和action_list列表有什麼關係?

其實很簡單,action_list可以被理解成一個來自init.rc的“草稿列表”,列表中的節點順序基本上和init.rc指令碼里編寫section時的順序一致,而這個順序不一定就是合適的“執行順序”,所以我們需要另一個按我們的要求依次串接的佇列,那就是action_queue佇列。另外,有些新的action並沒有體現在init.rc指令碼里,而是寫在具體程式碼裡的,這些action可以被稱為“內建action”,我們可以通過呼叫queue_builtin_action()將“內建action”新增進action_list列表和action_queue佇列中。

queue_builtin_action()的程式碼如下:

void queue_builtin_action(int (*func)(int nargs, char **args), char *name)
{
    struct action *act;
    struct command *cmd;

    act = calloc(1, sizeof(*act));
    act->name = name;
    list_init(&act->commands);
    list_init(&act->qlist);

    cmd = calloc(1, sizeof(*cmd));
    cmd->func = func;
    cmd->args[0] = name;
    list_add_tail(&act->commands, &cmd->clist);

    list_add_tail(&action_list, &act->alist);
    action_add_queue_tail(act);
}

init程式裡主要分割的“子階段”如下圖所示:

桔色方框表示的子階段,是比較重要的階段。

3.3.1.1early-init子階段

我們先看early-init子階段,這部分在init.rc裡是這樣表達的:

on early-init
    # Set init and its forked children's oom_adj.
    write /proc/1/oom_adj -16

    # Set the security context for the init process.
    # This should occur before anything else (e.g. ueventd) is started.
    setcon u:r:init:s0
    start ueventd
    # create mountpoints
    mkdir /mnt 0775 root system

這個action包含4條command,分別是write、setcon、start和mkdir。不同command對應的func回撥函式也是不同的,具體對應什麼,可以檢視Keywords.h。
【system/core/init/Keywords.h】

    KEYWORD(service,  SECTION, 0, 0)
    KEYWORD(setcon,      COMMAND, 1, do_setcon)
    KEYWORD(setenforce,  COMMAND, 1, do_setenforce)
    KEYWORD(setenv,      OPTION,  2, 0)
    KEYWORD(setkey,      COMMAND, 0, do_setkey)
    KEYWORD(setprop,     COMMAND, 2, do_setprop)
    KEYWORD(setrlimit,   COMMAND, 3, do_setrlimit)
    KEYWORD(setsebool,   COMMAND, 2, do_setsebool)
    KEYWORD(socket,      OPTION,  0, 0)
    KEYWORD(start,       COMMAND, 1, do_start)
    KEYWORD(stop,        COMMAND, 1, do_stop)
    KEYWORD(swapon_all,  COMMAND, 1, do_swapon_all)
    KEYWORD(trigger,     COMMAND, 1, do_trigger)
    KEYWORD(symlink,     COMMAND, 1, do_symlink)
    KEYWORD(sysclktz,    COMMAND, 1, do_sysclktz)
    KEYWORD(user,        OPTION,  0, 0)
    KEYWORD(wait,        COMMAND, 1, do_wait)
    KEYWORD(write,       COMMAND, 2, do_write)
    KEYWORD(copy,        COMMAND, 2, do_copy)
    KEYWORD(chown,       COMMAND, 2, do_chown)
    KEYWORD(chmod,       COMMAND, 2, do_chmod)

比如說start命令對應的回撥函式就是do_start():

int do_start(int nargs, char **args)
{
    struct service *svc;
    svc = service_find_by_name(args[1]);
    if (svc) {
        service_start(svc, NULL);
    }
    return 0;
}

啟動所指定的service。

3.3.1.2boot子階段

boot部分在init.rc裡是這樣表達的:

on boot
    ifup lo
    hostname localhost
    domainname localdomain
    setrlimit 13 40 40
    . . . . . .
    write /proc/sys/vm/overcommit_memory 1
    write /proc/sys/vm/min_free_order_shift 4
    chown root system /sys/module/lowmemorykiller/parameters/adj
    chmod 0664 /sys/module/lowmemorykiller/parameters/adj
    . . . . . .
    . . . . . .
    setprop net.tcp.default_init_rwnd 60
    class_start core
    class_start main

請注意最後的兩句,表示boot動作的最後,會自動先啟動所有型別為“core”的服務,而後再啟動所有型別為“main”的服務。我們在前文闡述init.rc指令碼中的service寫法時,特別讓大家留意service的class選項,比如class core和class main,現在要用到這個概念了。

class_start命令對應的回撥函式是do_class_start(),該函式的程式碼如下:
【system/core/init/Builtins.c】

int do_class_start(int nargs, char **args)
{
    service_for_each_class(args[1], service_start_if_not_disabled);
    return 0;
}
void service_for_each_class(const char *classname,
                            void (*func)(struct service *svc))
{
    struct listnode *node;
    struct service *svc;
    list_for_each(node, &service_list) {
        svc = node_to_item(node, struct service, slist);
        if (!strcmp(svc->classname, classname)) {
            func(svc);    // 回撥service_start_if_not_disabled()
        }
    }
}

其回撥的func,就是service_start_if_not_disabled(),程式碼如下:

static void service_start_if_not_disabled(struct service *svc)
{
    if (!(svc->flags & SVC_DISABLED)) {
        service_start(svc, NULL);
    }
}

程式碼很簡單,service_for_each_class()會遍歷service_list連結串列,找到所有和classname匹配的service節點,如果這個節點沒有被disabled的話,那麼就啟動其對應的服務。
boot子階段先啟動的“core”型別的服務有:

core型別的服務 對應的可執行檔案 說明
ueventd /sbin/ueventd
healthd /sbin/healthd
console /system/bin/sh
adbd /sbin/adbd
servicemanager /system/bin/servicemanager 大名鼎鼎的service manager service服務,Android的核心之一。
vold /system/bin/vold

而後,boot子階段啟動的“main”型別的服務有:

main型別的服務 對應的可執行檔案 說明
netd /system/bin/netd
debuggerd /system/bin/debuggerd
ril-daemon /system/bin/rild
surfaceflinger /system/bin/surfaceflinger
zygote /system/bin/app_process Android建立內部建立新程式的核心服務。
drm /system/bin/drmserver
media /system/bin/mediaserver
bootanim /system/bin/bootanimation
installd /system/bin/installd
flash_recovery /system/etc/install-recovery.sh
racoon /system/bin/racoon
mtpd /system/bin/mtpd
keystore /system/bin/keystore
dumpstate /system/bin/dumpstate
sshd /system/bin/start-ssh
mdnsd /system/bin/mdnsd

3.3.2for迴圈中執行action_queue佇列

現在我們繼續看,動作在編排進action_queue佇列之後,又是如何執行的呢?我們知道,init程式最終會進入一個for(;;)迴圈,在這個迴圈中,每次都會嘗試執行一個command:

int main(int argc, char **argv)
{
    . . . . . .
. . . . . .
    // 這個for迴圈非常重要哦!
    for(;;) {
        int nr, i, timeout = -1;
        execute_one_command();
        restart_processes();
    . . . . . .
}

其中呼叫的execute_one_command()的程式碼如下:

void execute_one_command(void)
{
    int ret;
    if (!cur_action || !cur_command || is_last_command(cur_action, cur_command))  
    {
        cur_action = action_remove_queue_head();
        cur_command = NULL;
        if (!cur_action)
            return;
        INFO("processing action %p (%s)\n", cur_action, cur_action->name);
        cur_command = get_first_command(cur_action);
    } else {
        cur_command = get_next_command(cur_action, cur_command);
    }

    if (!cur_command)
        return;

    ret = cur_command->func(cur_command->nargs, cur_command->args);
    INFO("command '%s' r=%d\n", cur_command->args[0], ret);
}

它的意思是說,執行“當前action”(cur_action)的“當前command”(cur_command)。如果執行時沒有“當前action”,就嘗試從action_queue佇列的頭部摘取一個節點。如果執行時沒有“當前command”,就從“當前action”中獲取下一個該執行的command。而一旦得到了該執行的command,就回撥其func函式指標。

在那幾個core型別的service中,有一個非常重要的service,叫做zygote,它是android內部建立新程式的核心服務,但本文就不對它細說了。

4補充說明幾個運作機理知識

下面我們補充說明幾個init程式裡的運作機理。

4.1service是如何重啟的?

關於service的重啟方法,其實用到了linux的一點兒訊號機制。在init程式的main()函式中,除了“early-init”、“init”等子階段外,還有個子階段叫作“signal_init”:

queue_builtin_action(signal_init_action, "signal_init");

當init程式執行到這個子階段時,會執行signal_init_action()回撥函式:
【system/core/init/Init.c】

static int signal_init_action(int nargs, char **args)
{
    signal_init();
    return 0;
}

【system/core/init/Signal_handler.c】

void signal_init(void)
{
    int s[2];

    struct sigaction act;
    memset(&act, 0, sizeof(act));
    act.sa_handler = sigchld_handler;
    act.sa_flags = SA_NOCLDSTOP;
    sigaction(SIGCHLD, &act, 0);   // 向系統註冊一個系統回撥

    /* create a signalling mechanism for the sigchld handler */
    if (socketpair(AF_UNIX, SOCK_STREAM, 0, s) == 0) {
        signal_fd = s[0];  // 以後回撥函式會向這個fd寫資料
        signal_recv_fd = s[1];
        fcntl(s[0], F_SETFD, FD_CLOEXEC);
        fcntl(s[0], F_SETFL, O_NONBLOCK);
        fcntl(s[1], F_SETFD, FD_CLOEXEC);
        fcntl(s[1], F_SETFL, O_NONBLOCK);
    }

    handle_signal();
}

請注意,signal_init()中呼叫了sigaction(SIGCHLD,…)一句。在linux系統中,當一個程式終止或者停止時,系統會向其父程式傳送SIGCHLD訊號。sigaction()動作可以被理解為向系統註冊一個系統回撥函式。在本例中,每當有子程式終止時,系統就會回撥sigchld_handler()回撥函式,該函式的程式碼如下:
【system/core/init/Signal_handler.c】

static void sigchld_handler(int s)
{
    write(signal_fd, &s, 1);
}

看到了嗎?無非是向signal_init()中建立的“socket對”裡的signal_fd寫資料,於是“socket對”的另一個控制程式碼signal_recv_fd就可以得到所寫的資料。

在init程式的main()函式中,最終進入那個無限for迴圈,監聽系統的風吹草動,其中就包括監聽這個signal_recv_fd:

int main(int argc, char **argv)
{
    . . . . . .
    . . . . . .
    for(;;) {
        . . . . . .
        if (!signal_fd_init && get_signal_fd() > 0) {
            ufds[fd_count].fd = get_signal_fd();  // 就是signal_recv_fd !
            ufds[fd_count].events = POLLIN;
            ufds[fd_count].revents = 0;
            fd_count++;
            signal_fd_init = 1;
        }
        . . . . . .
        . . . . . .
        nr = poll(ufds, fd_count, timeout);
        . . . . . .
        for (i = 0; i < fd_count; i++) {
            if (ufds[i].revents == POLLIN) {
                if (ufds[i].fd == get_property_set_fd())
                    handle_property_set_fd();  // 處理設定屬性的命令
                else if (ufds[i].fd == get_keychord_fd())
                    handle_keychord();    // 處理類似混合按鍵的命令,類似同時按
                                             // 鋼琴上的若干鍵
                else if (ufds[i].fd == get_signal_fd())
                    handle_signal();    // 處理因子程式掛掉而發來的訊號
            }
        }
    }
    . . . . . .
}

當監聽到signal_recv_fd有動靜時,會呼叫handle_signal()來處理:

void handle_signal(void)
{
    char tmp[32];

    /* we got a SIGCHLD - reap and restart as needed */
    read(signal_recv_fd, tmp, sizeof(tmp));
    while (!wait_for_one_process(0))
        ;
}

wait_for_one_process()的程式碼截選如下:

static int wait_for_one_process(int block)
{
    . . . . . .
while ( (pid = waitpid(-1, &status, block ? 0 : WNOHANG)) == -1 
          && errno == EINTR );
    . . . . . .
    svc = service_find_by_pid(pid);   // 查詢出是哪個service程式掛掉了
    . . . . . .

    svc->pid = 0;
    svc->flags &= (~SVC_RUNNING);

    if ((svc->flags & SVC_ONESHOT) && !(svc->flags & SVC_RESTART)) {
        svc->flags |= SVC_DISABLED;
    }
    if (svc->flags & (SVC_DISABLED | SVC_RESET) )  {
        notify_service_state(svc->name, "stopped");
        return 0;
    }
    . . . . . .

    svc->flags &= (~SVC_RESTART);
    svc->flags |= SVC_RESTARTING;

    /* Execute all onrestart commands for this service. */
    list_for_each(node, &svc->onrestart.commands) {
        cmd = node_to_item(node, struct command, clist);
        cmd->func(cmd->nargs, cmd->args);
    }
    notify_service_state(svc->name, "restarting");
    return 0;
}

該函式的程式碼比較清晰,當init程式被通知某個子程式終止時,它會嘗試找到這個子程式對應的service節點,並輾轉給該節點的flags域新增SVC_RESTARTING標記,然後又會馬上執行這個service節點中所有onrestart選項對應的動作。

程式碼中處理SVC_ONESHOT的地方多判斷了SVC_RESTART標誌,這是為什麼呢?我想理由是這樣的:SVC_ONESHOT表達的意思是“只打一槍”,也就是說以它裝飾的service程式,就算掛掉了,也不會重新啟動。然而必須兼顧到其他程式restart的情況。假如有另一個程式會連鎖restart該service,此時就算該service有SVC_ONESHOT標誌,它還是應該再次啟動的。

svc節點的onrestart域本身就是個action型別的域:

struct action onrestart;

現在開始遍歷onrestart域裡的commands列表:

list_for_each(node, &svc->onrestart.commands) {
    cmd = node_to_item(node, struct command, clist);
    cmd->func(cmd->nargs, cmd->args);
}

看來,service的那些onrestart子句是一次性完成的。我們以前文說的zygote服務為例,當它重啟時,會執行兩次do_write()以及兩次do_start(),分別啟動media服務和netd服務。

最後,wait_for_one_process()還會呼叫一下notify_service_state()。畢竟這是因為某個service掛掉了,才會再走到這裡的,現在我們馬上就要重新啟動那個剛死的service啦,所以最好還是做一些必要的“通知動作”。請注意,這種關於重啟service的“通知”並不是簡單發個事件什麼的,而是設定某個相應的系統屬性。具體的動作請看notify_service_state()的程式碼:

void notify_service_state(const char *name, const char *state)
{
    char pname[PROP_NAME_MAX];
    int len = strlen(name);
    if ((len + 10) > PROP_NAME_MAX)
        return;
    snprintf(pname, sizeof(pname), "init.svc.%s", name);
    property_set(pname, state);
}

看到了嗎?會設定一個以“init.svc.”打頭的系統屬性。比如重啟zygote服務,此時就會把“init.svc.zygote”屬性值設為“SVC_RESTARTING”。

大家有沒有注意到,wait_for_one_process()里根本沒有fork動作。這也就是說,wait_for_one_process()中並不會立即重啟新的service程式。大家都知道現在我們正處於init程式的無限for迴圈中,所以程式從wait_for_one_process()返回後,總會再次走到for迴圈中的restart_processes():

int main(int argc, char **argv)
{
    . . . . .
    for(;;) {
        int nr, i, timeout = -1;
        execute_one_command();
        restart_processes();

此時才會重啟新的程式:

static void restart_processes()
{
    process_needs_restart = 0;
    service_for_each_flags(SVC_RESTARTING, restart_service_if_needed);
}

遍歷service_list列表,找出那些flags中攜帶有SVC_RESTARTING標誌的service節點,並執行restart_service_if_needed()。

static void restart_service_if_needed(struct service *svc)
{
    time_t next_start_time = svc->time_started + 5;

    if (next_start_time <= gettime()) {
        svc->flags &= (~SVC_RESTARTING);
        service_start(svc, NULL);
        return;
    }

    if ((next_start_time < process_needs_restart) ||
        (process_needs_restart == 0)) {
        process_needs_restart = next_start_time;
    }
}

注意,為了防止出現service連續緊密重啟的情況,next_start_time會賦值為svc->time_started + 5,也就是說,至少得喘息個5毫秒,然後才能進行下一次重啟。這就是Android中重啟service的具體流程。

4.2混合按鍵是如何啟動service的?

現在我們順便說一下用混合按鍵重啟service的技術,這部分內容現在已經很少用到了。至少在我們常見的專案的init.rc指令碼里是搜不到“keycodes”關鍵字的。這個關鍵字是個option,如果某個service裡含有keycodes選項的話,就說明設計者希望在使用者按下某種組合鍵時,init程式能重啟這個service。

這種能點選出的組合鍵,很像同時按下幾個鋼琴鍵而發出和旋,因此被稱為keychord。在init程式的啟動子過程中,“keychord(初始化)子階段”甚至還要早於“init子階段”呢。

queue_builtin_action(keychord_init_action, "keychord_init");

其中keychord_init_action()的程式碼如下:

【system/core/init/Init.c】

static int keychord_init_action(int nargs, char **args)
{
    keychord_init();
    return 0;
}

【system/core/init/Keychords.c】

void keychord_init()
{
    int fd, ret;
    service_for_each(add_service_keycodes);
    if (!keychords)
        return;
    fd = open("/dev/keychord", O_RDWR);
    if (fd < 0) {
        ERROR("could not open /dev/keychord\n");
        return;
    }
    fcntl(fd, F_SETFD, FD_CLOEXEC);

    ret = write(fd, keychords, keychords_length);
    if (ret != keychords_length) {
        ERROR("could not configure /dev/keychord %d (%d)\n", ret, errno);
        close(fd);
        fd = -1;
    }
    free(keychords);
    keychords = 0;
    keychord_fd = fd;
}

初始化時,利用service_for_each(),遍歷service_list列表,對每個列表節點呼叫add_service_keycodes(),該函式程式碼如下:
【system/core/init/Keychords.c】

void add_service_keycodes(struct service *svc)
{
    struct input_keychord *keychord;
    int i, size;

    if (svc->keycodes) {
        /* add a new keychord to the list */
        size = sizeof(*keychord) + 
                svc->nkeycodes * sizeof(keychord->keycodes[0]);
        keychords = realloc(keychords, keychords_length + size);
        if (!keychords) {
            ERROR("could not allocate keychords\n");
            keychords_length = 0;
            keychords_count = 0;
            return;
        }

        keychord = (struct input_keychord *)
                                       ((char *)keychords + keychords_length);
        keychord->version = KEYCHORD_VERSION;
        keychord->id = keychords_count + 1;
        keychord->count = svc->nkeycodes;
        svc->keychord_id = keychord->id;

        for (i = 0; i < svc->nkeycodes; i++) {
            keychord->keycodes[i] = svc->keycodes[i];
        }
        keychords_count++;
        keychords_length += size;
    }
}

其中用到的keychords是個靜態變數:

static struct input_keychord *keychords = 0;

它實質上指向了一塊buffer,該buffer最終會存下所有keychord資訊。當我們遍歷service_list列表時,一旦發現某個service節點攜帶有keycodes,就會從這個buffer中劃分出一塊,並在其中寫入從service節點讀取到的keycodes資訊。因為不同service攜帶的keycode部分可能不一樣,所以每次分出的那塊記憶體的大小也不太一樣。不過大體上每一小塊記錄的都是input_keychord結構,該結構的定義如下:

【kernel/include/linux/Keychord.h】

struct input_keychord {
 __u16 version;
 __u16 id;
 __u16 count;
 __u16 keycodes[];
};

另外,請注意上面程式碼中的這幾句:

keychord->id = keychords_count + 1;
keychord->count = svc->nkeycodes;
svc->keychord_id = keychord->id;

keychord資訊裡有個唯一的id號,而且這個id號還會回寫到service節點的keychord_id域。 經過這次遍歷,我們大體上可以畫出下面這樣的示意圖:

在整理好keychords這塊buffer後,keychord_init()會把它寫入“/dev/keychord”裝置檔案。

fd = open("/dev/keychord", O_RDWR);
. . . . . . 
ret = write(fd, keychords, keychords_length);

這應該是向驅動層通知重要資訊了。而且請注意,這個fd檔案描述符會被記錄下來:

keychord_fd = fd;

記錄下fd有什麼用呢?很簡單,init程式在最後那個for迴圈裡,會監聽這個fd,從而感知到從驅動層發來的混合按鍵,程式碼如下:

if (!keychord_fd_init && get_keychord_fd() > 0) {
    ufds[fd_count].fd = get_keychord_fd(); // 得到的就是那個keychord檔案描述符
    ufds[fd_count].events = POLLIN;
    ufds[fd_count].revents = 0;
    fd_count++;
    keychord_fd_init = 1;
}

一旦監聽到有混合按鍵發生了,就會走到下面的handle_keychord():

        for (i = 0; i < fd_count; i++) {
            if (ufds[i].revents == POLLIN) {
                if (ufds[i].fd == get_property_set_fd())
                    handle_property_set_fd();
                else if (ufds[i].fd == get_keychord_fd())
                    handle_keychord();   // 處理混合按鍵
                else if (ufds[i].fd == get_signal_fd())
                    handle_signal();
            }
        }

【system/core/init/Keychords.c】

void handle_keychord()
{
    struct service *svc;
    char adb_enabled[PROP_VALUE_MAX];
    int ret;
    __u16 id;

    // Only handle keychords if adb is enabled.
    property_get("init.svc.adbd", adb_enabled);
    ret = read(keychord_fd, &id, sizeof(id));
    if (ret != sizeof(id)) {
        ERROR("could not read keychord id\n");
        return;
    }

    if (!strcmp(adb_enabled, "running")) {
        svc = service_find_by_keychord(id);
        if (svc) {
            INFO("starting service %s from keychord\n", svc->name);
            service_start(svc, NULL);
        } else {
            ERROR("service for keychord %d not found\n", id);
        }
    }
}

此時會從/dev/keychord裝置檔案裡讀取一個id號,還記得前文說到的“id號會回寫到service節點的keychord_id域”嗎,現在會再次遍歷service_list列表,找到那個keychord_id和讀到的id匹配的service節點,然後呼叫service_start(svc, NULL)啟動這個service。

5小結

關於init程式,我們就先說這麼多吧。限於篇幅,我們不得不把很多不那麼重要的細節省去,有興趣的同學可以自行深入研究。

相關文章