Android的init過程(二):初始化語言(init.rc)解析
本文使用的軟件版本
Android:4.2.2
Linux內核:3.1.10
本文及后續幾篇文章將對Android的初始化(init)過程進行詳細地、剝絲抽繭式地分析,並且在其中穿插了大量的知識,希望對讀者了解Android的啟動過程又所幫助。本章主要介紹了與硬件相關初始化文件名的確定以及屬性服務的原理和實現。
Android本質上就是一個基於Linux內核的操作系統。與Ubuntu Linux、Fedora Linux類似。只是Android在應用層專門為移動設備添加了一些特有的支持。既然Android是Linux內核的系統,那么基本的啟動過程也應符合Linux的規則。如果研究過其他Linux系統應該了解,一個完整的Linux系統首先會將一個Linux內核裝載到內存,也就是編譯Linux內核源代碼生成的bzImage文件,對於為Android優化的Linux內核源代碼會生成zImage文件。該文件就是Linux內核的二進制版本。由於zImage在內核空間運行,而我們平常使用的軟件都是在應用空間運行(關於內核空間和應用空間的詳細描述,可以參考《Android深度探索(卷1):HAL與驅動開發》一書的內容,在后續的各卷中將會對Android的整體體系進行全方位的剖析)。內核空間和應用空間是不能直接通過內存地址級別訪問的,所以就需要建立某種通訊機制。
目前Linux有很多通訊機制可以在用戶空間和內核空間之間交互,例如設備驅動文件(位於/dev目錄中)、內存文件(/proc、/sys目錄等)。了解Linux的同學都應該知道Linux的重要特征之一就是一切都是以文件的形式存在的,例如,一個設備通常與一個或多個設備文件對應。這些與內核空間交互的文件都在用戶空間,所以在Linux內核裝載完,需要首先建立這些文件所在的目錄。而完成這些工作的程序就是本文要介紹的init。Init是一個命令行程序。其主要工作之一就是建立這些與內核空間交互的文件所在的目錄。當Linux內核加載完后,要做的第一件事就是調用init程序,也就是說,init是用戶空間執行的第一個程序。
在分析init的核心代碼之前,還需要初步了解init除了建立一些目錄外,還做了如下的工作
1. 初始化屬性
2. 處理配置文件的命令(主要是init.rc文件),包括處理各種Action。
3. 性能分析(使用bootchart工具)。
4. 無限循環執行command(啟動其他的進程)。
盡管init完成的工作不算很多,不過代碼還是非常復雜的。Init程序並不是由一個源代碼文件組成的,而是由一組源代碼文件的目標文件鏈接而成的。這些文件位於如下的目錄。
<Android源代碼本目錄>/system/core/init
其中init.c是init的主文件,現在打開該文件,看看其中的內容。由於init是命令行程序,所以分析init.c首先應從main函數開始,現在好到main函數,代碼如下:
int main(int argc, char **argv) { int fd_count = 0; struct pollfd ufds[4]; char *tmpdev; char* debuggable; char tmp[32]; int property_set_fd_init = 0; int signal_fd_init = 0; int keychord_fd_init = 0; bool is_charger = false; if (!strcmp(basename(argv[0]), "ueventd")) return ueventd_main(argc, argv); if (!strcmp(basename(argv[0]), "watchdogd")) return watchdogd_main(argc, argv); /* clear the umask */ umask(0); // 下面的代碼開始建立各種用戶空間的目錄,如/dev、/proc、/sys等 mkdir("/dev", 0755); mkdir("/proc", 0755); mkdir("/sys", 0755); mount("tmpfs", "/dev", "tmpfs", MS_NOSUID, "mode=0755"); mkdir("/dev/pts", 0755); mkdir("/dev/socket", 0755); mount("devpts", "/dev/pts", "devpts", 0, NULL); mount("proc", "/proc", "proc", 0, NULL); mount("sysfs", "/sys", "sysfs", 0, NULL); /* 檢測/dev/.booting文件是否可讀寫和創建*/ close(open("/dev/.booting", O_WRONLY | O_CREAT, 0000)); open_devnull_stdio(); klog_init(); // 初始化屬性 property_init(); get_hardware_name(hardware, &revision); // 處理內核命令行 process_kernel_cmdline(); … … is_charger = !strcmp(bootmode, "charger"); INFO("property init\n"); if (!is_charger) property_load_boot_defaults(); INFO("reading config file\n"); // 分析/init.rc文件的內容 init_parse_config_file("/init.rc"); … …// 執行初始化文件中的動作 action_for_each_trigger("init", action_add_queue_tail); // 在charger模式下略過mount文件系統的工作 if (!is_charger) { action_for_each_trigger("early-fs", action_add_queue_tail); action_for_each_trigger("fs", action_add_queue_tail); action_for_each_trigger("post-fs", action_add_queue_tail); action_for_each_trigger("post-fs-data", action_add_queue_tail); } queue_builtin_action(property_service_init_action, "property_service_init"); queue_builtin_action(signal_init_action, "signal_init"); queue_builtin_action(check_startup_action, "check_startup"); if (is_charger) { action_for_each_trigger("charger", action_add_queue_tail); } else { action_for_each_trigger("early-boot", action_add_queue_tail); action_for_each_trigger("boot", action_add_queue_tail); } /* run all property triggers based on current state of the properties */ queue_builtin_action(queue_property_triggers_action, "queue_property_triggers"); #if BOOTCHART queue_builtin_action(bootchart_init_action, "bootchart_init"); #endif // 進入無限循環,建立init的子進程(init是所有進程的父進程) for(;;) { int nr, i, timeout = -1; // 執行命令(子進程對應的命令) execute_one_command(); restart_processes(); 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; } if (process_needs_restart) { timeout = (process_needs_restart - gettime()) * 1000; if (timeout < 0) timeout = 0; } if (!action_queue_empty() || cur_action) timeout = 0; // bootchart是一個性能統計工具,用於搜集硬件和系統的信息,並將其寫入磁盤,以便其 // 他程序使用 #if BOOTCHART if (bootchart_count > 0) { if (timeout < 0 || timeout > BOOTCHART_POLLING_MS) timeout = BOOTCHART_POLLING_MS; if (bootchart_step() < 0 || --bootchart_count == 0) { bootchart_finish(); bootchart_count = 0; } } #endif // 等待下一個命令的提交 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(); } } } return 0; }
我們可以看到main函數是非常復雜的,不過我們也不需要每條語句都弄得非常清楚(因為這樣弄是非常困難的),通常只需要了解init的主線即可。其實從init的main函數可以看出。Init實際上就分為如下兩部分。
1. 初始化(包括建立/dev、/proc等目錄、初始化屬性、執行init.rc等初始化文件中的action等)。
2. 使用for循環無限循環建立子進程。
第一項工作很好理解。而第二項工作是init中的核心。在Linux系統中init是一切應用空間進程的父進程。所以我們平常在Linux終端執行的命令,並建立進程。實際上都是在這個無限的for循環中完成的。也就是說,在Linux終端執行ps –e 命令后,看到的所有除了init外的其他進程,都是由init負責創建的。而且init也會常駐內容。當然,如果init掛了,Linux系統基本上就崩潰了。
由於init比較復雜,所以本文只分析其中的一部分,在后續文章中將詳細分析init的各個核心組成部分。
對於main函數最開始完成的建立目錄的工作比較簡單,這部分也沒什么可以分析的。就是調用了一些普通的API(mkdir)建立一些目錄。現在說一些題外話,由於Android的底層源代碼(包括init)實際上是屬於Linux應用編程領域,所以要想充分理解Android源代碼,除了Linux的基本結構要了解外,Linux應用層的API需要熟悉。為了滿足這些讀者的需要,后續我會寫一些關於Linux應用編程的文章。Ok,現在言歸正傳,接下來分析一個比較重要的部分:配置文件的解析。
這里的配置文件主要指init.rc。讀者可以進到Android的shell,會看到根目錄有一個init.rc文件。該文件是只讀的,即使有了root權限,可以修改該文件也沒有。因為我們在根目錄看到的文件只是內存文件的鏡像。也就是說,android啟動后,會將init.rc文件裝載到內存。而修改init.rc文件的內容實際上只是修改內存中的init.rc文件的內容。一旦重啟android,init.rc文件的內容又會恢復到最初的裝載。想徹底修改init.rc文件內容的唯一方式是修改Android的ROM中的內核鏡像(boot.img)。其實boot.img名曰內核鏡像,不過該文件除了包含完整的Linux內核文件(zImage)外,還包括另外一個鏡像文件(ramdisk.img)。ramdisk.img就包含了init.rc文件和init命令。所以只有修改ramdisk.img文件中的init.rc文件,並且重新打包boot.img文件,並刷機,才能徹底修改init.rc文件。如果讀者有Android源代碼,編譯后,就會看到out目錄中的相關子目錄會生成一個root目錄,該目錄實際上就是ramdisk.img解壓后的內容。會看到有init命令和init.rc文件。在后續的文章中將會討論具體如何修改init.rc文件,如何刷機。不過這些內容與本文關系不大,所以不做詳細的討論。
現在回到main函數,在創建完目錄后,會看到執行了如下3個函數。
property_init();
get_hardware_name(hardware, &revision);
process_kernel_cmdline();
其中property_init主要是為屬性分配一些存儲空間,該函數並不是核心。不過當我們查看init.rc文件時會發現該文件開始部分用一些import語句導入了其他的配置文件,例如,/init.usb.rc。大多數配置文件都直接使用了確定的文件名,只有如下的代碼使用了一個變量(${ro.hardware})執行了配置文件名的一部分。那么這個變量值是從哪獲得的呢?
import /init.${ro.hardware}.rc
首先要了解init.${ro.hardware}.rc配置文件的內容通常與當前的硬件有關。現在我們先來關注get_hardware_name函數,代碼如下:
void get_hardware_name(char *hardware, unsigned int *revision) { char data[1024]; int fd, n; char *x, *hw, *rev; /* 如果hardware已經有值了,說明hardware通過內核命令行提供,直接返回 */ if (hardware[0]) return; // 打開/proc/cpuinfo文件 fd = open("/proc/cpuinfo", O_RDONLY); if (fd < 0) return; // 讀取/proc/cpuinfo文件的內容 n = read(fd, data, 1023); close(fd); if (n < 0) return; data[n] = 0; // 從/proc/cpuinfo文件中獲取Hardware字段的值 hw = strstr(data, "\nHardware"); rev = strstr(data, "\nRevision"); // 成功獲取Hardware字段的值 if (hw) { x = strstr(hw, ": "); if (x) { x += 2; n = 0; while (*x && *x != '\n') { if (!isspace(*x)) // 將Hardware字段的值都轉換為小寫,並更新hardware參數的值 // hardware也就是在init.c文件中定義的hardware數組 hardware[n++] = tolower(*x); x++; if (n == 31) break; } hardware[n] = 0; } } if (rev) { x = strstr(rev, ": "); if (x) { *revision = strtoul(x + 2, 0, 16); } } }
從get_hardware_name方法的代碼可以得知,該方法主要用於確定hardware和revision的變量的值。Revision這里先不討論,只要研究hardware。獲取hardware的來源是從Linux內核命令行或/proc/cpuinfo文件中的內容。Linux內核命令行暫且先不討論(因為很少傳遞該值),先看看/proc/cpuinfo,該文件是虛擬文件(內存文件),執行cat /proc/cpuinfo命令會看到該文件中的內容,如圖1所示。在白框中就是Hardware字段的值。由於該設備是Nexus 7,所以值為grouper。如果程序就到此位置,那么與硬件有關的配置文件名是init.grouper.rc。有Nexus 7的讀者會看到在根目錄下確實有一個init.grouper.rc文件。說明Nexus 7的原生ROM並沒有在其他的地方設置配置文件名,所以配置文件名就是從/proc/cpuinfo文件的Hardware字段中取的值。
圖1
現在來看在get_hardware_name函數后面調用的process_kernel_cmdline函數,代碼如下:
static void process_kernel_cmdline(void) { /* don't expose the raw commandline to nonpriv processes */ chmod("/proc/cmdline", 0440); // 導入內核命令行參數 import_kernel_cmdline(0, import_kernel_nv); if (qemu[0]) import_kernel_cmdline(1, import_kernel_nv); // 用屬性值設置內核變量 export_kernel_boot_props(); }
在process_kernel_cmdline函數中除了使用import_kernel_cmdline函數導入內核變量外,主要的功能就是調用export_kernel_boot_props函數通過屬性設置內核變量,例如,通過ro.boot.hardware屬性設置hardware變量,也就是說可以通過ro.boot.hardware屬性值可以修改get_hardware_name函數中從/proc/cpuinfo文件中得到的hardware字段值。下面看一下export_kernel_boot_props函數的代碼。
static void export_kernel_boot_props(void) { char tmp[PROP_VALUE_MAX]; const char *pval; unsigned i; struct { const char *src_prop; const char *dest_prop; const char *def_val; } prop_map[] = { { "ro.boot.serialno", "ro.serialno", "", }, { "ro.boot.mode", "ro.bootmode", "unknown", }, { "ro.boot.baseband", "ro.baseband", "unknown", }, { "ro.boot.bootloader", "ro.bootloader", "unknown", }, }; // 通過內核的屬性設置應用層配置文件的屬性 for (i = 0; i < ARRAY_SIZE(prop_map); i++) { pval = property_get(prop_map[i].src_prop); property_set(prop_map[i].dest_prop, pval ?: prop_map[i].def_val); } // 根據ro.boot.console屬性的值設置console變量 pval = property_get("ro.boot.console"); if (pval) strlcpy(console, pval, sizeof(console)); /* save a copy for init's usage during boot */ strlcpy(bootmode, property_get("ro.bootmode"), sizeof(bootmode)); /* if this was given on kernel command line, override what we read * before (e.g. from /proc/cpuinfo), if anything */ // 獲取ro.boot.hardware屬性的值 pval = property_get("ro.boot.hardware"); if (pval)
// 這里通過ro.boot.hardware屬性再次改變hardware變量的值 strlcpy(hardware, pval, sizeof(hardware)); // 利用hardware變量的值設置設置ro.hardware屬性
// 這個屬性就是前面提到的設置初始化文件名的屬性,實際上是通過hardware變量設置的
property_set("ro.hardware", hardware); snprintf(tmp, PROP_VALUE_MAX, "%d", revision); property_set("ro.revision", tmp); /* TODO: these are obsolete. We should delete them */ if (!strcmp(bootmode,"factory")) property_set("ro.factorytest", "1"); else if (!strcmp(bootmode,"factory2")) property_set("ro.factorytest", "2"); else property_set("ro.factorytest", "0"); }
從export_kernel_boot_props函數的代碼可以看出,該函數實際上就是來回設置一些屬性值,並且利用某些屬性值修改console、hardware等變量。其中hardware變量(就是一個長度為32的字符數組)在get_hardware_name函數中已經從/proc/cpuinfo文件中獲得過一次值了,在export_kernel_boot_props函數中又通過ro.boot.hardware屬性設置了一次值,不過在Nexus 7中並沒有設置該屬性,所以hardware的值仍為grouper。最后用hardware變量設置ro.hardware屬性,所以最后的初始化文件名為init.grouper.rc。
這里還有一個問題,前面多次提到屬性或屬性文件,那么這些屬性文件指的是什么呢?是init.rc?當然不是。實際上這些屬性文件是一些列位於不同目錄,系統依次讀取的配置文件。
屬性服務(Property Service)
在研究這些配置文件之前應先了解init是如何處理這些屬性的。編寫過Windows本地應用的讀者都應了解,在windows中有一個注冊表機制,在注冊表中提供了大量的屬性。在Linux中也有類似的機制,這就是屬性服務。init在啟動的過程中會啟動屬性服務(Socket服務),並且在內存中建立一塊存儲區域,用來存儲這些屬性。當讀取這些屬性時,直接從這一內存區域讀取,如果修改屬性值,需要通過Socket連接屬性服務完成。在init.c文件中的一個action函數中調用了start_property_service函數來啟動屬性服務,action是init.rc及其類似文件中的一種執行機制,由於內容比較多,所以關於init.rc文件中的執行機制將在下一篇文章中詳細討論。
現在順藤摸瓜,找到start_property_service函數,該函數在Property_service.c文件中,該文件與init.c文件中同一個目錄。
void start_property_service(void) { int fd; // 裝載不同的屬性文件 load_properties_from_file(PROP_PATH_SYSTEM_BUILD); load_properties_from_file(PROP_PATH_SYSTEM_DEFAULT); load_override_properties(); /* Read persistent properties after all default values have been loaded. */ load_persistent_properties(); // 創建socket服務(屬性服務) fd = create_socket(PROP_SERVICE_NAME, SOCK_STREAM, 0666, 0, 0); if(fd < 0) return; fcntl(fd, F_SETFD, FD_CLOEXEC); fcntl(fd, F_SETFL, O_NONBLOCK); // 開始服務監聽 listen(fd, 8); property_set_fd = fd; }
現在我們已經知道屬性服務的啟動方式了,那么在start_property_service函數中還涉及到如下兩個宏。
PROP_PATH_SYSTEM_BUILD
PROP_PATH_SYSTEM_DEFAULT
這兩個宏都是系統預定義的屬性文件名的路徑。為了獲取這些宏的定義,我們先進行另外一個函數的分析。
在前面讀取屬性值時使用過一個property_get函數,該函數在Property_service.c中實現,代碼如下:
const char* property_get(const char *name) { prop_info *pi; if(strlen(name) >= PROP_NAME_MAX) return 0; pi = (prop_info*) __system_property_find(name); if(pi != 0) { return pi->value; } else { return 0; } }
可以看到,在property_get函數中調用了一個核心函數__system_property_find,該函數真正實現了獲取屬性值的功能。該函數屬於bionic的一個library,在system_properties.c文件中實現,讀者可以在如下的目錄找到該文件。
<Android源代碼根目錄>/bionic/libc/bionic
__system_property_find函數的代碼如下:
const prop_info *__system_property_find(const char *name) { // 獲取屬性存儲內存區域的首地址 prop_area *pa = __system_property_area__; unsigned count = pa->count; unsigned *toc = pa->toc; unsigned len = strlen(name); prop_info *pi; while(count--) { unsigned entry = *toc++; if(TOC_NAME_LEN(entry) != len) continue; pi = TOC_TO_INFO(pa, entry); if(memcmp(name, pi->name, len)) continue; return pi; } return 0; }
從__system_property_find函數的代碼很容易看出,第一行使用了一個__system_property_area__變量,該變量是全局的。在前面分析main函數時涉及到一個property_init函數,該函數調用了init_property_area函數,該函數用於初始化屬性內存區域,也就是__system_property_area__變量。
static int init_property_area(void) { prop_area *pa; if(pa_info_array) return -1; if(init_workspace(&pa_workspace, PA_SIZE)) return -1; fcntl(pa_workspace.fd, F_SETFD, FD_CLOEXEC); 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__ = pa; property_area_inited = 1; return 0; }
在前面涉及到的system_properties.c文件對應的頭文件system_properties.h中定義了前面提到的兩個表示屬性文件路徑的宏,其實還有另外兩個表示路徑的宏,一共4個屬性文件。system_properties.h文件可以在<Android源代碼根目錄>/bionic/libc/include/sys目錄中找到。這4個宏定義如下:
#define PROP_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"
現在讀者可以進入Android設備的相應目錄,通常可以找到上述4個文件,如一般會在根目錄,會發現一個default.prop文件,cat default.prop會看到該文件的內容。而屬性服務就是裝載所有這4個屬性文件中的所有屬性以及使用property_set設置的屬性。在Android設備的終端可以直接使用getprop命令從屬性服務獲取所有的屬性值。如圖2所示。getprop命令還可以直接根屬性名還獲取具體的屬性值,例如,getprop ro.build.product。
圖2
如果讀者感興趣,可以看一下getprop是如何通過屬性服務讀寫屬性的。getprop命令的源代碼文件是getprop.c。讀者可以在<Android源代碼根目錄>/system/core/toolbox目錄中找到該文件。實際上,getprop獲取屬性值也是通過property_get函數完成的。在前面分析過該函數,實際上調用了__system_property_find函數從__system_property_area__變量指定的內存區域獲取相應的屬性值。
此外在system_properties.c文件中還有如下兩個函數用於通過屬性服務修改或添加某個屬性的值。
static int send_prop_msg(prop_msg *msg) { struct pollfd pollfds[1]; struct sockaddr_un addr; socklen_t alen; size_t namelen; int s; int r; int result = -1; // 創建用於連接屬性服務的socket s = socket(AF_LOCAL, SOCK_STREAM, 0); if(s < 0) { return result; } memset(&addr, 0, sizeof(addr)); // property_service_socket是Socket設備文件名稱 namelen = strlen(property_service_socket); strlcpy(addr.sun_path, property_service_socket, sizeof addr.sun_path); addr.sun_family = AF_LOCAL; alen = namelen + offsetof(struct sockaddr_un, sun_path) + 1; if(TEMP_FAILURE_RETRY(connect(s, (struct sockaddr *) &addr, alen)) < 0) { close(s); return result; } r = TEMP_FAILURE_RETRY(send(s, msg, sizeof(prop_msg), 0)); if(r == sizeof(prop_msg)) { pollfds[0].fd = s; pollfds[0].events = 0; r = TEMP_FAILURE_RETRY(poll(pollfds, 1, 250 /* ms */)); if (r == 1 && (pollfds[0].revents & POLLHUP) != 0) { result = 0; } else { result = 0; } } close(s); return result; } // 用戶可以直接調用該函數設置屬性值 int __system_property_set(const char *key, const char *value) { int err; int tries = 0; int update_seen = 0; prop_msg msg; if(key == 0) return -1; if(value == 0) value = ""; if(strlen(key) >= PROP_NAME_MAX) return -1; if(strlen(value) >= PROP_VALUE_MAX) return -1; memset(&msg, 0, sizeof msg); msg.cmd = PROP_MSG_SETPROP; strlcpy(msg.name, key, sizeof msg.name); strlcpy(msg.value, value, sizeof msg.value); // 設置屬性值 err = send_prop_msg(&msg); if(err < 0) { return err; } return 0; }
在send_prop_msg函數中涉及到一個property_service_socket變量,定義如下:
static const char property_service_socket[] = "/dev/socket/" PROP_SERVICE_NAME;
實際上,send_prop_msg通過這個設備文件與屬性服務通訊的。讀者可以在Android設備的終端進入/dev/socket目錄,通常會看到一個property_service文件,該文件就是屬性服務映射的設備文件。
現在已經分析完了init如何確定與硬件相關的初始化文件名(init.grouper.rc),並且討論了4個屬性文件及其裝載過程,以及屬性服務實現的基本原理。在下一篇文章中將討論更深入的內容,例如,init.rc文件中提供了很多action,那么什么是aciton呢,init有是如何解析init.rc文件呢?這些內容都將在下一篇文章中揭曉。