Android 10.0 源碼閱讀 | Android Init 進程啓動流程研究

閱讀源碼的文章會是一個系列,本篇主要內容是 Android 源碼中啓動流程的第一部分,包含了 Linux 內核啓動部分與 Android init 進程啓動部分。html

Linux 內核啓動

爲何我會先提 Linux 的啓動呢?一方面 Linux 內核是 Android 平臺的基礎,另外一方面我最近接觸了一些 Linux 的基礎知識,因此但願把這些學到的東西也都記錄下來。linux

內核的做用其實就是控制計算機硬件資源並提供程序運行環境,具體的好比有:執行程序、文件操做、內存管理及設備驅動等,而內核對外提供的接口也被稱爲系統調用。android

既然內核這麼重要,提供了各類程序運行所需的服務,那啓動 Android 前確定是須要先把內核啓動起來的。具體內核如何啓動,咱們先來看看當咱們按下開機鍵後都發生了什麼。git

計算機通電後首先會去找 ROM(只讀內存),這裏面被固化了一些初始化程序,這個程序也叫 BIOS,具體幾步就像下面這樣:github

讀取 BIOS(基本輸入輸出系統,放在 ROM 中):shell

  • 硬件自檢,也就是檢查計算機硬件是否知足運行的基本條件;
  • 這個程序中查看啓動順序,固然這個能夠自行調整,這時就按照啓動順序去找下一階段的啓動程序在哪裏;

主引導記錄(BIOS 中把控制權交給啓動順序的第一位):json

  • 讀取該設備的第一個扇區的前 512 字節,若是以特定字符結束,就說明這個設備能夠用於啓動,若是不是就按照剛纔 BIOS 中的啓動順序將控制權交給下一個設備,這最前面 512 字節也叫主引導記錄(MBR);
  • MBR 中的 512 字節放不下太多東西,因此它主要是告訴計算機去哪裏找操做系統(硬盤上);
  • 這時經過 MBR 的分區表在硬盤上找到對應位置;

經過 boot loader 啓動操做系統:安全

  • Linux 使用的是 Grub2,它是啓動管理器,會將各類 img 加載進來;
  • 操做系統內核加載到內存中;
  • 以後會建立初始進程(0 / 1 / 2),後面會由一號進程來加載用戶態中其餘內容;

而若是你熟悉 Linux,你就會知道 Linux 啓動的入口函數是 start_kernel(在 init/main.c 中),它裏面都作了什麼比較重要的事情呢:bash

  • 0 號進程建立(後面會演變成 idle 進程);
  • 系統調用初始化;
  • 內存管理系統初始化;
  • 調度系統初始化;
  • 其餘初始化:
    • 1 號進程建立(用戶態);
    • 2 號進程建立(內核態);

Android init 進程啓動

上面提到 1 號進程,也叫 init 進程,而建立 1 號 init 進程時就會執行 Android 源碼中 system/core/init 下面的 main.cpp 了,它裏面會根據不一樣的參數調用不一樣的方法:socket

int main(int argc, char** argv) {
    // 略一部分
    // ueventd 主要用來建立設備節點
    if (!strcmp(basename(argv[0]), "ueventd")) {
        return ueventd_main(argc, argv);
    }
    if (argc > 1) {
        // 略一部分
        // selinux_setup
        if (!strcmp(argv[1], "selinux_setup")) {
            return SetupSelinux(argv);
        }
        // second_stage 
        if (!strcmp(argv[1], "second_stage")) {
            return SecondStageMain(argc, argv);
        }
    }

    return FirstStageMain(argc, argv);
}
複製代碼

經過對 system/core/init/README.md 的閱讀能夠知道 main 函數的會執行屢次,啓動順序是這樣的 FirstStageMain -> SetupSelinux -> SecondStageMain。

因此下面分開來看一下,這三個部分都作了作了什麼:

FirstStageMain

// 文件位置:system/core/init/first_stage_init.cpp
int FirstStageMain(int argc, char** argv) { 
    //  ...
    //  其實上面省略的基本是掛載文件系統、建立目錄、建立文件等操做
    //  好比掛載的有:tmpfs、devpts、proc、sysfs、selinuxfs 等
    //  把標準輸入、標準輸出、標準錯誤重定向到 /dev/null
    SetStdioToDevNull(argv);
    //  初始化本階段內核日誌
    InitKernelLogging(argv);
    //  ...
    //  好比獲取 「/」 的 stat(根目錄的文件信息結構),還會判斷是否強制正常啓動,而後切換 root 目錄
    //  這裏作了幾件事:初始化設備、建立邏輯分區、掛載分區
    DoFirstStageMount();
    //  ...
    //  再次啓動 main 函數,只不過此次傳入的參數是 selinux_setup
    const char* path = "/system/bin/init";
    const char* args[] = {path, "selinux_setup", nullptr};
    execv(path, const_cast<char**>(args));
}
複製代碼

第一階段更多的是文件系統掛載、目錄和文件的建立,爲何要掛載,這樣就能夠是使用它們了,這些都完成後就再次調用 main 函數,進入 SetupSelinux 階段。

SetupSelinux

// 文件位置:system/core/init/selinux.cpp
int SetupSelinux(char** argv) {
    //  初始化本階段內核日誌
    InitKernelLogging(argv);
    //  初始化 SELinux,加載 SELinux 策略
    SelinuxSetupKernelLogging();
    SelinuxInitialize();
    //  再次調用 main 函數,並傳入 second_stage 進入第二階段
    //  而且此次啓動就已經在 SELinux 上下文中運行
    const char* path = "/system/bin/init";
    const char* args[] = {path, "second_stage", nullptr};
    execv(path, const_cast<char**>(args));
}
複製代碼

這階段主要作的就是初始化 SELinux,那什麼是 SELinux 呢?其實就是安全加強型 Linux,這樣就能夠很好的對全部進程強制執行訪問控制,從而讓 Android 更好的保護和限制系統服務、控制對應用數據和系統日誌的訪問,下降惡意軟件的影響。

不過 SELinux 並非一次就初始化完成的,接下來就是再次調用 main 函數,進入最後的 SecondStageMain 階段。

SecondStageMain

//  文件位置:system/core/init/init.cpp
//  不那麼重要的地方就不貼代碼了
int SecondStageMain(int argc, char** argv) {
    //  又調用了這兩個方法
    SetStdioToDevNull(argv);
    //  初始化本階段內核日誌
    InitKernelLogging(argv);
    //  ...
    //  正在引導後臺固件加載程序
    close(open("/dev/.booting", O_WRONLY | O_CREAT | O_CLOEXEC, 0000));
    //  系統屬性初始化
    property_init();
    //  系統屬性設置相關,並且下面還有不少地方都在 property_set
    //  ...
    //  清理環境
    //  將 SELinux 設置爲第二階段 
    //  建立 Epoll
    Epoll epoll;
    //  註冊信號處理
    InstallSignalFdHandler(&epoll);
    //  加載默認的系統屬性
    property_load_boot_defaults(load_debug_prop);
    //  啓動屬性服務
    StartPropertyService(&epoll);
    //  重頭戲,解析 init.rc 和其餘 rc
    // am 和 sm 就是用來接收解析出來的數據
    //  裏面基本上是要執行的 action 和要啓動的 service
    LoadBootScripts(am, sm);
    //  往 am 裏面添加待執行的 Action 和 Trigger
    while (true) {
        //  執行 Action
        am.ExecuteOneCommand();
        //  還有就是重啓死掉的子進程
        auto next_process_action_time = HandleProcessActions();
    }
}
複製代碼

這是整個啓動階段最重要的部分,我以爲有四個比較重要的點,它們分別是屬性服務、註冊信號處理 、init.rc 解析以及方法尾部的死循環。

屬性服務

什麼是屬性服務,我以爲它更像關於這臺手機的各類系統信息,經過 key / value 的形式供咱們全部程序使用,下面內容就是個人模擬器進入 adb shell 後獲取到的屬性值,下面我從輸出結果裏面保留的一部分:

generic_x86:/ $ getprop
...
[dalvik.vm.heapsize]: [512m]
...
[dalvik.vm.usejit]: [true]
[dalvik.vm.usejitprofiles]: [true]
...
[init.svc.adbd]: [running]
...
[init.svc.gpu]: [running]
...
[init.svc.surfaceflinger]: [running]
...
[init.svc.zygote]: [running]
...
[ro.product.brand]: [google]
[ro.product.cpu.abi]: [x86]
...
[ro.serialno]: [EMULATOR29X2X1X0]
[ro.setupwizard.mode]: [DISABLED]
[ro.system.build.date]: [Sat Sep 21 05:19:49 UTC 2019]
...
//  zygote 啓動該啓動哪一個
[ro.zygote]: [zygote32]
[ro.zygote.disable_gl_preload]: [1]
[security.perf_harden]: [1]
[selinux.restorecon_recursive]: [/data/misc_ce/0]
...
[wifi.interface]: [wlan0]
複製代碼

屬性服務相關代碼在 SecondStageMain 階段其實主要作了三件事:建立共享內存、加載各類屬性值以及建立屬性服務的 Socket。下面是這關於這幾部分的片斷:

property_init {
    //  建立目錄 /dev/__properties__
    //  會從別的地方加載並解析屬性,而後寫到 /dev/__properties__/property_info 裏
    //  在 __system_property_area_init 的調用鏈跟蹤中,發現最終是經過 mmap 建立共享內存
}

property_load_boot_defaults {
    //  代碼中不少這樣的代碼
    load_properties_from_file("/system/build.prop", nullptr, &properties);
    load_properties_from_file("/vendor/default.prop", nullptr, &properties);
    load_properties_from_file("/vendor/build.prop", nullptr, &properties);
    load_properties_from_file("/product/build.prop", nullptr, &properties);
    load_properties_from_file("/product_services/build.prop", nullptr, &properties);
    load_properties_from_file("/factory/factory.prop", "ro.*", &properties);
    //  會調用 PropertySet 設置這些屬性值
}

StartPropertyService {
    //  建立 Sockte
    //  這個 Socket 就是用來處理系統屬性的,全部進程都經過它來修改共享內存裏面的系統屬性
    property_set_fd = CreateSocket(...);
    //  開始註冊監聽,handle_property_set_fd 是回調處理函數
    epoll->RegisterHandler(property_set_fd, handle_property_set_fd);
}
複製代碼

代碼上了理解起來並不那麼難,只是可能要問爲何要使用共享內存?Socket 做用是什麼?

首先共享內存是一種高效的進程間通訊方式,自己這些屬性值在內存中存在一份便可,不須要每一個進程都複製一份到本身的空間中,並且因爲是共享的,因此誰都能訪問。可是若是誰都能隨時來讀寫(除了只讀部分的屬性),那也仍是會出問題,可能會出現內容不一致問題,因此你們並非直接對共享內存進行操做,而是經過屬性服務的 socket 的對其進行操做,這樣就避免了因此進程直接對那塊共享內存進行操做。

註冊信號處理

在 SecondStageMain 階段,其實就是註冊了信號處理函數,從而能夠對底層信號做出響應。對應函數是:

InstallSignalFdHandler {
    //  ...
    //  註冊信號處理函數
    epoll->RegisterHandler(signal_fd, HandleSignalFd);
}

HandleSignalFd {
    //  ...
    //  ReapAnyOutstandingChildren 會對死掉的進程進行重啓
    SIGCHLD -> ReapAnyOutstandingChildren
    SIGTERM -> HandleSigtermSignal
    default -> 打印日誌
}

//  子進程異常退出後要標記須要從新啓動
ReapAnyOutstandingChildren {
    //  ...
    ReapOneProcess {
         // ...
        service.Reap {
            //  ...
            //  設置要重啓的標誌位,但這裏並非真的啓動
            flags_ &= (~SVC_RESTART);
            flags_ |= SVC_RESTARTING;
            onrestart_.ExecuteAllCommands();
        }
    }
}
複製代碼

init.rc 解析

init.rc 是什麼?它是很是重要的配置文件,並且衆多 rc 文件中 init.rc 是最主要的文件,不過這裏我不會講 rc 文件的語法是怎麼樣的,由於 system/core/init/README.md 中已經寫的很清楚了,init.rc 會根據 on 分紅不一樣階段,而且由 trigger 進行不一樣階段的觸發,而每一個階段裏面就是一條條要執行指令,好比 start 後面跟的就是要啓動的服務,mkdir 就是建立目錄。

既然分紅了多個階段,那先來看看觸發階段是怎麼樣的:

//  這三個階段是順序下去的,這三個階段的觸發順序是寫在 SecondStageMain 代碼中的
early-init -> init -> late-init

//  late-init 中再去觸發別的階段
on late-init
    trigger early-fs
    trigger fs
    trigger post-fs
    trigger late-fs
    trigger post-fs-data
    trigger load_persist_props_action
    //  這裏就是 zygote-start 啓動了
    trigger zygote-start
    trigger firmware_mounts_complete
    trigger early-boot
    trigger boot

複製代碼

那麼下面來看看 init.rc 解析在 SecondStageMain 階段都作了啥:

//  把這階段關於 rc 文件相關的一些重要代碼提取出來
int SecondStageMain(int argc, char** argv) {
    //  ...
    //  兩個用於存儲的容器
    ActionManager& am = ActionManager::GetInstance();
    ServiceList& sm = ServiceList::GetInstance();
    //  解析 init.rc
    LoadBootScripts(am, sm);
    //  ...
    //  加入觸發 early-init 語句
    am.QueueEventTrigger("early-init");
    //  ...
    //  加入觸發 init 語句
    am.QueueEventTrigger("init");
    //  ...
    //  代碼中還有不少 QueueBuiltinAction,插入要執行的 Action
    am.QueueBuiltinAction(InitBinder, "InitBinder");
    //  ...
    //  加入觸發 late-init 語句
      am.QueueEventTrigger("late-init");
}

LoadBootScripts(action_manager, service_list) {
    Parser parser = CreateParser(action_manager, service_list);
    //  系統屬性中去找 ro.boot.init_rc 對應的值
    std::string bootscript = GetProperty("ro.boot.init_rc", "");
    //  沒找到的話就去當前目錄找 init.rc 
    //  當前目錄就是 system/core/init/
    if (bootscript.empty()) {
        //  不管沒有找到最終解析的任務都是交給 ParseConfig 這個方法去處理 
        parser.ParseConfig("/init.rc");
        //  ... 
    } else {
        parser.ParseConfig(bootscript);
    }
}
複製代碼

其實上面的代碼寫主要作的就是解析 init.rc 文件中的內容,而且在加入要執行的動做。

方法尾部的死循環

這裏面主要作的就是執行剛入 ActionManager 中的動做和看看是否有須要重啓的進程。

while (true) {
    //  ...
    //  執行剛纔加入 ActionManager 的動做
    am.ExecuteOneCommand();
    //  ... 
    //  HandleProcessActions 纔是真正重啓進程的地方
    auto next_process_action_time = HandleProcessActions();
}

HandleProcessActions {
    //  ...
    //  對須要重啓的進行重啓,前面會有不少判斷
    auto result = s->Start();
}
    
複製代碼

到這裏大體的 init 進程啓動的三個階段基本上清晰了。

不過因爲是我第一次開始閱讀 AOSP 源碼,本篇文章討論的內容比較有限,其中還有不少細節的東西並無討論到,好比:

  • Linux 啓動流程的更多詳細內容;
  • 具體掛載的那些文件是什麼,它們都有什麼用途;
  • 屬性服務的完整讀寫流程是怎麼樣的;
  • 具體 init.rc 如何解析,如何執行;
  • zygote 的啓動等等;

不事後續部分,好比 zygote 我會盡可能在下次讀完以後分享出來的。

總結與收穫

若是你問我我讀完這些有什麼收穫,我以爲下面這三點是個人主要收穫:

  • 在某些狀況下(好比前期資源不足或者先後依賴),咱們能夠將大任務拆解,併合理分配好執行次序(包括順序、串並行安排等等),進而經過多階段任務的配合從而完成一個總體的執行目標;
  • 當資源是共享的時候,最好不要否則全部人都直接對資源進行操做,而是引入中間人,你們只和中間人交互,具體資源由中間人和其交互;
  • 代碼跑起來很重要,可是一個合理的監控模塊也很是須要,這樣能夠在必要的時候檢測出問題並及時做出響應;

感謝與參考

以上內容,除了源碼自己外,還參考瞭如下連接(順序不分前後):

計算機是如何啓動的?

Linux 的啓動流程

07 | 從BIOS到bootloader:創業伊始,有活兒老闆本身上

08 | 內核初始化:生意作大了就得成立公司

Android啓動流程簡析(一)

Linux 引導過程內幕

Linux下0號進程的前世(init_task進程)此生(idle進程)----Linux進程的管理與調度(五)

Android 中的安全加強型 Linux

Android系統啓動-Init篇

深刻研究源碼:Android10.0系統啓動流程(二)init進程

Android P (9.0) 之Init進程源碼分析

Android系統啓動流程之init進程啓動

Android啓動流程——1 2 3

最後的最後

源碼自己沒有歧義,不過因爲每一個人基礎不一樣,具體理解起來可能會些不一樣,因此有什麼問題,也請你們多指點,多交流。

若是你以爲我寫的還不錯的話,那就經過點贊,點贊,還 tm 是點讚的方式給我反饋吧,感謝你的支持。

相關文章
相關標籤/搜索