天天看點

OHOS 3.1的Init程序two_stages相關分析

::: 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所示。

OHOS 3.1的Init程式two_stages相關分析

在//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所示。

OHOS 3.1的Init程式two_stages相關分析

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/中。

OHOS 3.1的Init程式two_stages相關分析

如上圖所示,前面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指令将其目錄樹結構列印出來,如下:

OHOS 3.1的Init程式two_stages相關分析

這裡順便把root、userdata、vendor、updater子目錄的一級目錄結構放上來做一下對比和參考。

OHOS 3.1的Init程式two_stages相關分析

上兩圖中,共計有三個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所示。

OHOS 3.1的Init程式two_stages相關分析

由上圖可知:

  • 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。

OHOS 3.1的Init程式two_stages相關分析

【5-2】StartUeventd(devices, requiredNum)

通過uevent機制去為塊裝置建立DeviceNode,中間過程稍微有點複雜,這裡不展開分析,請感興趣的小夥伴們自行閱讀代碼了解一下。

【5-3】MountRequriedPartitions(fstab)

這一步會去按fstab的描述,會把system、vendor兩個塊裝置分别挂載到ramdisk根目錄下的/usr、/vendor路徑下,而/userdata塊裝置會因為沒有“required”flag而推遲到stage1才去挂載,/misc塊裝置會因為檔案類型為“none”而挂載失敗,可以先不用管。

這樣,ramdisk目錄結構就變成了如下的樣子:

OHOS 3.1的Init程式two_stages相關分析

【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之後,系統的根目錄結構,就變成了:

OHOS 3.1的Init程式two_stages相關分析

【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.思考與讨論

為什麼要引入這麼複雜的啟動流程?有什麼好處?

繼續閱讀