編輯:Android資訊
前些日子需要在科室內做關於Android系統啟動流程的培訓。為此,我在幾年前的技術手記的基礎上,重新改了一份培訓文檔。在重新整理文檔期間,我也重讀了一下Android 4.4的相關代碼,發現還有一些東西是我以前一直沒重視過的,所以打算寫下來總結一二。
我以前之所以沒有把關於Android系統啟動方面的手記整理成博文,主要是因為網上已經有許多類似的文章了,再說一遍好像也沒什麼意思。但這次的培訓既然已迫使我重整了一份文檔,那麼倒也不妨貼出來供大家參考。文中的某些細節是我最近新補充的內容,這樣或許能和網上其他文章有所區別吧。
我們先概述一下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信息分別組織成雙向鏈表,然後執行之。示意圖如下:
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
在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()。
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,表示這是個小節,我用黃色框表示。
一旦分析出某句腳本是以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()後再做處理。
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節點:
另一方面,解析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節點:
我們畫了一張關於parse_config()的調用關系圖,如下:
init_parse_config_file()函數會將Init.rc腳本解析成兩個雙向鏈表,對應的表頭分別是service_list和action_list。雙向鏈表示意圖如下:
經過解析一步,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隊列。
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進程裡主要分割的“子階段”如下圖所示:
桔色方框表示的子階段,是比較重要的階段。
我們先看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。
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”類型的服務有:
而後,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現在我們繼續看,動作在編排進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內部創建新進程的核心服務,但本文就不對它細說了。
下面我們補充說明幾個init進程裡的運作機理。
關於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的具體流程。
現在我們順便說一下用混合按鍵重啟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。
關於init進程,我們就先說這麼多吧。限於篇幅,我們不得不把很多不那麼重要的細節省去,有興趣的同學可以自行深入研究。
本博文演示了如何通過自定義View實現微信打飛機游戲。 全部源碼已經開源到GitHub,如果覺得不錯,歡迎大家Star和Fork! GitHub: https:/
前言 開始前我們先來關注一下Android Overflow menu的幾個相關問題: 什麼是Overflow menu Android 3.0以上默認不顯示o
顧名思義,AndroidEventBus ( github鏈接 : https://github.com/bboyfeiyu/AndroidEventBus )是
由於我們很容易習慣公式化的預置代碼,有時我們會忽略很優雅的細節。LayoutInflater以及它在Fragment的onCreateView()中填充View的