[深入理解Android卷一全文-第三章]深入理解init
由於《深入理解Android 卷一》和《深入理解Android卷二》不再出版,而知識的傳播不應該因為紙質媒介的問題而中斷,所以我將在CSDN部落格中全文轉發這兩本書的全部內容。
第3章 深入理解init
本章主要內容
· 深入分析init。
本章涉及的原始碼檔名及位置
下面是本章分析的原始碼檔名及其位置。
· init.c
system/core/init/init.c
· parser.c
system/core/init/parser.c
· builtins.c
system/core/init/builtins.c
· keywords.h
system/core/init/keywords/h
· init.rc
system/core/rootdir/init.rc
· properties_service.c
system/core/init/properties_service.c
· libc_init_dynamic.c
bionic/libc/bionic/libc_init_common.c
· libc_init_common.c
bionic/libc/bionic/libc_init_common.c
· properties.c
system/core/libcutils/properties.c
3.1 概述
init是一個程序,確切地說,它是Linux系統中使用者空間的第一個程序。由於Android是基於Linux核心的,所以init也是Android系統中使用者空間的第一個程序,它的程序號是1。作為天字第一號的程序,init被賦予了很多極其重要的工作職責,本章將關注其中兩個比較重要的職責:
· init程序負責建立系統中的幾個關鍵程序,尤其是下一章要介紹的Zygote,它更是Java世界的開創者。那麼,init程序是如何建立Zygote的呢?
· Android系統有很多屬性,於是init就提供了一個property service(屬性服務)來管理它們。那麼這個屬性服務是怎麼工作的呢?
如上所述,本章將通過下面兩方面內容來分析init:
· init如何建立zygote。
· init的屬性服務是如何工作的。
3.2 init分析
init程序的入口函式是main,它的程式碼如下所示:
[-->init.c]
int main(int argc, char **argv)
{
intdevice_fd = -1;
intproperty_set_fd = -1;
intsignal_recv_fd = -1;
intkeychord_fd = -1;
int fd_count;
ints[2];
intfd;
structsigaction act;
chartmp[PROP_VALUE_MAX];
structpollfd ufds[4];
char*tmpdev;
char*debuggable;
//設定子程序退出的訊號處理函式,該函式為sigchld_handler。
act.sa_handler = sigchld_handler;
act.sa_flags= SA_NOCLDSTOP;
act.sa_mask = 0;
act.sa_restorer = NULL;
sigaction(SIGCHLD, &act, 0);
......//建立一些資料夾,並掛載裝置,這些是和Linux相關的,不擬做過多討論。
mkdir("/dev/socket", 0755);
mount("devpts", "/dev/pts", "devpts", 0,NULL);
mount("proc", "/proc", "proc", 0, NULL);
mount("sysfs", "/sys", "sysfs", 0, NULL);
//重定向標準輸入/輸出/錯誤輸出到/dev/_null_。
open_devnull_stdio();
/*
設定init的日誌輸出裝置為/dev/__kmsg__,不過該檔案開啟後,會立即被unlink了,
這樣,其他程序就無法開啟這個檔案讀取日誌資訊了。
*/
log_init();
//上面涉及很多和Linux系統相關的知識,不熟悉的讀者可自行研究,它們不影響我們的分析
//解析init.rc配置檔案
parse_config_file("/init.rc");
......
//下面這個函式通過讀取/proc/cpuinfo得到機器的Hardware名,我的HTCG7手機為bravo。
get_hardware_name();
snprintf(tmp,sizeof(tmp), "/init.%s.rc", hardware);
//解析這個和機器相關的配置檔案,我的G7手機對應檔案為init.bravo.rc。
parse_config_file(tmp);
/*
解析完上述兩個配置檔案後,會得到一系列的Action(動作),下面兩句程式碼將執行那些處於
early-init階段的Action。init將動作執行的時間劃分為四個階段:early-init、init、
early-boot、boot。由於有些動作必須在其他動作完成後才能執行,所以就有了先後之分。哪些
動作屬於哪個階段由配置檔案決定。後面會介紹配置檔案的相關知識。
*/
action_for_each_trigger("early-init", action_add_queue_tail);
drain_action_queue();
/*
建立利用Uevent和Linux核心互動的socket。關於Uevent的知識,第9章中對
Vold進行分析時會做介紹。
*/
device_fd = device_init();
//初始化和屬性相關的資源
property_init();
//初始化/dev/keychord裝置,這和除錯有關,本書不討論它的用法。讀者可以自行研究,
//內容比較簡單。
keychord_fd = open_keychord();
......
/*
INIT_IMAGE_FILE定義為”/initlogo.rle”,下面這個函式將載入這個檔案作為系統的開機
畫面,注意,它不是開機動畫控制程式bootanimation載入的開機動畫檔案。
*/
if(load_565rle_image(INIT_IMAGE_FILE) ) {
/*
如果載入initlogo.rle檔案失敗(可能是沒有這個檔案),則會開啟/dev/ty0裝置,並
輸出”ANDROID”的字樣作為開機畫面。在模擬器上看到的開機畫面就是它。
*/
......
}
}
if(qemu[0])
import_kernel_cmdline(1);
......
//呼叫property_set函式設定屬性項,一個屬性項包括屬性名和屬性值。
property_set("ro.bootloader", bootloader[0] ? bootloader :"unknown");
......//執行位於init階段的動作
action_for_each_trigger("init", action_add_queue_tail);
drain_action_queue();
//啟動屬性服務
property_set_fd = start_property_service();
/*
呼叫socketpair函式建立兩個已經connect好的socket。socketpair是Linux的系統呼叫,
不熟悉的讀者可以利用man socketpair查詢相關資訊。後面就會知道它們的用處了。
*/
if(socketpair(AF_UNIX, SOCK_STREAM, 0, s) == 0) {
signal_fd = s[0];
signal_recv_fd = s[1];
......
}
......
//執行配置檔案中early-boot和boot階段的動作。
action_for_each_trigger("early-boot", action_add_queue_tail);
action_for_each_trigger("boot", action_add_queue_tail);
drain_action_queue();
......
//init關注來自四個方面的事情。
ufds[0].fd= device_fd;//device_fd用於監聽來自核心的Uevent事件
ufds[0].events = POLLIN;
ufds[1].fd = property_set_fd;//property_set_fd用於監聽來自屬性伺服器的事件
ufds[1].events= POLLIN;
//signal_recv_fd由socketpair建立,它的事件來自另外一個socket。
ufds[2].fd = signal_recv_fd;
ufds[2].events = POLLIN;
fd_count = 3;
if(keychord_fd > 0) {
//如果keychord裝置初始化成功,則init也會關注來自這個裝置的事件。
ufds[3].fd = keychord_fd;
ufds[3].events = POLLIN;
fd_count++;
}
......
#if BOOTCHART
......//與Boot char相關,不做討論了。
/*
Boot chart是一個小工具,它能對系統的效能進行分析,並生成系統啟動過程的圖表,
以提供一些有價值的資訊,而這些資訊最大的用處就是幫助提升系統的啟動速度。
*/
#endif
for(;;) {
//從此init將進入一個無限迴圈。
int nr, i, timeout = -1;
for (i = 0; i < fd_count; i++)
ufds[i].revents = 0;
//在迴圈中執行動作
drain_action_queue();
restart_processes(); //重啟那些已經死去的程序
......
#if BOOTCHART
...... // Boot Chart相關
#endif
//呼叫poll等待一些事情的發生
nr= poll(ufds, fd_count, timeout);
......
//ufds[2]儲存的是signal_recv_fd,用於接收來自socket的訊息。
if(ufds[2].revents == POLLIN) {
//有一個子程序去世,init要處理這個事情
read(signal_recv_fd, tmp, sizeof(tmp));
while (!wait_for_one_process(0))
;
continue;
}
if(ufds[0].revents == POLLIN)
handle_device_fd(device_fd);//處理Uevent事件
if(ufds[1].revents == POLLIN)
handle_property_set_fd(property_set_fd);//處理屬性服務的事件。
if(ufds[3].revents == POLLIN)
handle_keychord(keychord_fd);//處理keychord事件。
}
return0;
}
從上面的程式碼中可知,init的工作任務還是很重的。上面的程式碼雖已省略了不少行,可結果還是很長,不過從本章要分析的兩個知識點來看,可將init的工作流程精簡為以下四點:
· 解析兩個配置檔案,其中,將分析對init.rc檔案的解析。
· 執行各個階段的動作,建立Zygote的工作就是在其中的某個階段完成的。
· 呼叫property_init初始化屬性相關的資源,並且通過property_start_service啟動屬性服務。
· init進入一個無限迴圈,並且等待一些事情的發生。重點關注init如何處理來自socket和來自屬性伺服器相關的事情。
精簡工作流程,是以後分析程式碼時常用的方法。讀者在分析程式碼的過程中,也可使用這種方法。
3.2.1 解析配置檔案
根據上面的程式碼可知,在init中會解析兩個配置檔案,其中一個是系統配置檔案init.rc,另外一個是和硬體平臺相關的配置檔案。以HTC G7手機為例,這個配置檔名為init.bravo.rc,其中bravo是硬體平臺的名稱。對這兩個配置檔案進行解析,呼叫的是同一個parse_config_file函式。下面就來看這個函式,在分析過程中以init.rc為主。
[-->parser.c]
int parse_config_file(const char *fn)
{
char *data;
data = read_file(fn, 0);//讀取配置檔案的內容,這個檔案是init.rc。
if (!data) return -1;
parse_config(fn,data); //呼叫parse_config做真正的解析
return 0;
}
讀取完檔案的內容後,將呼叫parse_config進行解析,這個函式的程式碼如下所示:
[-->parser.c]
static void parse_config(const char *fn, char*s)
{
struct parse_state state;
char *args[SVC_MAXARGS];
int nargs;
nargs = 0;
state.filename = fn;
state.line = 1;
state.ptr = s;
state.nexttoken = 0;
state.parse_line = parse_line_no_op; //設定解析函式,不同的內容用不同的解析函式
for (;;) {
switch(next_token(&state)) {
case T_EOF:
state.parse_line(&state, 0, 0);
return;
caseT_NEWLINE:
if (nargs) {
//得到關鍵字的型別
int kw = lookup_keyword(args[0]);
if (kw_is(kw, SECTION)) { //判斷關鍵字型別是不是SECTION。
state.parse_line(&state,0, 0);
parse_new_section(&state,kw, nargs, args);//解析這個SECTION。
} else {
state.parse_line(&state, nargs, args);
}
nargs = 0;
}
break;
case T_TEXT:
......
break;
}
}
}
上面就是parse_config函式,程式碼雖短,實際卻比較複雜。從整體來說,parse_config首先會找到配置檔案的一個section,然後針對不同的 section使用不同的解析函式來解析。那麼,什麼是section呢?這和init.rc檔案的組織結構有關。先不必急著去看init.rc,還是先到程式碼中去尋找答案。
1. 關鍵字定義
keywords.h這個檔案定義了init中使用的關鍵字,它的用法很有意思,先來看這個檔案,程式碼如下所示:
[-->keywords.h]
#ifndef KEYWORD //如果沒有定義KEYWORD巨集,則走下面的分支
......//宣告一些函式,這些函式就是前面所說Action的執行函式。
int do_class_start(int nargs, char **args);
int do_class_stop(int nargs, char **args);
......
int do_restart(int nargs, char **args);
......
#define __MAKE_KEYWORD_ENUM__ //定義一個巨集
/*
定義KEYWORD巨集,雖然有四個引數,不過這裡只用第一個,其中K_##symbol中的##表示連線
的意思,即最後得到的值為K_symbol。symbol其實就是init.rc中的關鍵字
*/
#define KEYWORD(symbol, flags, nargs, func)K_##symbol,
enum { //定義一個列舉,這個列舉定義了各個關鍵字的列舉值。
K_UNKNOWN,
#endif
......
//根據上面KEYWORD的定義,這裡將得到一個列舉值K_class,
KEYWORD(class, OPTION, 0, 0)
KEYWORD(class_start, COMMAND, 1, do_class_start)//K_class_start,
KEYWORD(class_stop, COMMAND, 1, do_class_stop)//K_class_stop,
KEYWORD(on, SECTION, 0, 0)//K_on,
KEYWORD(oneshot, OPTION, 0, 0)
KEYWORD(onrestart, OPTION, 0, 0)
KEYWORD(restart, COMMAND, 1,do_restart)
KEYWORD(service, SECTION, 0,0)
......
KEYWORD(socket, OPTION, 0, 0)
KEYWORD(start, COMMAND, 1,do_start)
KEYWORD(stop, COMMAND, 1,do_stop)
......
#ifdef __MAKE_KEYWORD_ENUM__
KEYWORD_COUNT,
};
#undef __MAKE_KEYWORD_ENUM__
#undef KEYWORD //取消KEYWORD巨集定義
#endif
keywords.h好像沒什麼奇特,不過是個簡單的標頭檔案。為什麼說它的用法很有意思呢?來看程式碼中是如何使用它的,如下所示:
[-->parser.c]
......//parser.c中將包含keywords.h標頭檔案,而且還不只一次!!
//第一次包含keywords.h,根據keywords.h的程式碼,我們首先會得到一個列舉定義
#include "keywords.h"
/*
重新定義KEYWORD巨集,這回四個引數全用上了,看起來好像是一個結構體。其中#symbol表示
一個字串,其值為“symbol”。
*/
#define KEYWORD(symbol, flags, nargs, func) \
[K_##symbol ] = { #symbol, func, nargs + 1, flags, },
//定義一個結構體keyword_info陣列,它用來描述關鍵字的一些屬性,請注意裡面的註釋內容。
struct {
constchar *name; //關鍵字的名。
int(*func)(int nargs, char **args);//對應關鍵字的處理函式。
unsignedchar nargs;//引數個數,每個關鍵字的引數個數是固定的。
//關鍵字的屬性,有三種屬性,COMMAND、OPTION和SECTION。其中COMMAND有對應的處理函式
unsigned char flags;
} keyword_info[KEYWORD_COUNT] = {
[ K_UNKNOWN ] = { "unknown", 0, 0, 0},
/*
第二次包含keywords.h,由於已經重新定了KEYWORD巨集,所以以前那些作為列舉值的關鍵字
現在變成keyword_info陣列的索引了。
*/
#include "keywords.h"
};
#undef KEYWORD
//一些輔助巨集,幫助我們快速操作keyword_info中的內容。
#define kw_is(kw, type) (keyword_info[kw].flags& (type))
#define kw_name(kw) (keyword_info[kw].name)
#define kw_func(kw) (keyword_info[kw].func)
#define kw_nargs(kw) (keyword_info[kw].nargs)
現在領略了keywords.h的神奇之處了吧?原來它幹了兩件事情:
· 第一次包含keyworks.h時,它聲明瞭一些諸如do_classstart這樣的函式,另外還定義了一個列舉,列舉值為K_class,K_mkdir等關鍵字。
· 第二次包含keywords.h後,得到了一個keyword_info結構體陣列,這個keyword_info結構體陣列以前面定義的列舉值為索引,儲存對應的關鍵字資訊,這些資訊包括關鍵字名、處理函式、處理函式的引數個數,以及屬性。
目前,關鍵字資訊中最重要的就是symbol和flags了。什麼樣的關鍵字被認為是section呢?根據keywords.h的定義,symbol為下面兩個的關鍵字表示section:
KEYWORD(on, SECTION, 0, 0)
KEYWORD(service, SECTION, 0, 0)
有了上面的知識,再來看配置檔案init.rc的內容。
2. init.rc的解析
init.rc的內容如下所示:(我們截取了部分內容,注意,其中的註釋符號是#。)
[-->init.rc]
on init #根據上面的分析,on關鍵字標示一個section,對應的名字是”init”
...... #下面所有的內容都屬於這個section,直到下一個section開始時。
exportPATH /sbin:/system/sbin:/system/bin:/system/xbin
exportLD_LIBRARY_PATH /system/lib
exportANDROID_BOOTLOGO 1 #根據keywords.h的定義,export表示一個COMMAND
export ANDROID_ROOT /system
exportANDROID_ASSETS /system/app
...... #省略部分內容
on boot #這是一個新的section,名為”boot”
ifup lo#這是一個COMMAND
hostname localhost
domainname localdomain
......
#class_start也是一個COMMAND,對應函式為do_class_start,很重要,切記。
class_startdefault
......
#下面這個section的意思是:待屬性persist.service.adb.enable的值變為1後,
#需要執行對應的COMMAND,這個COMMAND是start adbd
onproperty:persist.service.adb.enable=1
start adbd //start是一個COMMAND
on property:persist.service.adb.enable=0
stopadbd
......
#service也是section的標示,對應section的名為“zygote“
service zygote /system/bin/app_process -Xzygote/system/bin –zygote \
--start-system-server
socketzygote stream 666 #socket關鍵字表示OPTION
onrestart write /sys/android_power/request_state wake #onrestart也是OPTION
onrestart write /sys/power/state on
onrestart restart media
#一個section,名為”media”
service media /system/bin/mediaserver
usermedia
groupsystem audio camera graphics inet net_bt net_bt_admin net_raw
iopriort 4
從上面對init.rc的分析中可知:
· 一個section的內容從這個標示section的關鍵字開始,到下一個標示section的地方結束。
· init.rc中出現了名為boot和init的section,這裡的boot和init,就是前面介紹的動作執行四個階段中的boot和init。也就是說,在boot階段執行的動作都是由boot這個section定義的。
另外還可發現,zygote被放在了一個servicesection中。下面以zygote這個section為例,介紹service是如何解析的。
3.2.2 解析service
zygote對應的service section內容是:
[-->init.rc::zygote]
service zygote /system/bin/app_process -Xzygote/system/bin –zygote \ --start-system-server
socketzygote stream 666 #socket是OPTION
#下面的onrestart是OPTION,而write和restart是COMMAND
onrestartwrite /sys/android_power/request_state wake
onrestart write /sys/power/state on
onrestartrestart media
解析section的入口函式是parse_new_section,它的程式碼如下所示:
[-->parser.c]
void parse_new_section(struct parse_state*state, int kw,
int nargs, char **args)
{
switch(kw) {
caseK_service: //解析service,用parse_service和parse_line_service
state->context = parse_service(state, nargs, args);
if(state->context) {
state->parse_line = parse_line_service;
return;
}
break;
caseK_on: //解析on section
......//讀者可以自己研究
break;
}
state->parse_line = parse_line_no_op;
}
其中,service解析時,用到了parse_service和parse_line_service兩個函式,在分別介紹它們之前,先看init是如何組織這個service的。
1. service結構體
init中使用了一個叫service的結構體來儲存和service section相關的資訊,不妨來看這個結構體,程式碼如下所示:
[-->init.h::service結構體定義]
struct service {
//listnode是一個特殊的結構體,在核心程式碼中用得非常多,主要用來將結構體連結成一個
//雙向連結串列。init中有一個全域性的service_list,專門用來儲存解析配置檔案後得到的service。
struct listnode slist;
constchar *name; //service的名字,對應我們這個例子就是”zygote”。
constchar *classname; //service所屬class的名字,預設是”defult”
unsigned flags;//service的屬性
pid_tpid; //程序號
time_ttime_started; //上一次啟動的時間
time_ttime_crashed; //上一次死亡的時間
intnr_crashed; //死亡次數
uid_tuid; //uid,gid相關
gid_tgid;
gid_tsupp_gids[NR_SVC_SUPP_GIDS];
size_tnr_supp_gids;
/*
有些service需要使用socket,下面這個socketinfo用來描述socket的相關資訊。
我們的zygote也使用了socket,配置檔案中的內容是socket zygote stream 666。
它表示將建立一個AF_STREAM型別的socket(其實就是TCP socket),該socket的名為“zygote”,
讀寫許可權是666。
*/
structsocketinfo *sockets;
//service一般執行在單獨的一個程序中,envvars用來描述建立這個程序時所需的環境變數資訊。
structsvcenvinfo *envvars;
/*
雖然關鍵字onrestart標示一個OPTION,可是這個OPTION後面一般跟著COMMAND,
下面這個action結構體可用來儲存command資訊,馬上就會分析到它。
*/
structaction onrestart;
//和keychord相關的內容
int*keycodes;
intnkeycodes;
intkeychord_id;
//io優先順序設定
intioprio_class;
intioprio_pri;
//引數個數
intnargs;
//用於儲存引數
char*args[1];
};
我們現在已瞭解的service的結構體,相對來說還算是清晰易懂的。而zygote中的那三個onrestart該怎麼表示呢?請看service中使用的這個action結構體:
[-->init.h::action結構體定義]
struct action {
/*
一個action結構體可存放在三個雙向連結串列中,其中alist用於儲存所有action,
qlist用於連結那些等待執行的action,tlist用於連結那些待某些條件滿足後
就需要執行的action。
*/
structlistnode alist;
structlistnode qlist;
structlistnode tlist;
unsigned hash;
constchar *name;
//這個OPTION對應的COMMAND連結串列,以zygote為例,它有三個onrestart option,所以
//它對應會建立三個command結構體。
structlistnode commands;
structcommand *current;
};
瞭解了上面的知識後,你是否能猜到parse_service和parse_line_service的作用了呢?馬上就來看它們。
2. parse_service
parse_service的程式碼如下所示:
[-->parser.c]
static void *parse_service(struct parse_state*state, int nargs, char **args)
{
structservice *svc; //宣告一個service結構體
......
//init維護了一個全域性的service連結串列,先判斷是否已經有同名的service了。
svc =service_find_by_name(args[1]);
if(svc) {
...... //如果有同名的service,則不能繼續後面的操作。
return 0;
}
nargs-= 2;
svc =calloc(1, sizeof(*svc) + sizeof(char*) * nargs);
......
svc->name = args[1];
svc->classname= "default";//設定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);
//把zygote這個service加到全域性連結串列service_list中。
list_add_tail(&service_list, &svc->slist);
returnsvc;
}
parse_service函式只是搭建了一個service的架子,具體的內容尚需由後面的解析函式來填充。來看service的另外一個解析函式parse_line_service。
3. parse_line_service
parse_line_service的程式碼如下所示:
[-->parser.c]
static void parse_line_service(structparse_state *state, int nargs,
char **args)
{
structservice *svc = state->context;
structcommand *cmd;
int i,kw, kw_nargs;
......
svc->ioprio_class = IoSchedClass_NONE;
//其實還是根據關鍵字來做各種處理。
kw =lookup_keyword(args[0]);
switch(kw) {
caseK_capability:
break;
caseK_class:
if(nargs != 2) {
......
}else {
svc->classname = args[1];
}
break;
......
caseK_oneshot:
/*
這是service的屬性,它一共有五個屬性,分別為:
SVC_DISABLED:不隨class自動啟動。下面將會看到class的作用。
SVC_ONESHOT:退出後不需要重啟,也就是這個service只啟動一次就可以了。
SVC_RUNNING:正在執行,這是service的狀態。
SVC_RESTARTING:等待重啟,這也是service的狀態。
SVC_CONSOLE:該service需要使用控制檯 。
SVC_CRITICAL:如果在規定時間內該service不斷重啟,則系統會重啟並進入恢復模式。
zygote沒有使用任何屬性,這表明它:會隨著class的處理自動啟動;
退出後會由init重啟;不使用控制檯;即使不斷重啟也不會導致系統進入恢復模式。
*/
svc->flags |= SVC_ONESHOT;
break;
caseK_onrestart: //根據onrestart的內容,填充action結構體的內容
nargs--;
args++;
kw= lookup_keyword(args[0]);
......
//建立command結構體
cmd = malloc(sizeof(*cmd) + sizeof(char*) * nargs);
cmd->func = kw_func(kw);
cmd->nargs = nargs;
memcpy(cmd->args, args, sizeof(char*) * nargs);
//把新建的command加入到雙向連結串列中。
list_add_tail(&svc->onrestart.commands, &cmd->clist);
break;
......
caseK_socket: { //建立socket相關資訊
struct socketinfo *si;
......
si= calloc(1, sizeof(*si));
if(!si) {
parse_error(state, "out of memory\n");
break;
}
si->name = args[1]; //socket的名字
si->type = args[2]; //socket的型別
si->perm = strtoul(args[3], 0, 8); //socket的讀寫許可權
if(nargs > 4)
si->uid = decode_uid(args[4]);
if(nargs > 5)
si->gid = decode_uid(args[5]);
si->next = svc->sockets;
svc->sockets = si;
break;
}
......
default:
parse_error(state, "invalid option '%s'\n", args[0]);
}
}
parse_line_service將根據配置檔案的內容填充service結構體,那麼,zygote解析完後會得到什麼呢?圖3-1表示了zygote解析後的結果:
圖3-1 zygote解析結果示意圖
從上圖中可知:
· service_list連結串列將解析後的service全部連結到了一起,並且是一個雙向連結串列,前向節點用prev表示,後向節點用next表示。
· socketinfo也是一個雙向連結串列,因為zygote只有一個socket,所以畫了一個虛框socket做為連結串列的示範。
· onrestart通過commands指向一個commands連結串列,zygote有三個commands。
zygote這個service解析完了,現在就是“萬事俱備,只欠東風”了。接下來要了解的是,init是如何控制service的。
3.2.3 init控制service
先看service是如何啟動的。
1.啟動zygote
init.rc中有這樣一句話:
#class_start是一個COMMAND,對應的函式為do_class_start,很重要,切記。
class_startdefault
class_start標示一個COMMAND,對應的處理函式為do_class_start,它位於boot section的範圍內。為什麼說它很重要呢?
還記得init程序中的四個執行階段嗎?當init程序執行到下面幾句話時,do_class_start就會被執行了。
//將bootsection節的command加入到執行佇列
action_for_each_trigger("boot",action_add_queue_tail);
//執行佇列裡的命令,class可是一個COMMAND,所以它對應的do_class_start會被執行。
drain_action_queue();
下面來看do_class_start函式:
[-->builtins.c]
int do_class_start(int nargs, char **args)
{
/*
args為do_class_start的引數,init.rc中只有一個引數,就是default。
下面這個函式將從service_list中尋找classname為”default”的service,然後
呼叫service_start_if_not_disabled函式。現在讀者明白了service結構體中
classname的作用了嗎?
*/
service_for_each_class(args[1],service_start_if_not_disabled);
return 0;
}
我們已經知道,zygote這個service的classname的值就是“default”,所以會針對這個service呼叫service_start_if_not_disabled,這個函式的程式碼是:
[-->parser.c]
static void service_start_if_not_disabled(structservice *svc)
{
if (!(svc->flags & SVC_DISABLED)) {
service_start(svc,NULL); //zygote可沒有設定SVC_DISABLED
}
}
service_start函式的程式碼如下所示:
[-->init.c]
void service_start(struct service *svc, constchar *dynamic_args)
{
structstat s;
pid_tpid;
intneeds_console;
int n;
svc->flags &= (~(SVC_DISABLED|SVC_RESTARTING));
svc->time_started = 0;
if(svc->flags & SVC_RUNNING) {
return;//如果這個service已在執行,則不用處理
}
/*
service一般運行於另外一個程序中,這個程序也是init的子程序,所以啟動service前需要判斷
對應的可執行檔案是否存在,zygote對應的可執行檔案是/system/bin/app_process
*/
if(stat(svc->args[0], &s) != 0) {
svc->flags |= SVC_DISABLED;
return;
}
......
pid =fork(); //呼叫fork建立子程序
if(pid == 0) {
//pid為零,我們在子程序中
struct socketinfo *si;
struct svcenvinfo *ei;
char tmp[32];
int fd, sz;
//得到屬性儲存空間的資訊並加到環境變數中,後面在屬性服務一節中會碰到使用它的地方。
get_property_workspace(&fd, &sz);
add_environment("ANDROID_PROPERTY_WORKSPACE", tmp);
//新增環境變數資訊
for (ei = svc->envvars; ei; ei = ei->next)
add_environment(ei->name, ei->value);
//根據socketinfo建立socket
for (si = svc->sockets; si; si = si->next) {
int s = create_socket(si->name,
!strcmp(si->type,"dgram") ?
SOCK_DGRAM :SOCK_STREAM,
si->perm,si->uid, si->gid);
if (s >= 0) {
//在環境變數中新增socket資訊。
publish_socket(si->name, s);
}
}
......//設定uid,gid等
setpgid(0, getpid());
if(!dynamic_args) {
/*
執行/system/bin/app_process,這樣就進入到app_process的main函式中了。
fork、execve這兩個函式都是Linux系統上常用的系統呼叫。
*/
if (execve(svc->args[0], (char**)svc->args, (char**) ENV) < 0) {
......
}
}else {
......
}
......//父程序init的處理,設定service的資訊,如啟動時間、程序號,以及狀態等。
svc->time_started = gettime();
svc->pid = pid;
svc->flags |= SVC_RUNNING;
//每一個service都有一個屬性,zygote的屬性為init.svc.zygote,現在設定它的值為running
notify_service_state(svc->name, "running");
}
原來,zygote是通過fork和execv共同建立的!但service結構中的那個onrestart好像沒有派上用場,原因何在?
2. 重啟zygote
根據名字,就可猜到onrestart應該是在zygote重啟時用的。下面先看在zygote死後,它的父程序init會有什麼動作:
[-->init.c]
static void sigchld_handler(int s)
{ //當子程序退出時,init的這個訊號處理函式會被呼叫
write(signal_fd, &s, 1); //往signal_fd write資料
}
signal_fd,就是在init中通過socketpair建立的兩個socket中的一個,既然會往這個signal_fd中傳送資料,那麼另外一個socket就一定能接收到,這樣就會導致init從poll函式中返回:
[-->init.rc::main函式程式碼片斷]
nr =poll(ufds, fd_count, timeout);
......
if(ufds[2].revents == POLLIN) {
read(signal_recv_fd, tmp, sizeof(tmp));
while (!wait_for_one_process(0))//呼叫wait_for_one_process函式處理
;
continue;
}
......
//直接看這個wait_for_one_process函式:
static int wait_for_one_process(int block)
{
pid_tpid;
intstatus;
structservice *svc;
structsocketinfo *si;
time_tnow;
structlistnode *node;
structcommand *cmd;
while( (pid = waitpid(-1, &status, block ? 0 : WNOHANG)) == -1 &&
errno == EINTR );
if(pid <= 0) return -1;
//找到死掉的那個service,現在應該找到了代表zygote的那個service。
svc = service_find_by_pid(pid);
......
if(!(svc->flags & SVC_ONESHOT)) {
//殺掉zygote建立的所有子程序,這就是zygote死後,Java世界崩潰的原因。
kill(-pid, SIGKILL);
}
//清理socket資訊,不清楚的讀者可以通過命令man 7 AF_UNIX查詢一下相關知識。
for(si = svc->sockets; si; si = si->next) {
char tmp[128];
snprintf(tmp, sizeof(tmp), ANDROID_SOCKET_DIR"/%s",si->name);
unlink(tmp);
}
svc->pid = 0;
svc->flags &= (~SVC_RUNNING);
if(svc->flags & SVC_ONESHOT) {
svc->flags |= SVC_DISABLED;
}
......
now= gettime();
/*
如果設定了SVC_CRITICAL標示,則4分鐘內該服務重啟次數不能超過4次,否則
機器會重啟進入recovery模式。根據init.rc的配置,只有servicemanager程序
享有此種待遇。
*/
if(svc->flags & SVC_CRITICAL) {
if(svc->time_crashed + CRITICAL_CRASH_WINDOW >= now) {
if (++svc->nr_crashed > CRITICAL_CRASH_THRESHOLD) {
......
sync();
__reboot(LINUX_REBOOT_MAGIC1,LINUX_REBOOT_MAGIC2,
LINUX_REBOOT_CMD_RESTART2, "recovery");
return 0;
}
}else {
svc->time_crashed = now;
svc->nr_crashed = 1;
}
}
svc->flags |= SVC_RESTARTING;
//設定標示為SVC_RESTARTING,然後執行該service onrestart中的COMMAND,這些內容就
//非常簡單了,讀者可以自行學習。
list_for_each(node, &svc->onrestart.commands) {
cmd = node_to_item(node, struct command, clist);
cmd->func(cmd->nargs, cmd->args);
}
//設定init.svc.zygote的值為restarting。
notify_service_state(svc->name, "restarting");
return0;
}
通過上面的程式碼,可知道onrestart的作用了,但zygote本身又在哪裡重啟的呢?答案就在下面的程式碼中:
[-->init.c::main函式程式碼片斷]
for(;;) {
int nr, i, timeout = -1;
for (i = 0; i < fd_count; i++)
ufds[i].revents = 0;
drain_action_queue(); //poll函式返回後,會進入下一輪的迴圈
restart_processes(); //這裡會重啟所有flag標誌為SVC_RESTARTING的service。
......
}
這樣,zygote又回來了!
3.2.4 屬性服務
我們知道,Windows平臺上有一個叫登錄檔的東西。登錄檔可以儲存一些類似key/value的鍵值對。一般而言,系統或某些應用程式會把自己的一些屬性儲存在登錄檔中,即使下次系統重啟或應用程式重啟,它還能夠根據之前在登錄檔中設定的屬性,進行相應的初始化工作。Android平臺也提供了一個型別機制,可稱之為屬性服務(property service)。應用程式可通過這個屬性機制,查詢或設定屬性。讀者可以用adb shell登入到真機或模擬器上,然後用getprop命令檢視當前系統中有哪些屬性。即如我的HTC G7測試結果,如圖3-2所示:(圖中只顯示了部分屬性)
圖3-2 HTC G7屬性示意圖
這個屬性服務是怎麼實現的呢?下面來看程式碼,其中與init.c和屬性服務有關的程式碼有下面兩行:
property_init();
property_set_fd = start_property_service();
分別來看看它們。
1. 屬性服務初始化
(1)建立儲存空間
先看property_init函式,程式碼如下所示:
[-->property_service.c]
void property_init(void)
{
init_property_area();//初始化屬性儲存區域
//載入default.prop檔案
load_properties_from_file(PROP_PATH_RAMDISK_DEFAULT);
}
在properyty_init函式中,先呼叫init_property_area函式,建立一塊用於儲存屬性的儲存區域,然後載入default.prop檔案中的內容。再看init_property_area是如何工作的,它的程式碼如下所示:
[-->property_service.c]
static int init_property_area(void)
{
prop_area *pa;
if(pa_info_array)
return -1;
/*
初始化儲存空間,PA_SIZE是這塊儲存空間的總大小,為32768位元組,pa_workspace
為workspace型別的結構體,下面是它的定義:
typedef struct {
void *data; //儲存空間的起始地址
size_tsize; //儲存空間的大小
int fd; //共享記憶體的檔案描述符
} workspace;
init_workspace函式呼叫Android系統提供的ashmem_create_region函式建立一塊
共享記憶體。關於共享記憶體的知識我們在第7章會接觸,這裡,只需把它當做一塊普通的記憶體就
可以了。
*/
if(init_workspace(&pa_workspace, PA_SIZE))
return -1;
fcntl(pa_workspace.fd, F_SETFD, FD_CLOEXEC);
//在32768個位元組的儲存空間中,有PA_INFO_START(1024)個位元組用來儲存頭部資訊
pa_info_array = (void*) (((char*) pa_workspace.data) + PA_INFO_START);
pa =pa_workspace.data;
memset(pa, 0, PA_SIZE);
pa->magic = PROP_AREA_MAGIC;
pa->version = PROP_AREA_VERSION;
//__system_property_area__這個變數由bionic libc庫輸出,有什麼用呢?
__system_property_area__ = pa;
return0;
}
上面的內容比較簡單,不過最後的賦值語句可是大有來頭。__system_property_area__是bionic libc庫中輸出的一個變數,為什麼這裡要給它賦值呢?
原來,雖然屬性區域是由init程序建立,但Android系統希望其他程序也能讀取這塊記憶體裡的東西。為做到這一點,它便做了以下兩項工作:
· 把屬性區域建立在共享記憶體上,而共享記憶體是可以跨程序的。這一點,已經在上面的程式碼中見到了,init_workspace函式內部將建立這個共享記憶體。
· 如何讓其他程序知道這個共享記憶體呢?Android利用了gcc的constructor屬性,這個屬性指明瞭一個__libc_prenit函式,當bionic libc庫被載入時,將自動呼叫這個__libc_prenit,這個函式內部就將完成共享記憶體到本地程序的對映工作。
(2)客戶端程序獲取儲存空間
關於上面的內容,來看相關程式碼:
[-->libc_init_dynamic.c]
//constructor屬性指示載入器載入該庫後,首先呼叫__libc_prenit函式。這一點和Windows上
//動態庫的DllMain函式類似
void __attribute__((constructor))__libc_prenit(void);
void __libc_prenit(void)
{
......
__libc_init_common(elfdata); //呼叫這個函式
......
}
__libc_init_common函式為:
[-->libc_init_common.c]
void __libc_init_common(uintptr_t *elfdata)
{
......
__system_properties_init();//初始化客戶端的屬性儲存區域
}
[-->system_properties.c]
int __system_properties_init(void)
{
prop_area *pa;
int s,fd;
unsigned sz;
char*env;
.....
//還記得在啟動zygote一節中提到的新增環境變數的地方嗎?屬性儲存區域的相關資訊
//就是在那兒新增的,這裡需要取出來使用了。
env =getenv("ANDROID_PROPERTY_WORKSPACE");
//取出屬性儲存區域的檔案描述符。關於共享記憶體的知識,第7章中將會進行介紹。
fd =atoi(env);
env =strchr(env, ',');
if(!env) {
return -1;
}
sz =atoi(env + 1);
//對映init建立的那塊記憶體到本地程序空間,這樣本地程序就可以使用這塊共享記憶體了。
//注意,對映的時候指定了PROT_READ屬性,所以客戶端程序只能讀屬性,而不能設定屬性。
pa =mmap(0, sz, PROT_READ, MAP_SHARED, fd, 0);
if(pa== MAP_FAILED) {
return -1;
}
if((pa->magic != PROP_AREA_MAGIC) || (pa->version !=PROP_AREA_VERSION)) {
munmap(pa, sz);
return -1;
}
__system_property_area__ = pa;
return0;
}
上面程式碼中很多地方和共享記憶體有關,在第7章中會對與共享記憶體有關問題進行介紹,讀者也可先行學習有關共享記憶體的知識。
總之,通過這種方式,客戶端程序可以直接讀取屬性空間,但沒有許可權設定屬性。客戶端程序又是如何設定屬性呢?
2. 啟動屬性伺服器
(1)啟動屬性伺服器
init程序會啟動一個屬性伺服器,而客戶端只能通過和屬性伺服器互動才能設定屬性。先來看屬性伺服器的內容,它由start_property_service函式啟動,程式碼如下所示:
[-->Property_servie.c]
int start_property_service(void)
{
intfd;
/*
載入屬性檔案,其實就是解析這些檔案中的屬性,然後把它設定到屬性空間中去。Android系統
一共提供了四個儲存屬性的檔案,它們分別是:
#definePROP_PATH_RAMDISK_DEFAULT "/default.prop"
#define PROP_PATH_SYSTEM_BUILD "/system/build.prop"
#define PROP_PATH_SYSTEM_DEFAULT "/system/default.prop"
#define PROP_PATH_LOCAL_OVERRIDE "/data/local.prop"
*/
load_properties_from_file(PROP_PATH_SYSTEM_BUILD);
load_properties_from_file(PROP_PATH_SYSTEM_DEFAULT);
load_properties_from_file(PROP_PATH_LOCAL_OVERRIDE);
//有一些屬性是需要儲存到永久介質上的,這些屬性檔案則由下面這個函式載入,這些檔案
//儲存在/data/property目錄下,並且這些檔案的檔名必須以persist.開頭。這個函式
//很簡單,讀者可自行研究。
load_persistent_properties();
//建立一個socket,用於IPC通訊。
fd =create_socket(PROP_SERVICE_NAME, SOCK_STREAM, 0666, 0, 0);
if(fd< 0) return -1;
fcntl(fd, F_SETFD, FD_CLOEXEC);
fcntl(fd, F_SETFL, O_NONBLOCK);
listen(fd, 8);
returnfd;
}
屬性服務建立了一個用來接收請求的socket,可這個請求在哪裡被處理呢?事實上,在init中的for迴圈那裡已經進行相關處理了。
(2)處理設定屬性請求
接收請求的地方是在init程序中,程式碼如下所示:
[-->init.c::main函式片斷]
if (ufds[1].revents == POLLIN)
handle_property_set_fd(property_set_fd);
當屬性伺服器收到客戶端請求時,init會呼叫handle_property_set_fd進行處理。這個函式的程式碼如下所示:
[-->property_service.c]
void handle_property_set_fd(int fd)
{
prop_msg msg;
int s;
int r;
intres;
structucred cr;
structsockaddr_un addr;
socklen_t addr_size = sizeof(addr);
socklen_t cr_size = sizeof(cr);
//先接收TCP連線
if ((s= accept(fd, (struct sockaddr *) &addr, &addr_size)) < 0) {
return;
}
//取出客戶端程序的許可權等屬性。
if(getsockopt(s, SOL_SOCKET, SO_PEERCRED, &cr, &cr_size) < 0) {
......
return;
}
//接收請求資料
r = recv(s,&msg, sizeof(msg), 0);
close(s);
......
switch(msg.cmd) {
casePROP_MSG_SETPROP:
msg.name[PROP_NAME_MAX-1] = 0;
msg.value[PROP_VALUE_MAX-1] = 0;
/*
如果是ctl開頭的訊息,則認為是控制訊息,控制訊息用來執行一些命令,例如用
adb shell登入後,輸入setprop ctl.start bootanim就可以檢視開機動畫了,
關閉的話就輸入setpropctl.stop bootanim,是不是很有意思呢?
*/
if(memcmp(msg.name,"ctl.",4) == 0) {
if (check_control_perms(msg.value, cr.uid, cr.gid)) {
handle_control_message((char*) msg.name + 4, (char*) msg.value);
}
......
}else {
//檢查客戶端程序是否有足夠的許可權
if (check_perms(msg.name, cr.uid, cr.gid)) {
//然後呼叫property_set設定。
property_set((char*) msg.name, (char*) msg.value);
}
......
}
break;
default:
break;
}
}
當客戶端的許可權滿足要求時,init就呼叫property_set進行相關處理,這個函式比較簡單,程式碼如下所示:
[-->property_service.c]
int property_set(const char *name, const char*value)
{
prop_area *pa;
prop_info *pi;
intnamelen = strlen(name);
intvaluelen = strlen(value);
......
//從屬性儲存空間中尋找是否已經存在該屬性
pi =(prop_info*) __system_property_find(name);
if(pi!= 0) {
//如果屬性名以ro.開頭,則表示是隻讀的,不能設定,所以直接返回。
if(!strncmp(name, "ro.", 3)) return -1;
pa= __system_property_area__;
//更新該屬性的值
update_prop_info(pi, value, valuelen);
pa->serial++;
__futex_wake(&pa->serial, INT32_MAX);
}else {
//如果沒有找到對應的屬性,則認為是增加屬性,所以需要新建立一項。注意,Android支援
//最多247項屬性,如果目前屬性的儲存空間中已經有247項,則直接返回。
pa= __system_property_area__;
if(pa->count == PA_COUNT_MAX) return -1;
pi= pa_info_array + pa->count;
pi->serial = (valuelen << 24);
memcpy(pi->name, name, namelen + 1);
memcpy(pi->value, value, valuelen +1);
pa->toc[pa->count] =
(namelen << 24) | (((unsigned) pi) - ((unsigned) pa));
pa->count++;
pa->serial++;
__futex_wake(&pa->serial, INT32_MAX);
}
//有一些特殊的屬性需要特殊處理,這裡,主要是以net.change開頭的屬性。
if(strncmp("net.", name, strlen("net.")) == 0) {
if(strcmp("net.change", name) == 0) {
return 0;
}
property_set("net.change", name);
} elseif (persistent_properties_loaded &&
strncmp("persist.", name,strlen("persist.")) == 0) {
//如果屬性名以persist.開頭,則需要把這些值寫到對應檔案中去。
write_persistent_property(name, value);
}
/*
還記得init.rc中的下面這句話嗎?
on property:persist.service.adb.enable=1
startadbd
當persist.service.adb.enable屬性置為1後,就會執行start adbd這個command,
這是通過property_changed函式來完成的,它非常簡單,讀者可以自己閱讀。
*/
property_changed(name, value);
return0;
}
好,屬性服務端的工作已經瞭解了,下面看客戶端是如何設定屬性的。
(3)客戶端傳送請求
客戶端通過property_set傳送請求,property_set由libcutils庫提供,程式碼如下所示:
[-->properties.c]
int property_set(const char *key, const char*value)
{
prop_msg msg;
unsigned resp;
......
msg.cmd = PROP_MSG_SETPROP;//設定訊息碼為PROP_MSG_SETPROP。
strcpy((char*) msg.name, key);
strcpy((char*) msg.value, value);
//傳送請求
returnsend_prop_msg(&msg);
}
static int send_prop_msg(prop_msg *msg)
{
int s;
int r;
//建立和屬性伺服器的socket連線
s =socket_local_client(PROP_SERVICE_NAME,
ANDROID_SOCKET_NAMESPACE_RESERVED,
SOCK_STREAM);
if(s< 0) return -1;
//通過socket傳送出去
while((r = send(s, msg, sizeof(prop_msg), 0)) < 0) {
if((errno == EINTR) || (errno == EAGAIN)) continue;
break;
}
if(r== sizeof(prop_msg)) {
r= 0;
} else{
r= -1;
}
close(s);
returnr;
}
至此,屬性伺服器就介紹完了。總體來說,還算比較簡單。
3.3 本章小結
本章講解了init程序如何解析zygote,以及屬性伺服器的工作原理,旨在幫助讀者認識這個天字號第一程序。從整體來說,init.rc的解析難度相對最大。相信讀者通過以上例項分析,已經理解了init.rc的解析原理。另外,inti涉及很多和Linux系統相關的知識,有興趣的讀者可以自行研究。