android的init過程分析

前言
Android系統是運作在linux kernal上的,因此它的啟動過程也遵循linux的啟動過程,當linux內核啟動之後,運行的第一個進程是init,這個進程是一個守護進程,它的生命周期貫穿整個linux 內核運行的始終,linux中所有其他的進程的共同始祖均為init進程。當然為瞭啟動並運行整個android系統,google實現瞭自己的init進程,下面主要分析init進程都做瞭些什麼?
 
1.首先,init是一個守護進程,為瞭防止init的子進程成為僵屍進程(zombie process),需要init在子進程在結束時獲取子進程的結束碼,通過結束碼將程序表中的子進程移除,防止成為僵屍進程的子進程占用程序表的空間,當程序表的空間達到上限時,則系統就不能再啟動新的進程瞭,那麼就會引起很嚴重的系統問題。
    在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.創建文件系統目錄並掛載相關的文件系統
 
    /* 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),保證新建的目錄的訪問權限不受屏蔽字影響.
 
2.2 在init初始化過程中,Android分別掛載瞭tmpfs,devpts,proc,sysfs 4類文件系統
 
2.2.1tmpfs文件系統
    tmpfs是一種虛擬內存文件系統,因此它會將所有的文件存儲在虛擬內存中,並且tmpfs下的所有內容均為臨時性的內容,如果你將tmpfs文件系統卸載後,那麼其下的所有的內容將不復存在。
    tmpfs有些像虛擬磁盤(ramdisk),但不是一回事。說其像虛擬磁盤,是因為它
可以使用你的RAM,但它也可以使用你的交換分區。傳統的虛擬磁盤是一個塊設
備,而且需要一個mkfs之類的命令格式化它才能使用。tmpfs是一個獨立的文件系
統,不是塊設備,隻要掛接,立即就可以使用。
    tmpfs的大下是不確定的,它最初隻有很小的空間,但隨著文件的復制和創建,
它的大小就會不斷變化,換句話說,它會根據你的實際需要而改變大小;tmpfs的速
度非常驚人,畢竟它是駐留在RAM中的,即使用瞭交換分區,性能仍然非常卓越;
由於tmpfs是駐留在RAM的,因此它的內容是不持久的,斷電後,tmpfs的內容就消失
瞭,這也是被稱作tmpfs的根本原因。
    關於tmpfs文件系統請參考linux內核文檔:
    kernel/Documentation/filesystems/tmpfs.txt
 
2.2.2devpts文件系統   
    devpts文件系統為偽終端提供瞭一個標準接口,它的標準掛接點是/dev/pts。隻要
pty的主復合設備/dev/ptmx被打開,就會在/dev/pts下動態的創建一個新的pty設備文
件。
2.2.3 proc文件系統
    proc文件系統是一個非常重要的虛擬文件系統,它可以看作是內核內部數據結構的接口,通過它我們可以獲得系統的信息,同時也能夠在運行時修改特定的內核參數。
    在proc文件系統中,你可以修改內核的參數,是不是很強大?怎麼修改呢?你隻需要echo一個新的值到對應的文件中即可,但是如果在修改過程中發生錯誤的話,那麼你將別無選擇,隻能重啟設備。
   
    關於tmpfs文件系統請參考linux內核文檔:
    kernel/Documentation/filesystems/proc.txt
2.2.4 sysfs文件系統
    與proc文件系統類似,sysfs文件系統也是一個不占有任何磁盤空間的虛擬文件系
統。它通常被掛接在/sys目錄下。sysfs文件系統是Linux2.6內核引入的,它把連接在系
統上的設備和總線組織成為一個分級的文件,使得它們可以在用戶空間存取。
 
3.屏蔽標準的輸入輸出,即標準的輸入輸出定向到NULL設備。
    這一步是通過調用函數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(fd, 0);
            dup2(fd, 1);
            dup2(fd, 2);
過程:
首先說明以下dup2的作用,這個函數主要是復制一個函數的描述符,一般用於重定向進程的stdin,stdout,stderr。它的原型如下:
int dup2(int oldfd, int newfd);
            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) {
        log_fd = open(name, O_WRONLY);
//當進程在進行exec系統調用時,要確保log_fd是關閉的(通過FD_CLOEXEC標志位來設置).
        fcntl(log_fd, F_SETFD, FD_CLOEXEC);
        unlink(name);
    }
}
有上述實現看出內核的log輸出是通過文件描述符log_fd寫入的,那到底寫入到什麼設備呢?/dev/kmsg,這個設備則會把它收到的任何寫入都作為printk的輸出。printk函數是內核中運行的向控制臺輸出顯示的函數。
 
 
5.解析init.rc
 
5.1 Android init language
 
    Android init language包含四種類型語句:Actions, Commands, Services, Options。
它的主要語法風格為:
    1.每一個語句占據一行,所有關鍵字通過空格來分割。
    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>
          <command>
          <command>
          <command>
    on是Actions的關鍵字,它表明下面的序列是Actions序列。
 
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文件的解析。
 
    1.首先判斷關鍵字,隻能有兩種可能on或者service,通過關鍵字來判定section范圍;
    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
    幾種特殊的屬性:
    1.ro.屬性,它表示隻讀屬性,它一旦被設置就不能被修改;
    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步
    1.初始化屬性區域(property area),主要工作是將屬性設備節點/dev/properties映射到內存空間上,將整個的屬性內容作為共享內存來處理,這個共享內存就是屬性區域,當前android中使用全局變量__system_property_area__來標記屬性區域。
    2.加載並設置/default.prop中定義的屬性,default.prop中主要是一些“ro.”隻讀屬性。
 
5.3.4 keychord_init
    這個東東不是太理解,目前的所有service均未用到這個機制。
 
5.3.5 console_init
    1.如果/proc/cmdline指定瞭控制臺終端,那麼優先使用這個控制臺,如果沒有指定,那麼將使用默認控制臺終端/dev/console。
    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,如果想自己添加開機圖片的話,具體過程請參考https://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
    1.讀取/system/build.prop,/system/default.prop,/data/local.prop以及/data/property/下的屬性並將其設置;
    2.創建一個服務器端UNIX Domain Socket,它的socket文件路徑為/dev/socket/property_service,這個socket監聽來自客戶端的屬性修改請求.
 
5.3.9 signal_init
    1.
    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主要由兩部分組成,
    1. 還是一些配置性的工作,例如基本的網絡配置;ActivityManagerService中用到的進程管理和資源回收時,需要用到的優先級變量的設置等。
    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=0
        start console
    如果當前ro.secure的值為0,那麼啟動console服務
 
5.3.13 bootchart_init
    Bootchart 能夠對系統的性能進行分析,並生成系統啟動過程的圖表,以便為你提供有價值的參考信息。綜合所得的信息,你就可以進行相應的改進,從而加快你的Linux 系統啟動過程。
    如果設置瞭Bootchart,則該過程初始化Bootchart。
 
5.4 init輪詢過程
    以上部分將所有需要操作的action均放在瞭action待執行隊列中,那麼init進程將要進入一個死循環過程,整個android的將會運行在這個生命周期內。
 
    1.執行action待執行隊列中的所有command;
    2.重啟所有需要重啟的service;
    3.註冊屬性設置property_set_fd,信號signal處理signal_recv_fd,keychord keychord_fd三個文件描述符的為輪詢對象。
    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;
        }
   
    有以上代碼可見,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 (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();
            }
        }
    上面的代碼為輪詢的總體體現,當有POLLIN事件發生時,相應的ufds[i].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機制。
 
摘自 杜文濤的專欄

發佈留言