前言 Android系統是運做在linux kernal上的,所以它的啓動過程也遵循linux的啓動過程,當linux內核啓動以後,運行的第一個進程是init,這個進程是一個守護進程,它的生命週期貫穿整個linux 內核運行的始終, linux中全部其餘的進程的共同始祖均爲init進程。固然爲了啓動並運行整個android系統,google實現了本身的init進程,下面主要分析init進程都作了些什麼? html
1.首先,init是一個守護進程,爲了防止init的子進程成爲殭屍進程(zombie process),須要init在子進程在結束時獲取子進程的結束碼,經過結束碼將程序表中的子進程移除,防止成爲殭屍進程的子進程佔用程序表的空間,當程序表的空間達到上限時,則系統就不能再啓動新的進程了,那麼就會引發很嚴重的系統問題。 node
在linux當中,父程序是經過捕捉SIGCHLD信號來得知子進程結束的狀況的;因爲系統默認在子進程暫停時也會發送信號SIGCHLD,init須要忽略子進程在暫停時發出的SIGCHLD信號,所以將act.sa_flags 置爲SA_NOCLDSTOP,該標誌位的含義是就是要求系統在子進程暫停時不發送SIGCHLD信號。具體的代碼以下所示: struct sigaction act; ……………… act.sa_handler = sigchld_handler; act.sa_flags = SA_NOCLDSTOP; act.sa_mask = 0; act.sa_restorer = NULL; sigaction(SIGCHLD, &act, 0);
2.建立文件系統目錄並掛載相關的文件系統 linux
/ clear the umask / umask(0);
/ Get the basic filesystem setup we need put
- together in the initramdisk on / and then we'll
- let the rc file figure out the rest.
mkdir("/dev", 0755); mkdir("/proc", 0755); mkdir("/sys", 0755);
mount("tmpfs", "/dev", "tmpfs", 0, "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);
2.1 清除屏蔽字(file mode creation mask),保證新建的目錄的訪問權限不受屏蔽字影響. android
2.2 在init初始化過程當中,Android分別掛載了tmpfs,devpts,proc,sysfs 4類文件系統 服務器
2.2.1 tmpfs文件系統 網絡
tmpfs是一種虛擬內存文件系統,所以它會將全部的文件存儲在虛擬內存中,而且tmpfs下的全部內容均爲臨時性的內容,若是你將tmpfs文件系統卸載後,那麼其下的全部的內容將不復存在。 tmpfs有些像虛擬磁盤(ramdisk),但不是一回事。說其像虛擬磁盤,是由於它可使用你的RAM,但它也可使用你的交換分區。傳統的虛擬磁盤是一個塊設 備,並且須要一個mkfs之類的命令格式化它才能使用。tmpfs是一個獨立的文件系 統,不是塊設備,只要掛接,當即就可使用。
tmpfs的大下是不肯定的,它最初只有很小的空間,但隨着文件的複製和建立,它的大小就會不斷變化,換句話說,它會根據你的實際須要而改變大小;tmpfs的速 度很是驚人,畢竟它是駐留在RAM中的,即便用了交換分區,性能仍然很是卓越; 因爲tmpfs是駐留在RAM的,所以它的內容是不持久的,斷電後,tmpfs的內容就消失 了,這也是被稱做tmpfs的根本緣由。
關於tmpfs文件系統請參考linux內核文檔: kernel/Documentation/filesystems/tmpfs.txt
2.2.2 devpts文件系統 數據結構
devpts文件系統爲僞終端提供了一個標準接口,它的標準掛接點是/dev/pts。只要pty的主複合設備/dev/ptmx被打開,就會在/dev/pts下動態的建立一個新的pty設備文 件。 2.2.3 proc文件系統
proc文件系統是一個很是重要的虛擬文件系統,它能夠看做是內核內部數據結構的接口,經過它咱們能夠得到系統的信息,同時也可以在運行時修改特定的內核參數。 在proc文件系統中,你能夠修改內核的參數,是否是很強大?怎麼修改呢?你只須要echo一個新的值到對應的文件中便可,可是若是在修改過程當中發生錯誤的話,那麼你將別無選擇,只能重啓設備。
關於tmpfs文件系統請參考linux內核文檔: kernel/Documentation/filesystems/proc.txt2.2.4 sysfs文件系統
與proc文件系統相似,sysfs文件系統也是一個不佔有任何磁盤空間的虛擬文件系統。它一般被掛接在/sys目錄下。sysfs文件系統是Linux2.6內核引入的,它把鏈接在系 統上的設備和總線組織成爲一個分級的文件,使得它們能夠在用戶空間存取。
3.屏蔽標準的輸入輸出,即標準的輸入輸出定向到NULL設備。 socket
這一步是經過調用函數open_devnull_stdio實現的,下面咱們研究一下open_devnull_stdio的函數實現void open_devnull_stdio(void) {
int fd; static const char name = "/dev/null";//建立一個字符專用文件(character special file) /dev/null
if (mknod(name, S_IFCHR | 0600, (1 << 8) | 3) == 0) {//獲取/dev/null的文件描述符,並輸出該文件
fd = open(name, O_RDWR); unlink(name);//將與進程相關的標準輸入(0),標準輸出(1),標準錯誤輸出(2),均定向到NULL設備
if (fd >= 0) {dup2(fd, 0); dup2(fd, 1); dup2(fd, 2); if (fd > 2) {}close(fd);} return;
}
exit(1);
} 函數
這裏解釋一下過程: 首先說明如下dup2的做用,這個函數主要是複製一個函數的描述符,通常用於重定向進程的stdin,stdout,stderr。它的原型以下: int dup2(int oldfd, int newfd);dup2(fd, 0); dup2(fd, 1); dup2(fd, 2);
dup2(fd, 0); dup2(fd, 1); dup2(fd, 2);這三次調用一次將依次表明stdin,stdout,stderr的描述符0,1,2,重定向到dev/null,經過這種方式達到屏蔽標準輸入輸出的做用。 4. 初始化內核log系統
這個過程對應的源碼爲:log_init(); 這個函數詳細實現爲 void log_init(void) {
static const char name = "/dev/kmsg"; if (mknod(name, S_IFCHR | 0600, (1 << 8) | 11) == 0) {//當進程在進行exec系統調用時,要確保log_fd是關閉的(經過FD_CLOEXEC標誌位來設置).log_fd = open(name, O_WRONLY);
fcntl(log_fd, F_SETFD, FD_CLOEXEC); unlink(name);
}} 有上述實現看出內核的log輸出是經過文件描述符log_fd寫入的,那到底寫入到什麼設備呢?/dev/kmsg,這個設備則會把它收到的任何寫入都做爲printk的輸出。printk函數是內核中運行的向控制檯輸出顯示的函數。
5.解析init.rc post
5.1 Android init language
Android init language包含四種類型語句:Actions, Commands, Services, Options。
它的主要語法風格爲:
2.c語言風格的反斜槓(\)將被轉義爲插入一個空格; 3.若是一個關鍵字含有一個或多個空格,那麼怎麼保證關鍵字完整呢?可使用雙引號來肯定關鍵字的範圍。 4.用於行尾的反斜槓表示續行符。 5.Actions和Services聲明一個字段(section),緊隨其後的Commands和Options均屬於這個字段,在第一個字段以前的Commands和Options的沒有意義。 6.Actions和Services有獨一無二的名字,若是Actions和Services的名字有重名,那麼將被視做錯誤。
5.1.1 Actions
Actions其實就是一組被命名的Commands序列。當知足觸發器的事件發生時,這個action就會被置於一個隊列中,這個隊列存放着將要被執行的action。其格式以下: on<trigger>
on是Actions的關鍵字,它代表下面的序列是Actions序列。<command>
<command>
<command>
5.1.2 Services
Services是有init進程啓動的或者從新啓動的程序。其格式以下: service<name>
<pathname>
<argument><option>
<option>
5.1.3 Options
Options是Services的修飾符,由它來指定什麼時候而且如何啓動Services程序。
5.1.4 Commands
Commands便是在知足triger條件後,Actions中執行的內容。
Options和Commands的取值在這裏就不描述裏,有興趣請參考system/core/rootdir/init.rc
5.2 init.rc解析過程
咱們繼續回到init.c的main函數中,看init.rc的解析過程。init文件有兩個init.rc和init.hardware.rc。
init_parse_config_file("/init.rc");//解析init.rc
/ pull the kernel commandline and ramdisk properties file in / import_kernel_cmdline(0);//從/proc/cmdline讀取內核啓動參數,並保存到相應的變量中
get_hardware_name(hardware, &revision);//從/proc/cpuinfo中獲取硬件信息 snprintf(tmp, sizeof(tmp), "/init.%s.rc", hardware); init_parse_config_file(tmp);//解析硬件相關的init信息
着重介紹一下init_parse_config_file過程,這個函數負責init文件的解析。
2.根據Actions和Services的格式對section進行逐行解析; 3.將解析出的內容存放到雙向循環鏈表中。
解析過程當中的雙向循環鏈表的使用,android用到了一個很是巧妙的鏈表實現方法,通常狀況下若是鏈表的節點是一個單獨的數據結構的話,那麼針對不一樣的數據結構,都須要定義不一樣鏈表操做。 而在初始化過程當中使用到的鏈表則解決了這個問題,它將鏈表的節點定義爲了一個很是精簡的結構,只包含前向和後向指針,那麼在定義不一樣的數據結構時,只須要將鏈表節點嵌入到數據結構中便可。 例如,鏈表節點定義以下, struct listnode {struct listnode next; struct listnode prev;};
數據結構的定義以下,拿Action的數據結構爲例,
struct action {/ node in list of all actions / struct listnode alist; / node in the queue of pending actions / struct listnode qlist; / node in list of actions for a trigger / struct listnode tlist;
unsigned hash; const char name;
struct listnode commands; struct command current;
};
這樣的話,全部的鏈表的基本操做,例如插入,刪除等只會針對listnode進行操做,而不是針對特定的數據結構,如action進行操做,那麼在多個數據結構使用雙向鏈表時,鏈表的實現獲得了統一,即精簡了代碼,又提升了效率。 可是這樣的鏈表實現,存在一個問題,鏈表節點listnode中只有前向和後向指針,而且前向和後向指針均指向listnode,那麼咱們經過什麼方式來訪問數據結構action的內容呢? 在這裏引入了一個宏offsetof,咱們man一下這個宏的的定義,發現這個宏是結構體中成員變量的偏移量。這下你們內心是否是已經意識到怎麼訪問數據結構action了吧,對!就是計算鏈表節點在數據結構中的偏移量,來計算數據結構實例的地址。
Android的init過程是經過下面的宏定義來實現的,
#define node_to_item(node, container, member) \
(container ) (((char) (node)) - offsetof(container, member))
小結一下這種鏈表的優勢:(1)全部鏈表基本操做都是基於listnode指針的,所以添加類型時,不須要重複寫鏈表基本操做函數(2)一個container數據結構能夠含有多個listnode成員,這樣就能夠同時掛到多個不一樣的鏈表中。
5.3 Actions待執行隊列
當解析完全部的init.rc內容以後,在執行這些action以前,須要按順序將其置於一個待執行隊列中,如action_for_each_trigger("early-init", action_add_queue_tail);
還有一些沒有在init.rc中定義的action,相比init.rc,這些action的共同點是沒有參數,如 queue_builtin_action(wait_for_coldboot_done_action, "wait_for_coldboot_done");
下面咱們分析一下init中的Actions待執行隊列的順序以及功能
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(property_init_action, "property_init"); queue_builtin_action(keychord_init_action, "keychord_init"); queue_builtin_action(console_init_action, "console_init"); queue_builtin_action(set_init_properties_action, "set_init_properties");
/ execute all the boot actions to get us started /
action_for_each_trigger("init", action_add_queue_tail); 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);
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");
/ execute all the boot actions to get us started / 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_propety_triggers");
#if BOOTCHART
queue_builtin_action(bootchart_init_action, "bootchart_init");#endif
5.3.1 early-init
查看init.rc中的相應字符段爲 start ueventd 這個action主要目的是經過early-init啓動ueventd服務,這個服務負責uevent(user space event)的處理,uevent是內核向用戶空間發出的一個時間通知,使應用程序可以有機會對該event作出反應。
5.3.2 wait_for_coldboot_done
android 冷過程結束後會生成dev/.coldboot_done文件,wait_for_coldboot_done這個action會等待dev/.coldboot_done文件的生成,等待時長爲5s。固然這個action不會阻塞android的冷啓動過程,它會沒查詢一次就會休眠0.1s,直到冷啓動結束。
5.3.3 property_init
幾種特殊的屬性:
2.net.屬性,顧名思義,就是與網絡相關的屬性,net.屬性中有一個特殊的屬性:net.change,它記錄了每一次最新設置和更新的net.屬性,也就是每次設置和更新net.屬性時則會自動的更新net.change屬性,net.change屬性的value就是這個被設置或者更新的net屬性的name。例如咱們更新了屬性net.bt.name的值,因爲net有屬性發生了變化,那麼屬性服務就會自動更新net.change,將其值設置爲net.bt.name。 3.persist.屬性,以文件的形式保存在/data/property路徑下。persist.屬性因爲將其保存在了用戶空間中,因此在property_init中是不能對其更新的,只能將其更新過程交給用戶來處理。 4.ctl.屬性,雖然是以屬性的形式來進行設置,其實它的目的是爲了啓動或關閉它指定的service 初始化android的屬性系統,整個的過程分爲下面2步
2.加載並設置/default.prop中定義的屬性,default.prop中主要是一些「ro.」只讀屬性。
5.3.4 keychord_init
這個東東不是太理解,目前的全部service均未用到這個機制。
5.3.5 console_init
2.加載開機圖片,參考load_565rle_image函數 a,經過ioctl函數修改dev/tty0(即終端控制檯)爲圖像顯示模式; b,嘗試打開/initlogo.rle,若是失敗,那麼將dev/tty0恢復爲文本顯示模式,則開機時顯示"ANDROID"文字; c,若是打開/initlogo.rle成功,那麼init將會打開Framebuffer,下面咱們分析一下這個過程//logo.c static int fb_open(struct FB fb) {//打開Framebuffer對應的設備文件/dev/graphics/fb0 fb->fd = open("/dev/graphics/fb0", O_RDWR); if (fb->fd < 0) return -1; //經過ioctl函數得到Framebuffer相關信息 //FBIOGET_FSCREENINFO對應的是Framebuffer的固定信息 //FBIOGET_VSCREENINFO對應的是Framebuffer的可變信息 if (ioctl(fb->fd, FBIOGET_FSCREENINFO, &fb->fi) < 0) goto fail; if (ioctl(fb->fd, FBIOGET_VSCREENINFO, &fb->vi) < 0) goto fail; //因爲Framebuffer是能夠被用戶直接讀寫的,因此須要將/dev/graphics/fb0映射到用戶空間的內存區。 fb->bits = mmap(0, fb_size(fb), PROT_READ | PROT_WRITE,MAP_SHARED, fb->fd, 0);if (fb->bits == MAP_FAILED) goto fail;
return 0;
fail:close(fb->fd); return -1;} d,將initlogo.rle數據寫到Framebuffer中。
目前android默認是沒有initlogo.rle,若是想本身添加開機圖片的話,具體過程請參考 http://www.cnmsdn.com/html/201005/1274855679ID5109.html
5.3.6 set_init_properties
設置與硬件載頻相關的只讀屬性。
5.3.7 init
執行init.rc中init action字段中定義的處理。init.rc中的actions就再也不一一分析了,有興趣或者有時間在分析。
5.3.8 property_service_init
2.建立一個服務器端UNIX Domain Socket,它的socket文件路徑爲/dev/socket/property_service,這個socket監聽來自客戶端的屬性修改請求.
5.3.9 signal_init
2.經過socketpair建立一對已鏈接的socket,將生成的兩個socket設置爲O_NONBLOCK模式,也就是將對socket句柄的讀寫操做設置爲非阻塞模式。
5.3.10 check_startup
確保5.3.8中屬性設置socket文件描述符和signal_init中signal socket文件描述符,若是兩個有其一不存在,那麼將退出系統。
5.3.11 boot
boot action主要由兩部分組成,
2. 啓動全部init.rc聲明的未指定class的service; 具體的command爲 class_start default。 在解析init.rc時,若是service未指定class選項的話,那麼會給它的classname默認的指定爲「default」,而目前的init.rc中的全部的service均未指定class選項,因此命令「class_start default」將按順序啓動全部的service。 也能夠爲須要一塊兒啓動,一塊兒關閉的services指定一個相同的class,那麼就能夠對這些service進行統一處理了。 還需注意:若是service中定義了disabled選項,那麼不能經過class_start來啓動它,只能顯示的一個一個的啓動。被disabled修飾的service通常是在
5.3.12 queue_propety_triggers
根據init.rc中action指定的property值與屬性中的值比較,若是相等則執行對應的command。例如 on property:ro.secure=0start console若是當前ro.secure的值爲0,那麼啓動console服務
5.3.13 bootchart_init
Bootchart 可以對系統的性能進行分析,並生成系統啓動過程的圖表,以便爲你提供有價值的參考信息。綜合所得的信息,你就能夠進行相應的改進,從而加快你的 Linux 系統啓動過程。 若是設置了Bootchart,則該過程初始化Bootchart。
5.4 init輪詢過程
以上部分將全部須要操做的action均放在了action待執行隊列中,那麼init進程將要進入一個死循環過程,整個android的將會運行在這個生命週期內。
2.重啓全部須要重啓的service; 3.註冊屬性設置property_set_fd,信號signal處理signal_recv_fd,keychord keychord_fd三個文件描述符的爲輪詢對象。 if (!property_set_fd_init && get_property_set_fd() > 0) {ufdsfd_count.fd = get_property_set_fd(); ufdsfd_count.events = POLLIN; ufdsfd_count.revents = 0; fd_count++; property_set_fd_init = 1;} if (!signal_fd_init && get_signal_fd() > 0) {ufdsfd_count.fd = get_signal_fd(); ufdsfd_count.events = POLLIN; ufdsfd_count.revents = 0; fd_count++; signal_fd_init = 1;} if (!keychord_fd_init && get_keychord_fd() > 0) {ufdsfd_count.fd = get_keychord_fd(); ufdsfd_count.events = POLLIN; ufdsfd_count.revents = 0; fd_count++; keychord_fd_init = 1;}
有以上代碼可見,init進程將三個描述符均定義爲了POLLIN事件響應,當描述符有可讀數據時,對於socket描述符,有鏈接請求時ufds就會收到POLLIN事件。
4.下面分別對這3個文件描述符的輪詢過程做簡單的介紹nr = poll(ufds, fd_count, timeout); if (nr <= 0)continue;
for (i = 0; i < fd_count; i++) {if (ufdsi.revents == POLLIN) {}if (ufdsi.fd == get_property_set_fd())}handle_property_set_fd();else if (ufdsi.fd == get_keychord_fd())handle_keychord();else if (ufdsi.fd == get_signal_fd())handle_signal();
上面的代碼爲輪詢的整體體現,當有POLLIN事件發生時,相應的ufdsi.revents就會被置爲POLLIN,而後執行各自的handler A,property_set_fd 收到屬性設置的socket請求以後,設置相關屬性。
B,signal_recv_fd 當有子進程終止時,也就是service終止時,內核會給init發送SIGCHLD,此時調用註冊的handler函數 static void sigchld_handler(int s) {write(signal_fd, &s, 1);}這個handler函數是向其中的一個socket signal_fd寫入數據,因爲signal_init過程當中初始化了一對已鏈接的socket signal_fd和signal_recv_fd,所以此時signal_recv_fd會收到向signal_fd寫入的數據,而後查詢那個service終止,而後根據該service的屬性來做相關的操做,是重啓仍是結束進行資源回收。
C,keychord_fd 目前的init過程當中沒有service執行keychord機制。