Android 4.4 的 init 程式詳解
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程式,我們就先說這麼多吧。限於篇幅,我們不得不把很多不那麼重要的細節省去,有興趣的同學可以自行深入研究。
相關文章
- Linux init程式詳解Linux
- 2.16.10.init程式詳解1
- git init命令詳解Git
- Linux init詳解Linux
- 【Android】【init】解析init程式啟動過程Android
- (轉)Linux init詳解Linux
- Linux init詳解(轉)Linux
- Android系統啟動:init程式與init語言Android
- 圖解 Android 系列(二)深入理解 init 與 zygote 程式圖解AndroidGo
- 詳解Python中的__init__和__new__Python
- 詳細解讀Python中的__init__()方法Python
- Linux 執行級init詳解(轉)Linux
- AndroidNDKOverview—-Android4.4AndroidView
- Android 4.4 KitKat新特性Android
- Android程式間通訊詳解Android
- Linux中init.d目錄詳解Linux
- AndroidNDKHow-To—-Android4.4Android
- android 4.4/5.1上使用aar的問題Android
- Android 桌布設定程式碼 詳解Android
- android程式與執行緒詳解一:程式Android執行緒
- Android系統啟動流程(一)解析init程式Android
- OpenHarmony的init程式、init配置與啟動項配置
- Nuxt.js 應用中的 nitro:init 事件鉤子詳解UXJS事件
- android 混淆規則作用,Android程式碼混淆詳解Android
- Android4.4中的近場通訊(NFC)Android
- Android 程式間通訊 AIDL詳解AndroidAI
- Android 啟動過程簡析(一)之 init 程式Android
- 詳解Android RxJava的使用AndroidRxJava
- android4.4修改低電壓提醒Android
- 詳解Python魔法函式,__init__,__str__,__del__Python函式
- Android 網路程式設計系列(3)WebView 詳解Android程式設計WebView
- android程式與執行緒詳解三:AsyncTaskAndroid執行緒
- python 詳解類class的繼承、__init__初始化、super方法Python繼承
- Android 9.0 init 啟動流程Android
- Android AsyncTask 詳解Android
- Android:動畫詳解Android動畫
- Android拖拽詳解Android
- Android:Service詳解Android