Android小知識-深入淺出Android系統啟動流程(上)

顧林海發表於2019-01-17

本篇文章已授權微信公眾號 顧林海 獨家釋出

init程式啟動過程

Android系統基於Linux,init程式是Android系統中使用者空間的第一個程式,程式號為1,init原始碼在system/core/init目錄下。既然init程式是Android系統使用者空間的第一個程式,因此擔負著非常重要的責任,主要負責以下兩件事:

  1. 解析配置init.rc,然後啟動系統各種native程式,比如Zygote程式、SurfaceFlinger程式以及media程式等。

  2. 初始化並啟動屬性服務。

init程式的入口函式

init的入口函式是main,程式碼如下所示:

//路徑:/system/core/init/init.cpp
int main(int argc, char** argv) {
    //註釋1
    if (!strcmp(basename(argv[0]), "ueventd")) {
        return ueventd_main(argc, argv);
    }
    //註釋2
    if (!strcmp(basename(argv[0]), "watchdogd")) {
        return watchdogd_main(argc, argv);
    }
    //註釋3
    if (REBOOT_BOOTLOADER_ON_PANIC) {
        install_reboot_signal_handlers();
    }

    ...
}
複製程式碼

註釋1處判斷當前程式是不是ueventd。init程式建立子程式ueventd,並將建立裝置節點檔案的工作託付給ueventd。ueventd主要是負責裝置節點的建立、許可權設定等一系列工作。服務通過使用uevent,監控驅動傳送的訊息,做進一步處理。

ueventd通過兩種方式建立裝置節點檔案。

  1. “冷插拔”(Cold Plug),即以預先定義的裝置資訊為基礎,當ueventd啟動後,統一建立裝置節點檔案。這一類裝置節點檔案也被稱為靜態節點檔案。

  2. “熱插拔”(Hot Plug),即在系統執行中,當有裝置插入USB埠時,ueventd就會接收到這一事件,為插入的裝置動態建立裝置節點檔案。這一類裝置節點檔案也被稱為動態節點檔案。

註釋2處判斷當前程式是不是watchdogd。Android系統在長時間的執行下會面臨各種軟硬體的問題,為了解決這個問題,Android開發了WatchDog類作為軟體看門狗來監控SystemServer中的執行緒,一旦發現問題,WatchDog會殺死SystemServer程式,SystemServer的父程式Zygote接收到SystemServer的死亡訊號後,會殺死自己。Zygote程式死亡的訊號傳遞到init程式後,init程式會殺死Zygote程式所有的子程式並重啟Zygote。

註釋3處判斷是否緊急重啟,如果是緊急重啟,就安裝對應的訊息處理器。

//路徑:/system/core/init/init.cpp
int main(int argc, char** argv) {
    ...
    //註釋1
    add_environment("PATH", _PATH_DEFPATH);
    //註釋2
    bool is_first_stage = (getenv("INIT_SECOND_STAGE") == nullptr);

    if (is_first_stage) {
        // 用於記錄啟動時間
        boot_clock::time_point start_time = boot_clock::now();
        // 清除遮蔽字(file mode creation mask),保證新建的目錄的訪問許可權不受遮蔽字影響
        umask(0);
        // 掛載tmpfs檔案系統
        mount("tmpfs", "/dev", "tmpfs", MS_NOSUID, "mode=0755");
        mkdir("/dev/pts", 0755);
        mkdir("/dev/socket", 0755);
        // 掛載devpts檔案系統
        mount("devpts", "/dev/pts", "devpts", 0, NULL);
        #define MAKE_STR(x) __STRING(x)
        // 掛載proc檔案系統
        mount("proc", "/proc", "proc", 0, "hidepid=2,gid=" MAKE_STR(AID_READPROC));
        //8.0新增, 收緊了cmdline目錄的許可權
        chmod("/proc/cmdline", 0440);
        // 8.0新增,增加了個使用者組
        gid_t groups[] = { AID_READPROC };
        setgroups(arraysize(groups), groups);
        // 掛載sysfs檔案系統
        mount("sysfs", "/sys", "sysfs", 0, NULL);
        // 8.0新增
        mount("selinuxfs", "/sys/fs/selinux", "selinuxfs", 0, NULL)
        // 提前建立了kmsg裝置節點檔案,用於輸出log資訊
        mknod("/dev/kmsg", S_IFCHR | 0600, makedev(1, 11));
        mknod("/dev/random", S_IFCHR | 0666, makedev(1, 8));
        mknod("/dev/urandom", S_IFCHR | 0666, makedev(1, 9));
        ...
    }
    ...
}
複製程式碼

註釋1處新增環境變數。註釋2處獲取本次啟動是否是系統啟動的第一階段,如果是第一階段,進入下面的if語句中,建立並掛載相關的檔案系統。

以上建立並掛載的五類檔案系統分別如下所示:

  • tmpfs:一種虛擬記憶體檔案系統,它會將所有的檔案儲存在虛擬記憶體中,如果你將tmpfs檔案系統解除安裝後,那麼其下的所有的內容將不復存在。tmpfs既可以使用RAM,也可以使用交換分割槽,會根據你的實際需要而改變大小。tmpfs的速度非常驚人,畢竟它是駐留在RAM中的,即使用了交換分割槽,效能仍然非常卓越。由於tmpfs是駐留在RAM的,因此它的內容是不持久的。斷電後,tmpfs的內容就消失了,這也是被稱作tmpfs的根本原因。

  • devpts:為偽終端提供了一個標準介面,它的標準掛接點是/dev/ pts。只要pty的主複合裝置/dev/ptmx被開啟,就會在/dev/pts下動態的建立一個新的pty裝置檔案。

  • proc:一個非常重要的虛擬檔案系統,它可以看作是核心內部資料結構的介面,通過它我們可以獲得系統的資訊,同時也能夠在執行時修改特定的核心引數。

  • sysfs:與proc檔案系統類似,也是一個不佔有任何磁碟空間的虛擬檔案系統。它通常被掛接在/sys目錄下。sysfs檔案系統是Linux2.6核心引入的,它把連線在系統上的裝置和匯流排組織成為一個分級的檔案,使得它們可以在使用者空間存取。

  • selinuxfs:用於支援SELinux的檔案系統,SELinux提供了一套規則來編寫安全策略檔案,這套規則被稱之為 SELinux Policy 語言。

//路徑:/system/core/init/init.cpp
int main(int argc, char** argv) {
    ...
    if (is_first_stage) {
        ...
        //重定向輸入輸出/核心Log系統
        InitKernelLogging(argv);
        LOG(INFO) << "init first stage started!";
        //掛在一些分割槽裝置
        if (!DoFirstStageMount()) {
            LOG(ERROR) << "Failed to mount required partitions early ...";
            panic();
        }
        //註釋1
        SetInitAvbVersionInRecovery();
        //註釋2
        selinux_initialize(true);
        ...
    }
    ...
}
複製程式碼

註釋1處初始化安全框架AVB(Android Verified Boot),AVB主要用於防止系統檔案本身被篡改,還包含了防止系統回滾的功能,以免有人試圖回滾系統並利用以前的漏洞。註釋2處呼叫selinux_initialize啟動SELinux。

//路徑:/system/core/init/init.cpp
int main(int argc, char** argv) {
    ...
    if (is_first_stage) {
        ...
    }
    ...
    //註釋1
    property_init();  
    ...
    //註釋2
    signal_handler_init();
    ...
    //註釋3
    start_property_service();
    ...
}
複製程式碼

註釋1處通過property_init函式對屬性服務進行初始化,註釋3通過start_property_service函式啟動屬性服務。註釋2處signal_handler_init設定子程式退出的訊號處理函式,當子程式異常退出的時候,init程式會去捕獲異常資訊,當它捕獲到這些異常資訊之後,就會呼叫該函式設定的相應的捕獲函式來處理。比如init程式的子程式Zygote死之後,init程式捕獲到這些異常資訊,就會呼叫handle_signal()函式去重啟Zygote程式。

//路徑:/system/core/init/init.cpp
int main(int argc, char** argv) {
    ...
    if (is_first_stage) {
        ...
    }
    ...
     if (bootscript.empty()) {
        //註釋1
        parser.ParseConfig("/init.rc");
        ...
    } else {
        ...
    }
    ...
    while (true) {
        ...
        ServiceManager::GetInstance().IsWaitingForExec())) {
            //註釋2
            am.ExecuteOneCommand();
        }
        if (!(waiting_for_prop || ServiceManager::GetInstance().IsWaitingForExec())) {
            //註釋3
            restart_processes();
            ...
        }
        ...
    }    
    return 0;
}
複製程式碼

註釋1處解析init.rc配置檔案,在註釋2處執行子程式對應的命令,也就是執行init.rc檔案裡配置的命令。在註釋3處重啟死掉的service。

解析init.rc

在init.rc中使用的語言稱為Android Init Language,翻譯過來就是“Android初始化語言”,init語言共有五種型別的表示式,分別如下所示:

  • Action:Action中包含了一系列的Command。

  • Command:init語言中的命令。

  • Service:由init程式啟動的服務。

  • Option:對服務的配置選項。

  • Import:引入其他配置檔案。

Action表示式的語法如下所示:

on <trigger> [&& <trigger>]*
    <command>
    <command>
    <command>
複製程式碼

這裡的trigger是Action執行的觸發器,當觸發器條件滿足時,command會被執行。觸發器有如下兩類:

  • 事件觸發器:當指定的事件發生時觸發。事件可能由“trigger”命令發出,也可能是init程式通過QueueEventTrigger()函式發出。

  • 屬性觸發器:當指定的屬性滿足某個值時觸發。

Action中的Command是init語言定義的命令,所有支援的命令如下表:

命令 引數格式 說明
bootchart_init - 啟動bootchart
chmod octal-mode path 改變檔案的訪問許可權
chown owner group path 改變檔案的擁有者和組
class_start serviceclass 啟動指定類別的服務
class_stop serviceclass 停止並“disable”指定類別的服務
class_reset serviceclass 停止指定類別的服務,但是不“disable”它們
copy src dst 複製檔案
domainname name 設定域名
enable servicename enable一個被disable的服務
exec [seclabel[user[group]]] -- command [argument]* fork一個子程式來執行指定的命令
export name value 匯出環境變數
hostname name 設定host名稱
ifup iterface 使網路卡線上
insmod path 安裝指定路徑的模組
load_all_props - 從/system、/vendor等路徑載入屬性
load_persist_props - 載入持久化的屬性
loglevel level 設定核心的日誌級別
mkdir path[mode][owner][group] 建立目錄
mount_all fstab[path]*[--option] 掛載檔案系統並且匯入指定的.rc檔案
mount typedevicedir[flag]*[options] 掛載一個檔案系統
powerctl - 內部實現使用
restart service 重啟服務
restorecon path[path]* 設定檔案的安全上下文
restorecon_recursive path[path]* restorecon的遞迴版本
rm path 對於指定路徑呼叫unlink(2)
rmdir path 刪除資料夾
setprop namevalue 設定屬性值
setrlimit resourcecurmax 指定資源的rlimit
start service 啟動服務
stop service 停止服務
swapon_all fstab 在指定檔案上呼叫fs_mgr_swapon_all
symlink targetpath 建立符合連結
sysclktz mins_west_of_gmt 指定系統時鐘基準
trigger event 觸發一個事件
umount path ummount指定的檔案系統
verity_load_state - 內部實現使用
verity_update_state mount_point 內部實現使用
wait path[timeout] 等待某個檔案存在直到超時,若存在則直接返回
write pathcontent 寫入內容到指定檔案

Service是init程式啟動的可執行程式,Service表示式的語法如下所示:

service <name> <pathname> [ <argument> ]*    <option>
    <option>
複製程式碼

Option是對服務的修飾,它們影響著init程式如何以及何時啟動服務。所有支援的Option入下所示:

Option 引數格式 說明
critical - 標識為系統關鍵服務,該服務若退出多次將導致系統重啟到recovery模式
disabled - 不會隨著類別自動啟動,必須明確start
setenv name value 為啟動的程式設定環境變數
socket nametypeperm[user[group[seclabel]]] 建立UNIX Domain Socket
user username 在執行服務之前切換使用者
group groupname[groupname]* 在執行服務之前切換組
seclabel seclabel 在執行服務之前切換seclabel
oneshot - 一次性服務,死亡後不用重啟
class name 指定服務的類別
onrestart - 當服務重啟時執行命令
writepid file... 寫入子程式的pid到指定檔案

import是一個關鍵字,而不是一個命令,可以在.rc檔案中通過這個關鍵字來載入其他的.rc檔案,它的語法如下:

import path
複製程式碼

path可以是另一個.rc檔案,也可以是一個資料夾。如果是資料夾,那麼這個資料夾下面的所有檔案都會被匯入,但是它不會迴圈載入子目錄中的檔案。

啟動Zygote

init.rc檔案有如下配置程式碼:

...import /init.${ro.zygote}.rc...on nonencrypted
    class_start main
    class_start late_start...    
複製程式碼

在init.rc檔案的開頭使用了import型別語句來引入Zygote啟動指令碼,其中ro.zygote根據不同的內容引入不同的檔案,從Android 5.0開始,Android開始支援64位程式,Zygote就有了32位和64位之分,如下圖所示:

image

檢視init.zygote64.rc的程式碼如下所示:

service zygote /system/bin/app_process64 -Xzygote /system/bin --zygote --start-system-server
    class main
    priority -20    user root    group root readproc
    socket zygote stream 660 root system    onrestart write /sys/android_power/request_state wake
    onrestart write /sys/power/state on
    onrestart restart audioserver
    onrestart restart cameraserver
    onrestart restart media
    onrestart restart netd
    onrestart restart wificond
    writepid /dev/cpuset/foreground/tasks
複製程式碼

Service用於通知init程式建立名為zygote的程式,這個程式執行程式的路徑為/system/bin/app_process64,後面的程式碼是傳遞給app_process64的引數,class main指的是Zygote的classname為main。在解析Service型別語句時會將Service物件加入Service連結串列中。

再回過頭看init.rc配置檔案:

...import /init.${ro.zygote}.rc...on nonencrypted
    class_start main
    class_start late_start...    
複製程式碼

class_start是一個command,對應的函式是do_class_start,用於啟動classname為main的Service,也就是前面的Zygote,因此class_start main是用來啟動Zygote的,do_class_start函式在builtins.cpp中定義,程式碼如下所示:

//路徑:/system/core/init/builtins.cpp
static int do_class_start(const std::vector<std::string>& args) {
        /* Starting a class does not start services
         * which are explicitly disabled.  They must
         * be started individually.
         */
    ServiceManager::GetInstance().
        ForEachServiceInClass(args[1], [] (Service* s) { s->StartIfNotDisabled(); });
    return 0;
}
複製程式碼

ForEachServiceInClass函式會遍歷Service連結串列,找到classname為main的Zygote,並執行StartIfNotDisabled函式,程式碼如下所示:

//路徑:/system/core/init/service.cpp
bool Service::StartIfNotDisabled() {
    if (!(flags_ & SVC_DISABLED)) {
        return Start();
    } else {
        flags_ |= SVC_DISABLED_START;
    }
    return true;
}
複製程式碼

如果Service沒有在其對應的rc檔案中設定disabled選項,就會呼叫Start函式,Start函式如下所示:

//路徑:/system/core/init/service.cpp
bool Service::Start() {
    ...
    pid_t pid = -1;
    if (namespace_flags_) {
        pid = clone(nullptr, nullptr, namespace_flags_ | SIGCHLD, nullptr);
    } else {
        //註釋1
        pid = fork();
    }

    if (pid == 0) {
        ...
        //註釋2
        if (execve(strs[0], (char**) &strs[0], (char**) ENV) < 0) {
            PLOG(ERROR) << "cannot execve('" << strs[0] << "')";
        }
        ...
    }
    ...
}
複製程式碼

在註釋1處通過fork函式建立子程式,並返回pid,如果pid為0說明當前程式碼邏輯在子執行緒中執行,接著執行註釋2處的execve函式,來啟動Service子程式,進入Service的main函式中,如果Service是Zygote,執行程式的路徑是/system/bin/app_process64,對應的檔案是app_main.cpp,也就是會進入app_main.cpp的main函式中。程式碼如下所示:

int main(int argc, char* const argv[])
{
    ...
    while (i < argc) {
        const char* arg = argv[i++];
        if (strcmp(arg, "--zygote") == 0) {
            //註釋1
            zygote = true;
            niceName = ZYGOTE_NICE_NAME;
        } else if (strcmp(arg, "--start-system-server") == 0) {
            startSystemServer = true;
        } else if (strcmp(arg, "--application") == 0) {
            application = true;
        } else if (strncmp(arg, "--nice-name=", 12) == 0) {
            niceName.setTo(arg + 12);
        } else if (strncmp(arg, "--", 2) != 0) {
            className.setTo(arg);
            break;
        } else {
            --i;
            break;
        }
    }
    ...
    if (zygote) {
        //註釋2
        runtime.start("com.android.internal.os.ZygoteInit", args, zygote);
    } else if (className) {
        runtime.start("com.android.internal.os.RuntimeInit", args, zygote);
    } else {
        fprintf(stderr, "Error: no class name or --zygote supplied.\n");
        app_usage();
        LOG_ALWAYS_FATAL("app_process: no class name or --zygote supplied.");
    }
}
複製程式碼

在註釋1處判斷執行命令時是否帶了--zygote,如果攜帶了,zygote賦值為true,接在註釋2處判斷如果zygote為true,就會通過runtime.start啟動com.android.internal.os.ZygoteInit。


838794-506ddad529df4cd4.webp.jpg

相關文章