1. 概述
init做了很多事情,在很多修改驅動,或者對系統作一些修改的時候,經常會對init.rc進行修改,因此,無論做系統研究,還是作平臺性的修改,這部分都很重要。另外,《深入理解》是根據2.3的版本寫的代碼分析,我再根據4.0相應的代碼做了類似的分析和修改。
這章主要講了兩個方面,init如何創建zygote和init的屬性服務是如何工作的。我個人最大的收穫,是系統地讀了下init的工作流程,當然,原來看過對init.rc的語法分析的東西,相對有點幫助,可以看得稍微快一點。
init的源文件多放在system/core/init下, init.rc放在system/core/rootdir/init.rc下,當然,不同平臺(device)的init.rc放在device/下。
2. init 分析
整理init.c 裏的main函數,按順序做了下面這些事情(沒有一件一件羅列,只是把關注的和主要的列了下):
a. 建立一些根目錄下必需的目錄,其他目錄從init.rc中讀取建立 ——>
b. 重定向標註輸入/輸出/錯誤輸出到/dev/_null_ (open_devnull_stdio()) ——>
c. 設置init的日誌輸出設備(klog_fd)爲/dev/__kmsg__,設置完後馬上unlink,其他進程就無法打開這個文件讀取日誌信息了(klog_init())——>
d. 分析init.rc以及init.hardware.rc得到一系列的action(parse_config_file())——>
e. 把early-init階段的action加到全局定義的action_queue (init_parser.c) 裏——>
f. 通過調用queue_builtin_action()把wait_for_coldboot_done_action, property_init_action, keychord_init_action, logo_init_action, console_init_action, set_init_propertyes_action加到action_queue 裏 ——>
g. 把init段的action加到action_queue裏(action_for_each_trigger("init", action_add_queue_tail);)——>
h. 把property_service_init_action, signal_init_action, check_startup_action, 加到action_queue 裏 ——>
i. 如果爲charger模式,則把charger加到action_queue裏,否則把early-boot, boot加到action_queue裏 ——>
j. 把queue_property_triggers_action加到action_queue裏,否則把early-boot, boot加到action_queue裏 ——>
k. 進入無限for循環,通過執行execute_one_command()按序執行action_queue裏的action,每執行完一個action,就會調用一次restart_processes(), (如果action_queue裏的action已經完成,則不做action,只是不斷監聽事件,作爲“天字一號”程序運行),只是如下:
- <span style="font-size:16px;"> <span style="font-size:12px;">execute_one_command();
- restart_processes();</span>
- </span>
並且根據需要來設置ufds[], 分別監聽來自屬性服務器,由soketpair創建的另一個socket,keychord設備這三個事件
- <span style="font-size:12px;"> if (!property_set_fd_init && get_property_set_fd() > 0) {
- ufds[fd_count].fd = get_property_set_fd();
- ufds[fd_count].events = POLLIN;
- ufds[fd_count].revents = 0;
- fd_count++;
- property_set_fd_init = 1;
- }
- if (!signal_fd_init && get_signal_fd() > 0) {
- ufds[fd_count].fd = get_signal_fd();
- ufds[fd_count].events = POLLIN;
- ufds[fd_count].revents = 0;
- fd_count++;
- signal_fd_init = 1;
- }
- if (!keychord_fd_init && get_keychord_fd() > 0) {
- ufds[fd_count].fd = get_keychord_fd();
- ufds[fd_count].events = POLLIN;
- ufds[fd_count].revents = 0;
- fd_count++;
- keychord_fd_init = 1;
- }</span>
- <span style="font-size:12px;"> nr = poll(ufds, fd_count, timeout);
- if (nr <= 0)
- continue;
- 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();
- }
- }</span>
由上面的分析可以知道,其實e到f的步驟都只是按照一定的執行順序把init.rc裏的action加到action_queue裏,然後在for循環裏一邊執行一邊監聽3類事件,等所有action完成後,繼續一直停留在for循環裏監聽3類事件。各個section的工作順序可以從這裏看得出來。跟2.3不一樣的地方是,2.3是先做完early-init action, 再做property_init, 再加載開機第一畫面,再做init action,然後啓動屬性服務器,然後建立sockpair對, 然後執行early-boot 和 boot action,再打開3類監聽器,然後進入for循環,執行其他aciton, 並監聽3類事件。在4.0裏,開機畫面加載(load_565rle_image(INIT_IMAGE_FILE))放在console_init_action裏,然後所有的action都放在for循環中執行。
2.1 解析配置文件
init的main函數裏調用init_parse_config_file來處理init.rc或和硬件平臺相關的init.**.rc, init_parse_config()主要調用了parse_config()來分析,如下:
- <span style="font-size:12px;">static void parse_config(const char *fn, char *s)
- {
- struct parse_state state;
- char *args[INIT_PARSER_MAXARGS];
- int nargs;
- nargs = 0;
- state.filename = fn;
- state.line = 0;
- 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;
- case T_NEWLINE:
- state.line++;
- if (nargs) {
- int kw = lookup_keyword(args[0]);
- if (kw_is(kw, SECTION)) {
- state.parse_line(&state, 0, 0);
- parse_new_section(&state, kw, nargs, args);
- } else {
- state.parse_line(&state, nargs, args);
- }
- nargs = 0;
- }
- break;
- case T_TEXT:
- if (nargs < INIT_PARSER_MAXARGS) {
- args[nargs++] = state.text;
- }
- break;
- }
- }
- }</span>
其中的lookupkeyword在下面的2.1.1會詳細講到。找到keyword後,如果不是section屬性,則調用parse_new_line(), 調用parse_new_section(), parse_new_section()分析了關鍵字 on, service, import三個屬性的section,如下:
- <span style="font-size:12px;">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:
- if (nargs != 2) {
- ERROR("single argument needed for import\n");
- } else {
- int ret = init_parse_config_file(args[1]);
- if (ret)
- ERROR("could not import file %s\n", args[1]);
- }
- }
- state->parse_line = parse_line_no_op;
- }</span>
其中service是系統服務,on是section的起始標誌,import是引進別的類似init.rc(這是加入硬件平臺的init.rc的另外一種常用方法)。
2.1.1 關鍵字定義
關鍵字定義在system/core/init/keywords.h文件裏,文件首先定義了action實際執行的函數,部分代碼:
- <span style="font-size:12px;">int do_class_start(int nargs, char **args);
- int do_class_stop(int nargs, char **args);
- int do_class_reset(int nargs, char **args);
- int do_domainname(int nargs, char **args);
- int do_exec(int nargs, char **args);
- int do_export(int nargs, char **args);
- int do_hostname(int nargs, char **args);
- int do_ifup(int nargs, char **args);
- int do_insmod(int nargs, char **args);</span>
然後通過##來定義一些keyword枚舉,如:
- <span style="font-size:12px;">KEYWORD(group, OPTION, 0, 0)
- KEYWORD(hostname, COMMAND, 1, do_hostname)
- KEYWORD(ifup, COMMAND, 1, do_ifup)
- KEYWORD(insmod, COMMAND, 1, do_insmod)
- KEYWORD(import, SECTION, 1, 0)
- KEYWORD(keycodes, OPTION, 0, 0)
- KEYWORD(mkdir, COMMAND, 1, do_mkdir)
- KEYWORD(mount, COMMAND, 3, do_mount)
- KEYWORD(on, SECTION, 0, 0)
- KEYWORD(oneshot, OPTION, 0, 0)
- KEYWORD(onrestart, OPTION, 0, 0)
- KEYWORD(restart, COMMAND, 1, do_restart)
- KEYWORD(rm, COMMAND, 1, do_rm)
- KEYWORD(rmdir, COMMAND, 1, do_rmdir)
- KEYWORD(service, SECTION, 0, 0)
- KEYWORD(setenv, OPTION, 2, 0)</span>
- <span style="font-size:12px;">#define SECTION 0x01
- #define COMMAND 0x02
- #define OPTION 0x04
- #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
- #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)</span>
第一次包含keywords.h時,它聲明瞭一些諸如do_class_start的函數,另外還定義了一個枚舉,枚舉值爲K_class,K_mkdir等關鍵字。第二次包含keywords.h後,得到了keyword_info的結構體數組,以前面的枚舉值爲索引,存儲對應的關鍵字信息,包括關鍵字名稱,處理函數,處理函數的參數個數,以及屬性。
2.3中,只有當symbol爲on和service爲SECTION,4.0中增加了import也爲section,用於添加分析新的init.rc設置。
2.2.2 解析init.rc
粘貼部分init.rc如下:
- <span style="font-size:12px;">on early-init
- # Set init and its forked children's oom_adj.
- write /proc/1/oom_adj -16
- start ueventd
- # create mountpoints
- mkdir /mnt 0775 root system
- on init
- sysclktz 0
- loglevel 3
- # setup the global environment
- export PATH /sbin:/vendor/bin:/system/sbin:/system/bin:/system/xbin
- export LD_LIBRARY_PATH /vendor/lib:/system/lib
- export ANDROID_BOOTLOGO 1
- export ANDROID_ROOT /system
- ......
- on fs
- # mount mtd partitions
- # Mount /system rw first to give the filesystem a chance to save a checkpoint
- mount yaffs2 mtd@system /system
- mount yaffs2 mtd@system /system ro remount
- mount yaffs2 mtd@userdata /data nosuid nodev
- mount yaffs2 mtd@cache /cache nosuid nodev
- on post-fs
- # once everything is setup, no need to modify /
- mount rootfs rootfs / ro remount
- # We chown/chmod /cache again so because mount is run as root +
- ......
- ## Daemon processes to be run by init.
- ##
- service ueventd /sbin/ueventd
- class core
- critical
- service console /system/bin/sh
- class core
- console
- disabled
- user shell
- group log
- on property:ro.debuggable=1
- start console
- # adbd is controlled via property triggers in init.<platform>.usb.rc
- service adbd /sbin/adbd
- class core
- disabled
- # adbd on at boot in emulator
- on property:ro.kernel.qemu=1
- start adbd</span>
2.2 解析 service
2.2.1 service 結構體
以zygote爲例(雖然zygote是虛擬機的始祖,但是如果不是系統的重度開發者,是不會涉及到zygote的修改吧,所以我的關注點並不是zygote本身,而是透過書上的這個例子看一下典型的service的啓動過程,當然,zygote的崩潰導致死機或者系統重啓的現象和原因,有助於當產線上老化機器出現死機或者系統自動重啓問題時候,對問題的debug),看一下service到底是怎麼一回事,先看下init.rc裏關於zygote的設置:
- <span style="font-size:12px;">service zygote /system/bin/app_process -Xzygote /system/bin --zygote --start-system-server
- class main
- socket zygote stream 666
- onrestart write /sys/android_power/request_state wake
- onrestart write /sys/power/state on
- onrestart restart media
- onrestart restart netd</span>
上面裏,service是一個section的開始,zygote是service的名字,/system/bin/app_process是zygote對應的可執行文件,後面是執行參數。下面都是OPTION。class main 讓service的calssname屬性爲main, 這是4.0的新特性,2.3中這個屬性默認爲default,以在do_class_start()中默認啓動服務。socket是建立名字爲zygote,類型爲stream,讀寫權限是666的socket。onrestart是一個屬性,這個屬性附加了4個command。parse_new_section()通過parse_service()生成service結構體,查找是否衝突,否則把下圖的service結構體加入到鏈表service_list(), 並且把service的classname屬性設置爲“default”,以保證在do_class_start中被匹配啓動。返回新生成的結構體。然後,通過調用parse_line_service(),讀取service的各種屬性,生成socket(如果需要),填入service結構體,生成的service機構體如下:
a. servide_list鏈表將解析後的service全部鏈接到了一起,並且是一個雙向鏈表,前向節點用prev表示,後向節點用next表示。
b. socketinfo也是一個雙向鏈表,因爲zygote只有一個socket,所以畫了一個虛框socket作爲鏈表的示範。
c. onrestart通過commands指向一個commands鏈表, zygote有4個commands, 圖中按照2.3來只畫了3個。
2.3 init控制service
上面講了init.rc的service是如何匹配到service結構體,下面講一下parse_new_section()後,init是怎麼啓動service的。
2.3.1 啓動 zygote
init.rc中有COMMAND class_start的時候,會執行do_class_start的函數,2.3中class_start 只會有一個default參數,會把所有classname爲default的service啓動(通常是放在boot section), 4.0中class_start加了core, main, charger, late_start這4個類型。do_class_start()中調用service_start_if_not_disabled(),
再調用service_start(),用fork() + execve()來創建子進程並執行service指定的可執行文件來終於啓動了service。這個過程會根據一些service的屬性,例如SVC_DISABLED等來決定是否進行啓動service。
2.3.2 重啓 zygote
在signal_init()(函數在core/init/signal_handle.c,通過queue_builtin_action(signal_init_action, "signal_init");加入到init的action_list中)函數中,通過socketpair()把signal_fd和signal_recv_fd配對。當子進程退出時,會執行sigchld_handle()函數,往signal_fd寫數據,而signal_init()調用的handle_signal()函數通過讀取signal_recv_fd,調用wait_for_one_process(),這個函數通過Pid找到對應的service,然後根據SVC_ONESHOT,
SVC_RESET, SVC_DISABLED等的值,來決定如何處理。需要關注的是,如果設置了SVC_ONESHOT,那麼會調用kill(-pid, SIGKELL)把所有該Pid創建的所有子進程都殺死,zygote就是設置了這個屬性。如果設置了SVC_CRITICAL,那麼4分鐘內該服務的次數不能超過4次,否則機器會在重啓時進入recovery模式。像servicemanager就有這個設置。如果SVC_RESTARTING,那麼屬性onrestart的command就會執行。
2.4 屬性服務
property service, 屬性服務,類似於windows的註冊表屬性機制,通過getprop可以查看當前屬性。property主要設計到property_init()和property_set_fd = start_property_service()兩個函數。
2.4.1 屬性服務初始化
- <span style="font-size:12px;">void property_init(bool load_defaults)
- {
- init_property_area();
- if (load_defaults)
- load_properties_from_file(PROP_PATH_RAMDISK_DEFAULT);
- }</span>
a.把屬性區域創建在共享內存上,而共享內存是可以跨進程的。
b. 爲了讓其他進程知道這個共享內存,Android利用了gcc的constructor屬性, 這個屬性指明瞭一個__libc_prenit函數(void __attribute__((constructor)) __libc_prenit(void)),當bionic libc庫被加載時,將自動調用這個__libc_prenit,這個函數內部就將完成共享內存到本地進程的映射工作。
c. constructor屬性指示加載器加載該庫後,首先調用__libc_prenit函數,和Windows上的動態庫的DllMain函數類似。__libc_prenit()調用__libc_init_common()再調用__system_properties_init(),用mmap把ANDROID_PROPERTY_WORKSPACE對應的內存以只讀的形式映射到本地。
2.4.2 屬性服務器的分析
start_property_service()加載 /default.prop, /system/build.prop, /system/default.prop, /data/local.prop。對於保存在永久介質上的屬性文件,在/data/property目錄下,並且這些文件的文件名必須以persist.開頭。當屬性服務器收到客戶端請求時,init會調用handle_property_set_fd進行處理。客戶端則通過property_set(由libcutils庫提供)來向服務器發送設置屬性的請求。
3.小結
這一章詳細分析了init.rc中的各項關鍵字,瞭解了SECTION,COMMAND,OPTION的意思,還有service是怎樣啓動的,對於增減驅動或者修改一些系統屬性時修改init.rc是個啓發性的閱讀。另外,這一章會設計到一些socket編程的概念。