標籤:android init
功能概述
init進程是Android核心啟動的第一個進程,其進程號(pid)為1,是Android系統所有進程的祖先,因此它肩負著系統啟動的重要責任。Android的init原始碼位於system/core/init/目錄下,伴隨Android系統多個版本的迭代,init原始碼也幾經重構。
目前Android4.4原始碼中,init目錄編譯後產生如下Android系統的三個檔案,分別是
- /init
- /sbin/ueventd-->/init
- /sbin/watchdogd-->/init
其中ueventd與wathdogd均是指向/init的軟連結。(具體實現請閱讀init/Android.mk)。
在Android系統早期版本(2.2之前)只有init進程,Android2.2中將建立裝置驅動節點檔案功能獨立到ueventd進程完成,在Android4.1中則添加了watchdogd。
/init主要完成三大功能:
- 解析init.rc初始化Android屬性系統,並維護屬性服務
- 初始化Android屬性系統,並維護屬性服務
- 處理子進程啟動、停止、重啟動
/ueventd用於建立裝置驅動節點。/watchdogd 是看門狗服務進程。
程式碼分析分析代碼當先抓住主幹,瞭解其大致結構與流程,再逐塊深入,分析其實現細節。這樣先大局再細節的方法可以讓我們在閱讀代碼時保持頭腦的清醒,切忌不可在沒有對整體流程瞭解的情況下深入細節,那很容易導致我們迷失在代碼森林中。
接下來分析init.c的main函數。為了方便分析,將main函數代碼做了精簡,代碼如下。
int main(int argc, char **argv){ //<part 1> if (!strcmp(basename(argv[0]), "ueventd")) return ueventd_main(argc, argv); if (!strcmp(basename(argv[0]), "watchdogd")) return watchdogd_main(argc, argv); //<part2> umask(0); 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); .... open_devnull_stdio(); klog_init(); property_init(); .... //<part3> INFO("reading config file\n"); init_parse_config_file("/init.rc"); ... action_for_each_trigger("early-init", action_add_queue_tail); .... queue_builtin_action(queue_property_triggers_action, "queue_property_triggers"); //<part4> for(;;) { ... execute_one_command(); restart_processes(); .... 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函數分為上述4個部分,對應part1到part4,下面分別做具體說明。
代碼<part1>通過命令列判斷argv[0]的字串內容,來區分當前程式是init,ueventd或是watchdogd。
C程式的main函數原型為 main(int argc, char* argv[]), ueventd以及watchdogd的啟動都在init.rc中描述,由init進程解析後執行fork、exec啟動,因此其入口參數的構造在init代碼中,將在init.rc解析時分析。此時我們只需要直到argv[0]中將儲存可執行檔的名字。
代碼<part2>
umaks(0)用於設定當前進程(即/init)的檔案模型建立掩碼(file mode creation mask),注意這裡的檔案是廣泛意義上的檔案,包括普通檔案、目錄、連結檔案、裝置節點等。
PS. 以上解釋摘自umask的mannual,可在linux系統中執行man 3 umask查看。
Linux C庫中mkdir與open的函數運行如下。
int mkdir(const char *pathname, mode_t mode);
int open(const char *pathname, int flags, mode_t mode);
Linux核心給每一個進程都設定了一個掩碼,當程式調用open、mkdir等函數建立檔案或目錄時,傳入open的mode會現在掩碼做運算,得到的檔案mode,才是檔案真正的mode。
譬如要建立一個目錄,並設定它的檔案許可權為0777,
mkdir("testdir", 0777)
但實際上寫入的檔案許可權卻未必是777,因為mkdir系統調用在建立testdir時,會將0777與當前進程的掩碼(稱為umask)運算,具體運算方法為 0777&~umask作為testdir的真正許可權。因此上述init中首先調用umask(0)將進程掩碼清0,這樣調用open/mkdir等函數建立檔案或目錄時,傳入的mode就會作為實際的值寫入檔案系統。
接下來建立目錄,並掛載核心檔案系統,它們是
- tmpfs,虛擬記憶體檔案系統,該檔案系統被掛載到/dev目錄下,主要存放裝置節點檔案,使用者進程通過訪問/dev目錄下的裝置節點檔案可以與硬體驅動程式互動。
- devpts,一種虛擬終端檔案系統
- proc,虛擬檔案系統,被掛載到/proc目錄下,通過該檔案系統可與核心資料結構互動,查看以及設定核心參數。
- sysfs,虛擬檔案系統,被掛載到/sys目錄下,它與proc類似,是2.6核心在吸收了proc檔案系統的設計經驗和教訓的基礎上所實現的一種較新的檔案系統,為核心提供了統一的裝置驅動模型。(引用:http://www.ibm.com/developerworks/cn/linux/l-cn-sysfs/index.html)
代碼<part2>隨後的代碼如下。
open_devnull_stdio(); klog_init(); property_init(); get_hardware_name(hardware, &revision); process_kernel_cmdline(); union selinux_callback cb; cb.func_log = log_callback; selinux_set_callback(SELINUX_CB_LOG, cb); cb.func_audit = audit_callback; selinux_set_callback(SELINUX_CB_AUDIT, cb); selinux_initialize(); /* These directories were necessarily created before initial policy load * and therefore need their security context restored to the proper value. * This must happen before /dev is populatedproperty_init(); by ueventd. */ restorecon("/dev"); restorecon("/dev/socket"); restorecon("/dev/__properties__"); restorecon_recursive("/sys"); is_charger = !strcmp(bootmode, "charger"); INFO("property init\n"); property_load_boot_defaults();open_devnull_stdio()該函數名字暗示將init進程的stido,包括stdin(標準輸入,檔案描述符為0)、stdout(標準輸出,檔案描述符為1)以及stderr(標準錯誤,檔案描述符號為2),全部重新導向/dev/null裝置,但是細心的讀者可能會有疑問,在代碼<part2>中雖然掛載了tmpfs檔案系統到/dev目錄下,但是並未建立任何裝置節點檔案,/dev/null此時並不存在啊,如何才能將stdio重新導向到null裝置中呢?帶著疑問我們來分析該函數實現。
void open_devnull_stdio(void){ int fd; static const char *name = "/dev/__null__"; if (mknod(name, S_IFCHR | 0600, (1 << 8) | 3) == 0) { fd = open(name, O_RDWR); unlink(name); if (fd >= 0) { dup2(fd, 0); dup2(fd, 1); dup2(fd, 2); if (fd > 2) { close(fd); } return; } } exit(1);}該函數中通過mknode函數建立/dev/__null__裝置節點檔案,隨後開啟該檔案得到檔案描述符fd,然後利用dup2系統調用將檔案描述符0、1、2綁定到fd上。這個/dev/__null__看起來很奇怪,Linux系統中的null不是/dev/null麼,這兩者有什麼關係嗎?
在Linux核心為裝置節點檔案分別了一個主、次裝置號,核心實際以這兩個裝置號來標識某個裝置驅動,而並不以檔案名稱作為標識。mknod系統調用建立裝置節點檔案,其第三個參數的高8位為主裝置號,低8位次裝置號。可見/dev/__null__的主次裝置號分別是1、3。它是否就是/dev/null呢?我們需要深入核心去確認這一點。
kernel/Documentation/devices.txt 中存在如下片段
1 char Memory devices 1 = /dev/mem Physical memory access 2 = /dev/kmem Kernel virtual memory access 3 = /dev/null Null device 4 = /dev/port I/O port access 5 = /dev/zero Null byte source 6 = /dev/core OBSOLETE - replaced by /proc/kcore 7 = /dev/full Returns ENOSPC on write 8 = /dev/random Nondeterministic random number gen. 9 = /dev/urandom Faster, less secure random number gen. 10 = /dev/aio Asynchronous I/O notification interface 11 = /dev/kmsg Writes to this come out as printk's 12 = /dev/oldmem Used by crashdump kernels to access the memory of the kernel that crashed.
可見/dev/__null__與/dev/null的裝置號完全相同,它就是/dev/null的馬甲。那麼為什麼init進程不直接建立/dev/null呢? 當前我們還無法回答這個問題,要等到分析/sbin/uevnted的原理時才能明白。
還有一個疑問,為什麼要將stdio重新導向/dev/__null__裝置呢?這是因為此時Anrdoid系統上處於啟動的早期階段,可用於接收init進程標準輸出、標準錯誤的裝置節點還不存在。因此init進程一不做二不休,直接把它們重新導向到/dev/__nulll__了。
當我們學習C語言時,第一個helloworld程式是通過printf列印的,我們知道它通過標準輸出列印到終端上。printf也是我們廣大程式員最喜愛的調試方法之一。現在標準輸出被重新導向到null裝置了,如果我們想在init中添加列印語句,怎麼辦呢?帶著這樣的擔憂,我們繼續分析代碼。
klog_init()隨後klog_init()顯然是在暗示我們,雖然標準輸出沒了,但是還有方法列印log的。帶著欣喜又好奇的心情,讓我們看看klog_init是如何?的。
void klog_init(void){ static const char *name = "/dev/__kmsg__"; if (klog_fd >= 0) return; /* Already initialized */ if (mknod(name, S_IFCHR | 0600, (1 << 8) | 11) == 0) { klog_fd = open(name, O_WRONLY); if (klog_fd < 0) return; fcntl(klog_fd, F_SETFD, FD_CLOEXEC); unlink(name); }}klog_init函數首先檢查klog_fd是否已經初始化。首次執行時,調用mknod建立主裝置號為1,從裝置號為11的裝置節點檔案/dev/__kmsg__,然後開啟該檔案將檔案描述符儲存到變數klog_fd中,接著調用fcntl(klog_fd, F_SETFD, FD_CLOEXEC)句作用是設定當執行execv時,關閉該檔案描述符。隨後調用unlink來刪除/dev/__kmsg__檔案,這裡比較特殊,具體解釋下。
當open某個檔案卻還沒有close它時,調用unlink並不能刪除該檔案,該檔案將在調用close後被刪除。對核心來說,當調用open開啟一個檔案,核心維護對應該檔案的資料結構,其中存在一個變數維護當前檔案的引用計數,該資料結構在使用者空間即對應檔案描述符。第一次open後,引用計數為1,調用open將使引用計數加1, 調用close將使得引用計數減1。當調用unlink系統調用時,若檔案引用計數非0,則核心並不會立刻刪除該檔案,核心會在每次close該檔案時檢查引用計數,若為0時將真正刪除檔案。
P.S.根據unlink的mannul,(man 2 unlink),其中寫道:If the name was the last link to a file but any processes still have the file open the file will remain in existence until the last file descriptor referring to it is closed.
/dev/__kmsg__檔案與/dev/kmsg的裝置節點完全相同,前者同樣是後者的馬甲。該裝置驅動節點是核心記錄檔,核心調用printk函數列印的log可以通過該裝置節點訪問,向該檔案中寫入則等同於執行核心printk。該檔案的內容可通Linux系統標準程式dmesg讀取,Android系統也提供了dmesg命令。
klog.c檔案代碼較少,在此一併分析
static int klog_level = KLOG_DEFAULT_LEVEL;int klog_get_level(void) { return klog_level;}void klog_set_level(int level) { klog_level = level;}#define LOG_BUF_MAX 512void klog_vwrite(int level, const char *fmt, va_list ap){ char buf[LOG_BUF_MAX]; if (level > klog_level) return; if (klog_fd < 0) klog_init(); if (klog_fd < 0) return; vsnprintf(buf, LOG_BUF_MAX, fmt, ap); buf[LOG_BUF_MAX - 1] = 0; write(klog_fd, buf, strlen(buf));}void klog_write(int level, const char *fmt, ...){ va_list ap; va_start(ap, fmt); klog_vwrite(level, fmt, ap); va_end(ap);}klog_write調用klog_vwrite函數可用於向/dev/__kmesg__中寫入日誌,第一個參數是當前log的層級,如果當前level大於klog_leve則直接返回,即無法將log寫入/dev/__kmesg__中。此外,提供了兩個函數klog_set_level與klog_get_level分別用於設定和讀取當前的klog_level,預設level為KLOG_DEFAULT_LEVEL,在klog.h中定義。
klog.h
#define KLOG_ERROR_LEVEL 3#define KLOG_WARNING_LEVEL 4#define KLOG_NOTICE_LEVEL 5#define KLOG_INFO_LEVEL 6#define KLOG_DEBUG_LEVEL 7#define KLOG_ERROR(tag,x...) klog_write(KLOG_ERROR_LEVEL, "<3>" tag ": " x)#define KLOG_WARNING(tag,x...) klog_write(KLOG_WARNING_LEVEL, "<4>" tag ": " x)#define KLOG_NOTICE(tag,x...) klog_write(KLOG_NOTICE_LEVEL, "<5>" tag ": " x)#define KLOG_INFO(tag,x...) klog_write(KLOG_INFO_LEVEL, "<6>" tag ": " x)#define KLOG_DEBUG(tag,x...) klog_write(KLOG_DEBUG_LEVEL, "<7>" tag ": " x) #define KLOG_DEFAULT_LEVEL 3 /* messages <= this level are logged */
可見預設層級為3,即KLOG_ERROR_LEVEL,只有調用KLOG_ERROR才能被輸出到/dev/__kmesg__中。
property_init();
這一句用來初始化Android的屬性系統,將在init之屬性系統中專門介紹。
get_hardware_name
get_hardware_name(hardware, &revision)通過讀取/proc/cpuinfo檔案擷取硬體資訊,以筆者的山寨機為例,該檔案內容如下。
[email protected]:/ $ cat /proc/cpuinfo Processor : ARMv7 Processor rev 1 (v7l)processor : 0BogoMIPS : 348.76processor : 1BogoMIPS : 348.76processor : 2BogoMIPS : 348.76processor : 3BogoMIPS : 348.76Features : swp half thumb fastmult vfp edsp thumbee neon vfpv3 tls vfpv4CPU implementer : 0x41CPU architecture: 7CPU variant : 0x0CPU part : 0xc05CPU revision : 1Hardware : QRD MSM8625Q SKUDRevision : 0000Serial : 0000000000000000
get_hardware_name函數讀取該檔案,將Hardware欄位的值填入hardware數組中,將Revision欄位的值轉換為16進位數字填入revision變數中。
process_kernel_cmdline
接下來init程式調用函數process_kernel_cmdline解析核心啟動參數。核心通常由bootloader(啟動引導程式)載入啟動,目前廣泛使用的bootloader大都基於u-boot定製。核心允許bootloader啟動自己時傳遞參數。在核心啟動完畢之後,啟動參數可通過/proc/cmdline查看。
例如android4.4模擬器啟動後,查看其核心啟動參數,如下
[email protected]:/ # cat /proc/cmdline
qemu.gles=0 qemu=1 console=ttyS0 android.qemud=ttyS1 android.checkjni=1 ndns=1
static void process_kernel_cmdline(void){ /* don't expose the raw commandline to nonpriv processes */ chmod("/proc/cmdline", 0440); /* first pass does the common stuff, and finds if we are in qemu. * second pass is only necessary for qemu to export all kernel params * as props. */ import_kernel_cmdline(0, import_kernel_nv); if (qemu[0]) import_kernel_cmdline(1, import_kernel_nv); /* now propogate the info given on command line to internal variables * used by init as well as the current required properties */ export_kernel_boot_props();}首先修改/proc/cmdline檔案許可權,0440即表明只有root使用者或root組使用者可以讀寫該檔案,其他使用者無法訪問。隨後連續調用import_kernel_cmdline函數,第一個參數標識當前Android裝置是否是模擬器,第二個參數一個函數指標。
import_kernel_cmdline函數將/proc/cmdline內容讀入到內部緩衝區中,並將cmdline內容的以空格拆分成小段字串,依次傳遞給import_kernel_nv函數處理。以前面/proc/cmdline的輸出為例子,該字串共可以拆分成以下幾段
qemu.gles=0qemu=1console=ttyS0android.qemud=ttyS1android.checkjni=1ndns=1
因此在import_kernel_nv將會被連續調用6次,依次傳入上述字串。函數實現如下:
import_kernel_nv
static void import_kernel_nv(char *name, int for_emulator){ char *value = strchr(name, '='); int name_len = strlen(name); if (value == 0) return; *value++ = 0; if (name_len == 0) return; if (for_emulator) { /* in the emulator, export any kernel option with the * ro.kernel. prefix */ char buff[PROP_NAME_MAX]; int len = snprintf( buff, sizeof(buff), "ro.kernel.%s", name ); if (len < (int)sizeof(buff)) property_set( buff, value ); return; } if (!strcmp(name,"qemu")) { strlcpy(qemu, value, sizeof(qemu)); } else if (!strncmp(name, "androidboot.", 12) && name_len > 12) { const char *boot_prop_name = name + 12; char prop[PROP_NAME_MAX]; int cnt; cnt = snprintf(prop, sizeof(prop), "ro.boot.%s", boot_prop_name); if (cnt < PROP_NAME_MAX) property_set(prop, value); }}import_kernel_cmdline第一次執行時,傳入import_kernel_nv的形式參數for_emulator為 0,,因此將匹配name是否為qemu,如果是,將其值儲存到qemu全域靜態緩衝區中。對於android模擬器,存在/proc/cmdline中存在“qemu=1”欄位。如果for_emulator為1,則將產生ro.kernel.{name}={value}屬性寫入Android的屬性系統中。
此時回到process_kernel_cmdline函數,繼續執行
if (qemu[0]) import_kernel_cmdline(1, import_kernel_nv);
當系統為模擬器時,qemu[0]其值為‘1‘,第二次執行import_kernel_cmdline,將再次調用6次import_kernel_nv,並且for_emulator為1,因此將產生6個屬性,現在來確定以下我們的分析。
[email protected]:/ # getprop | grep ro.kernel. [ro.kernel.android.checkjni]: [1][ro.kernel.android.qemud]: [ttyS1][ro.kernel.console]: [ttyS0][ro.kernel.ndns]: [1][ro.kernel.qemu.gles]: [0][ro.kernel.qemu]: [1]
可驗證我們的分析是正確的。
export_kernel_boot_props()
接下來繼續執行process_kernel_cmdline函數的最後一句export_kernel_boot_props。由於該函數實現非常直觀,其代碼不在詳細描述。該函數用於設定幾個系統屬性,具體包括如下:
讀取ro.boot.serialno,若存在其值寫入ro.serialno,否則ro.serialno寫入空。讀取ro.boot.mode,若存在其值寫入ro.bootmode,否則ro.bootmode寫入"unkown"讀取ro.boot.baseband,若存在其值寫入ro.baseband,否則ro.baseband寫入"unkown"
讀取ro.boot.bootloader,若存在其值寫入ro.bootloader,否則ro.bootloader寫入"unkown"讀取ro.boot.console,若存在,其值寫入全域緩衝區console中讀取ro.bootmode,若存在,其值儲存到全域緩衝區bootmode中
讀取ro.boot.hardware,若存在其值寫入ro.hardware,否則將/proc/cmdline中解析出來的hardware寫入ro.hardware中。
SELinux
union selinux_callback cb; cb.func_log = log_callback; selinux_set_callback(SELINUX_CB_LOG, cb); cb.func_audit = audit_callback; selinux_set_callback(SELINUX_CB_AUDIT, cb); selinux_initialize(); /* These directories were necessarily created before initial policy load * and therefore need their security context restored to the proper value. * This must happen before /dev is populated by ueventd. */ restorecon("/dev"); restorecon("/dev/socket"); restorecon("/dev/__properties__"); restorecon_recursive("/sys");這部分代碼是在Android4.1之後添加的,隨後伴隨Android系統更新不停迭代。這段代碼主要涉及SELinux初始化。由於SELinux與Android系統啟動關閉不大,暫不分析。
回到init函數<part2>繼續分析
is_charger = !strcmp(bootmode, "charger"); INFO("property init\n"); property_load_boot_defaults();第一句將利用bootmode與字串"charger"將其儲存到is_charger變數中,is_charger非0表明但前Android是以充電模式啟動,否則為正常模式。正常啟動模式與充電模式需要啟動的進程不同的,這兩種模式啟動具體啟動的程式差別將在init.rc解析時介紹。
接下來調用INFO宏列印一條log語句,此宏定義在init/log.h中,其實現如下
#define ERROR(x...) KLOG_ERROR("init", x)#define NOTICE(x...) KLOG_NOTICE("init", x)#define INFO(x...) KLOG_INFO("init", x)顯然這是一條level為KLOG_INFO_LEVEL的log語句。它是否能輸出到/dev/__kmesg__中跟當前klog level的值有關。預設情況下,klog level為3,這條語句將不會輸出到/dev/__kmsg__中。
到這裡init.c main函數之<part2>程式碼分析分析完畢。
接下來<part3>代碼涉及init進程核心功能:init.rc解析。這部分代碼邏輯我們將在獨立文章《Android init原始碼分析(2)init.rc解析》中介紹。
Android init原始碼分析(1)概要分析