編輯: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進程,我們就先說這麼多吧。限於篇幅,我們不得不把很多不那麼重要的細節省去,有興趣的同學可以自行深入研究。
Android自定義View實現微信打飛機游戲
本博文演示了如何通過自定義View實現微信打飛機游戲。 全部源碼已經開源到GitHub,如果覺得不錯,歡迎大家Star和Fork! GitHub: https:/
Android ActionBar中Overflow Menu(溢出菜單)中的一些問題
前言 開始前我們先來關注一下Android Overflow menu的幾個相關問題: 什麼是Overflow menu Android 3.0以上默認不顯示o
Android事件總線還能怎麼玩?
顧名思義,AndroidEventBus ( github鏈接 : https://github.com/bboyfeiyu/AndroidEventBus )是
Android中深入理解 LayoutInflater.inflate()
由於我們很容易習慣公式化的預置代碼,有時我們會忽略很優雅的細節。LayoutInflater以及它在Fragment的onCreateView()中填充View的