::: hljs-center
OHOS 3.1的Init程序two_stages相關分析-1-編譯部分
:::
::: hljs-center
梁開祝 2022.05.04
:::
【注:本文可做為《沉浸式剖析OpenHarmony源代碼》一書的第5章的5.4小節部分内容的大綱或草稿。】
OHOS LTS3.0版本的标準系統還不支援two_stages,3.1版本開始支援。這裡的two_stages是指OHOS 3.1之後的标準系統,從核心态切換到使用者态運作init程序時,分成兩個stages來完成系統的啟動工作:
- stage0運作在ramdisk中,主要是生成裝置節點、挂載根檔案系統,并切換到stage1去運作;
- stage1完成OHOS架構各子產品、各程序的啟動工作。
不過,OHOS 3.1标準系統燒錄到HI3516DV300開發闆,跑起來相當吃力,是以本文将基于DAYU200開發闆,分别從編譯和實作兩大部分來對two_stages展開分析,最後再通過log确認一遍相關流程。
【本文很長,分兩篇文章來釋出:編譯部分 和 實作部分】
1.編譯部分
1.1 OHOS 3.1簡明編譯流程
請先去《OHOS3.1 簡明編譯流程》閱讀和簡單了解一下OHOS3.1系統的編譯流程。
1.2 GN階段與Ramdisk相關部分
在《OHOS3.1 簡明編譯流程》的“6.gn”步驟會執行“gn_gen”去預處理所有的.gn檔案,生成對應的.ninja檔案。其中與ramdisk相關的部分(局部),如下圖1所示。
![](https://img.laitimes.com/img/_0nNw4CM6IyYiwiM6ICdiwiI9s2RkBnVHFmb1clWvB3MaVnRtp1XlBXe0xCMy81dvRWYoNHLwEzX5xCMx8FesU2cfdGLwMzX0xiRGZkRGZ0Xy9GbvNGLpZTY1EmMZVDUSFTU4VFRR9Fd4VGdsQTMfVmepNHLrJXYtJXZ0F2dvwVZnFWbp1zczV2YvJHctM3cv1Ce-cGcq5CMyAjN5ETN1QWM4QTZyU2NyYzXyETOwgTMzEzLcVDMyIDMy8CXn9Gbi9CXzV2Zh1WavwVbvNmLvR3YxUjL0M3Lc9CX6MHc0RHaiojIsJye.jpg)
在//productdefine/common/device/rk3568.json檔案裡,定義了enable_ramdisk為true,它将會作為全局參數引入RK3568項目的編譯流程中。
GN執行到//device/board/hihope/rk3568/BUILD.gn時,這裡的group("rk3568_group")的deps關系中,就有對enable_ramdisk的判斷和使用。其中的:
-
"cfg:init_configs":會拷貝fstab.required檔案到//out/rk3568/目錄下備用,也會把ramdisk_resource_config.ini檔案拷貝到//out/rk3568/packages/phone/目錄下備用。
注意,這裡不再把ramdisk_resource_config.ini檔案拷貝到//build/ohos/images/mkimage/目錄下了,可以避免出現文章《OHOS3.1 cannot stat 'packages/phone/../../../../ramdisk.img》中描述的異常。
- "kernel:kernel":裡面的action("kernel")會在後面的核心編譯階段執行"build_kernel.sh"腳本,enable_ramdisk将作為其中一個參數參與編譯核心。
- "updater:updater_files":這一步會把會把updater_ramdisk_resource_config.ini檔案拷貝到//out/rk3568/packages/phone/目錄下備用。【後文暫不深入對updater_ramdisk_resource_config.ini進行分析】
在gn_gen階段,//build/core/gn/BUILD.gn中還定義了group("packages")和group("images") ,其中的make_packages和make_images的動作,是在ninja的最後階段去執行的,enable_ramdisk也作為重要的參數去生成燒錄鏡像,如下圖2所示。
1.3 Ninja階段與Ramdisk相關部分
在《OHOS3.1 簡明編譯流程》的“6.ninja”步驟會執行“ninja”程式,根據上一步生成的.ninja檔案去生成所有的中間檔案(.a/.o/.so/配置檔案/可執行檔案......)。
1.3.1 group("rk3568_group")
在執行到group("rk3568_group")時,會将fstab.required、ramdisk_resource_config.ini等檔案拷貝到對應的目錄備用。
1.3.2 action("kernel")
在執行到action("kernel")時,會執行build_kernel.sh去編譯核心,并把resource.img、parameter.txt、MiniLoaderAll.bin、uboot.img、config.cfg等檔案拷貝到//out/rk3568/packages/phone/images/中,注意:
if [ "enable_ramdisk" != "${6}" ]; then
cp ${KERNEL_OBJ_TMP_PATH}/boot_linux.img ${2}/boot_linux.img
fi
這裡是不支援enable_ramdisk時,才會直接拷貝boot_linux.img;支援enable_ramdisk的話,這裡先不拷貝,等後面group("images")階段生成ramdisk.img時才會通過執行make-boot.sh,去重新生成boot_linux.img并拷貝到//out/rk3568/packages/phone/images/中。
如上圖所示,前面5個檔案都是在build_kernel.sh時拷貝到這裡的;後面5個.img檔案,除了boot_linux.img是在make-boot.sh生成并拷貝到這裡之外,其餘4個都是group("images")階段執行build_image.py腳本生成并拷貝到這裡的。
1.3.3 group("packages")
在執行到group("packages")時,其中一步會執行modules_install.py腳本,先删除//out/rk3568/packages/phone/目錄下已存在的system、vendor、updater、ramdisk等目錄,并重新生成它們。
此時注意看ramdisk和system子目錄下的内容,用tree指令将其目錄樹結構列印出來,如下:
這裡順便把root、userdata、vendor、updater子目錄的一級目錄結構放上來做一下對比和參考。
上兩圖中,共計有三個init可執行程式:
- ramdisk子目錄下的init(暫标記為initA)可執行程式
- system子目錄下的init(暫标記為initB)可執行程式
-
updater子目錄下的init(暫标記為initA')可執行程式
三者bit-to-bit的,你可以認為它們是同一個可執行程式的三份拷貝,它們都是由//base/startup/init_lite/目錄下的代碼編譯出來的init可執行程式。
但是,從另外一個角度來看,initA、initB、initA’是三個完全不同的可執行程式!!!這個會在實作部分做詳細說明。
1.3.4 group("images")
在執行到group("images")時,會執行build_image.py腳本去生成ramdisk.img、system.img、vendor.img、userdata.img鏡像。
通過參數确定需要生成哪個image,先删除已經存在的image,再重新生成對應的子目錄和link檔案,執行mkimages.py去make具體的image。
mkimages.py會根據參數先加載config_file,即xxx_image_conf.txt檔案,根據檔案内的fs_type參數決定調用哪個工具(mkf2fsimage.py、mkextimage.py、mkcpioimage.py)去生成對應的image。xxx_image_conf.txt和dac.txt檔案的使用說明,見同目錄下的README.txt檔案。
生成這些鏡像的簡單流程,可以參考1.2小節的圖2的右半部分。
-
生成ramdisk.img
流程見1.2小節的圖2的右半部分,分成三步:
[3-1] 根據ramdisk_resource_config.ini的描述,通過cpio工具将//out/rk3568/packages/phone/ramdisk/目錄打包成//out/rk3568/ramdisk.img。
[3-2] 執行make-boot.sh,将//out/rk3568/ramdisk.img拷貝到//out/kernel/src_tmp/linux-5.10/boot_linux/extlinux/目錄下,根據同目錄下的extlinux.conf的描述,用mke2fs工具生成boot_linux.img,并拷貝到//out/rk3568/packages/phone/images/目錄下。
[3-3] 支援two_stages時,這一步并不跑,直接return 0了。
這裡生成的boot_linux.img鏡像,我沒有仔細研究鏡像制作的細節,不大清楚img内部結構具體是什麼樣的,但大概估計一下,可能如下:
OHOS 3.1的Init程式two_stages相關分析
-
生成system.img
生成system.img,也可以參考1.2小節的圖2的右半部分,具體流程圖我就不畫了。
mk_images()會先調用build_rootdir(src_dir),來把root目錄和system目錄拼接在一起,再調用mkextimage.py将它們制作成ext4格式的system.img。我沒有仔細研究鏡像制作的細節,不大清楚img内部結構具體是什麼樣的,但大概估計一下,可能如下:
OHOS 3.1的Init程式two_stages相關分析 生成其它的images的過程,請小夥伴們自行分析一下。
2.實作部分
2.1 initA(stage0)和initB(stage1)的差異
為什麼我說initA(stage0)和initB(stage1)是兩個完全不同的可執行程式呢?看一下init的實作代碼就可以知道了,見//base/startup/init_lite/services/init/main.c
static const pid_t INIT_PROCESS_PID = 1;
int main(int argc, char * const argv[])
{
int isSecondStage = 0;
// Number of command line parameters is 2
if (argc == 2 && (strcmp(argv[1], "--second-stage") == 0)) {
isSecondStage = 1;
}
if (getpid() != INIT_PROCESS_PID) {
return 0;
}
//OHOS 的log部分還沒有初始化,在這裡用INIT_LOGI是無法列印log出來的。
printf("###############################################################\n");
printf("##################[Init ] [Stage%d] [%s]###################\n",
isSecondStage, isSecondStage?"/System":"RamDisk");
if (isSecondStage == 0) { //Stage0
printf(" [-][Init ] [main.c] init_main[5-1][Stage0]: SystemPrepare()\n");
SystemPrepare();
} else { //Stage1
printf(" [-][Init ] [main.c] init_main[5-1][Stage1]: LogInit()\n");
LogInit();
}
INIT_LOGI("init_main[5-2][Stage%d]: SystemInit()\n", isSecondStage);
SystemInit();
INIT_LOGI("init_main[5-3][Stage%d]: SystemExecuteRcs()\n", isSecondStage);
SystemExecuteRcs();
INIT_LOGI("init_main[5-4][Stage%d]: SystemConfig() --> DoJob\n", isSecondStage);
SystemConfig();
INIT_LOGI("init_main[5-5][Stage%d]: SystemRun() --> Looping...\n", isSecondStage);
SystemRun();
INIT_LOGI("init_main[5-5][Stage%d]: End.\n", isSecondStage); //never run to this step
return 0;
}
initA即stage0,它在SystemPrepare()裡面的StartInitSecondStage()的最後一步,通過執行execv("/bin/init", args),切換到initB即stage1去運作了,并不會跑剩餘的[5-2/3/4/5]幾個步驟,而是讓stage1的initB來跑。initB也不跑SystemPrepare()部分,而是去跑LogInit()以及接下來的[5-2/3/4/5]幾個步驟。
如流程圖3所示。
由上圖可知:
- initA就僅僅是跑SystemPrepare()而已;
- initA’又僅僅是跑SystemPrepare()的一部分,就轉去執行"/bin/updater"了;
-
initB則是跑init中除了SystemPrepare()之外的其餘部分。
從這個角度來看,就可以認為initA、initB、initA’是“三個完全不同的”可執行程式了。
接下來我們看一下initA和initB的具體實作。
2.2 initA(stage0)的實作和流程
SystemPrepare()的前幾步,initA和initA’基本相同,沒啥說的,看一下代碼就明白了。不同的地方是initA’不跑StartInitSecondStage(),而是轉去執行"/bin/updater"跑更新流程去了,這裡不展開分析。
我們關注一下StartInitSecondStage()裡面的五大步驟,用【5-1/2/3/4/5】标記。
【5-1】Fstab* fstab = LoadRequiredFstab()
這一步會去讀取并解析“/etc/fstab.required”檔案,這個檔案就是編譯時拷貝到//out/rk3568/目錄下的那個,在制作ramdisk鏡像和system鏡像時,會再次拷貝到鏡像的/etc/目錄下被使用。
char **devices = GetRequiredDevices(*fstab, &requiredNum)會讀取其中帶有“required”flag的裝置。
注意其中的userdata塊裝置,沒有帶“required”flag;而misc塊裝置,類型是none。
【5-2】StartUeventd(devices, requiredNum)
通過uevent機制去為塊裝置建立DeviceNode,中間過程稍微有點複雜,這裡不展開分析,請感興趣的小夥伴們自行閱讀代碼了解一下。
【5-3】MountRequriedPartitions(fstab)
這一步會去按fstab的描述,會把system、vendor兩個塊裝置分别挂載到ramdisk根目錄下的/usr、/vendor路徑下,而/userdata塊裝置會因為沒有“required”flag而推遲到stage1才去挂載,/misc塊裝置會因為檔案類型為“none”而挂載失敗,可以先不用管。
這樣,ramdisk目錄結構就變成了如下的樣子:
【5-4】SwitchRoot("/usr")
在真正 SwitchRoot 之前,我把目前路徑(ramdisk)下的一級目錄列印了出來,如log中下面這一小段所示:
SwitchRoot: [-]Before SwitchRoot:
.
[d]vendor/
[d]lib/
[d]etc/
[d]sys/
[d]storage/
[d]usr/
[d]mnt/
[l]init //link to 'bin/init',即initA
[d]system/
[d]bin/
[d]proc/
[d]root/ //空目錄,暫不知道哪裡生成的
[d]dev/
這基本上契合了【5-3】步驟後的ramdisk的目錄結構,隻是我還沒找到root這個空目錄是在哪裡生成的。在build_image.py的_prepare_ramdisk()中,并沒有在ramdisk中生成root目錄(或挂載點),在SystemPrepare()的前幾步中也沒看到要生成root目錄(或挂載點)的地方。
SwitchRoot("/usr")這一步,非常關鍵,裡面做了以下一組事情,如log所示:
SwitchRoot: [0]Switch root from ramdisk's '/' to '/usr' Begin:
SwitchRoot: MountToNewTarget('/usr')
MountToNewTarget: [0] continue [/]: [-][is '/'][-]
MountToNewTarget: [1]Move mount [/vendor] to [/usr/vendor]
MountToNewTarget: [2] continue [/usr]: [-][-][mountPoint is same]
MountToNewTarget: [3] continue [/sys/fs/selinux]: already UnderBasicMountPoint
MountToNewTarget: [4]Move mount [/sys] to [/usr/sys]
MountToNewTarget: [5]Move mount [/proc] to [/usr/proc]
MountToNewTarget: [6] continue [/dev/pts]: already UnderBasicMountPoint
MountToNewTarget: [7]Move mount [/storage] to [/usr/storage]
MountToNewTarget: [8]Move mount [/mnt] to [/usr/mnt]
MountToNewTarget: [9]Move mount [/dev] to [/usr/dev]
SwitchRoot: chdir('/usr')
SwitchRoot: mount('/usr' to '/')
SwitchRoot: chroot('.')
FreeOldRoot: Failed to unlink[init], err = 20
SwitchRoot: [0]Switch root from ramdisk's '/' to '/usr' End. OK
簡單來說就是把stage0的“/proc/mounts”上描述的、挂載到ramdisk根目錄下的各個裝置節點,全部統一重新挂載到/usr/路徑下對應節點上。這個/usr/就是【5-3】步驟挂載上去的system.img所描述的塊裝置。
需要注意的是,這裡的“/proc/mounts”是stage0階段的裝置挂載資訊,與系統跑完stage1之後,我們在shell上“cat /proc/mounts”所看到的資訊,可能還有點不一樣,這個請小夥伴們自行确認一下。
Move mount步驟之後,再通過chdir(‘/usr’)、mount('/usr' to '/')、chroot('.')操作,把原先的ramdisk根目錄替換成以/usr為根的新的目錄結構。
在執行完 SwitchRoot 之後,我再次把目前路徑(已經切換到usr/)下的一級目錄列印出來,如下log所示:
SwitchRoot: [-]After SwitchRoot:
.
[d]storage/
[d]chip_prod/
[d]chipset/
[d]mnt/
[d]tmp/
[d]sys_prod/
[d]data/
[l]etc
[d]vendor/
[d]sys/
[d]proc/
[d]dev/
[l]bin
[l]init
[l]lib
[d]lost+found/
[d]updater/
[d]config/
[d]system/
chroot之後,系統的根目錄結構,就變成了:
【5-5】execv("/bin/init", args)
這一步就很明朗了,args定義為:
char * const args[] = {
"/bin/init",
"--second-stage",
NULL,
};
printf("StartInitSecondStage[5-5]: execv('/bin/init')-->>[Stage1]\n");
if (execv("/bin/init", args) != 0) {
INIT_LOGE("Failed to exec \"/bin/init\", err = %d", errno);
exit(-1);
}
帶參數去運作/bin/init,這個init就是新的root下的/bin/init,也就是前面說的initB。
stage0的initA程序到此就結束了,它的上下文環境仍然保持不變,但是從這裡開始切換去運作initB,即流程圖3的右邊綠色部分,進入stage1。
2.3 initB(stage1)的實作和流程
這一階段就是OHOS架構的啟動入口了,請小夥伴自己閱讀代碼去了解一下。
3.Log确認流程
我對init程序的two_stages流程做了一下整理,把相關log列印出來,完整的log如附件所示。
4.思考與讨論
為什麼要引入這麼複雜的啟動流程?有什麼好處?