閱讀源碼的文章會是一個系列,本篇主要內容是 Android 源碼中啓動流程的第一部分,包含了 Linux 內核啓動部分與 Android init 進程啓動部分。html
爲何我會先提 Linux 的啓動呢?一方面 Linux 內核是 Android 平臺的基礎,另外一方面我最近接觸了一些 Linux 的基礎知識,因此但願把這些學到的東西也都記錄下來。linux
內核的做用其實就是控制計算機硬件資源並提供程序運行環境,具體的好比有:執行程序、文件操做、內存管理及設備驅動等,而內核對外提供的接口也被稱爲系統調用。android
既然內核這麼重要,提供了各類程序運行所需的服務,那啓動 Android 前確定是須要先把內核啓動起來的。具體內核如何啓動,咱們先來看看當咱們按下開機鍵後都發生了什麼。git
計算機通電後首先會去找 ROM(只讀內存),這裏面被固化了一些初始化程序,這個程序也叫 BIOS,具體幾步就像下面這樣:github
讀取 BIOS(基本輸入輸出系統,放在 ROM 中):shell
主引導記錄(BIOS 中把控制權交給啓動順序的第一位):json
經過 boot loader 啓動操做系統:安全
而若是你熟悉 Linux,你就會知道 Linux 啓動的入口函數是 start_kernel(在 init/main.c 中),它裏面都作了什麼比較重要的事情呢:bash
上面提到 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。
因此下面分開來看一下,這三個部分都作了作了什麼:
// 文件位置: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 階段。
// 文件位置: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 階段。
// 文件位置: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 是什麼?它是很是重要的配置文件,並且衆多 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 源碼,本篇文章討論的內容比較有限,其中還有不少細節的東西並無討論到,好比:
不事後續部分,好比 zygote 我會盡可能在下次讀完以後分享出來的。
若是你問我我讀完這些有什麼收穫,我以爲下面這三點是個人主要收穫:
以上內容,除了源碼自己外,還參考瞭如下連接(順序不分前後):
07 | 從BIOS到bootloader:創業伊始,有活兒老闆本身上
Linux下0號進程的前世(init_task進程)此生(idle進程)----Linux進程的管理與調度(五)
深刻研究源碼:Android10.0系統啓動流程(二)init進程
源碼自己沒有歧義,不過因爲每一個人基礎不一樣,具體理解起來可能會些不一樣,因此有什麼問題,也請你們多指點,多交流。
若是你以爲我寫的還不錯的話,那就經過點贊,點贊,還 tm 是點讚的方式給我反饋吧,感謝你的支持。