Android 7.0 啓動篇 — init原理(一)(轉 Android 9.0 分析)

========================================================          ========================================================html

=              【原創文章】:參考部分博客內容,學習之餘進行了大量的篩減細化分析                        =          =                          【特殊申明】:避諱抄襲侵權之嫌疑,特此說明,歡迎轉載!                           =   
========================================================          ========================================================node

***************************************************************************         ***************************************************************************linux

*                  Android源碼下載:https://mirrors.tuna.tsinghua.edu.cn/help/AOSP/                   *         *  源碼編譯可參考【牛肉麪大神之做】:http://blog.csdn.net/cjpx00008/article/details/60474883 *android

***************************************************************************         ***************************************************************************shell

【開篇說明】安全

  在【Android啓示錄】中,提到了主要的分析對象和分享內容,拋開Android內核級的知識點,學習Android第一步即是「init」,做爲天字第一號進程,代碼羞澀難懂,可是也極其重要,熟悉init的原理對後面Zygote -- SystemServer -- 核心服務等一些列源碼的研究是有很大做用的,因此既然說研究Android源碼,就先拿init 「庖丁解牛」!app

【正文開始】dom

  Init進程,它是一個由內核啓動的用戶級進程,當Linux內核啓動以後,運行的第一個進程是init,這個進程是一個守護進程,確切的說,它是Linux系統中用戶控件的第一個進程,因此它的進程號是1。它的生命週期貫穿整個linux 內核運行的始終, linux中全部其它的進程的共同始祖均爲init進程,能夠經過「adb shell ps | grep init」查看進程號。
  Android init進程的入口文件在system/core/init/init.cpp中,因爲init是命令行程序,因此分析init.cpp首先應從main函數開始:socket

int main(int argc, char** argv) {    // 入口函數main
    if (!strcmp(basename(argv[0]), "ueventd")) {
        return ueventd_main(argc, argv);
    }

    if (!strcmp(basename(argv[0]), "watchdogd")) {
        return watchdogd_main(argc, argv);
    }

    // Clear the umask.
    umask(0);    // 清除屏蔽字(file mode creation mask),保證新建的目錄的訪問權限不受屏蔽字影響。
    add_environment("PATH", _PATH_DEFPATH);
    
    bool is_first_stage = (argc == 1) || (strcmp(argv[1], "--second-stage") != 0);    // 判斷是不是系統啓動的第一階段,只有啓動參數中有--second-stage才爲第二階段
    // Get the basic filesystem setup we need put together in the initramdisk 
// on / and then we'll let the rc file figure out the rest.
if (is_first_stage) {
mount(
"tmpfs", "/dev", "tmpfs", MS_NOSUID, "mode=0755"); // 掛載tmpfs文件系統
mkdir("/dev/pts", 0755);
mkdir(
"/dev/socket", 0755);
mount(
"devpts", "/dev/pts", "devpts", 0, NULL); // 掛載devpts文件系統
#define MAKE_STR(x) __STRING(x)
mount(
"proc", "/proc", "proc", 0, "hidepid=2,gid=" MAKE_STR(AID_READPROC)); // 掛載proc文件系統
mount("sysfs", "/sys", "sysfs", 0, NULL); // 掛載sysfs文件系統
}

  以上代碼主要作的工做就是:【建立文件系統目錄並掛載相關的文件系統】ide

int main(int argc, char** argv) {
    /* 01. 建立文件系統目錄並掛載相關的文件系統 */
    /* 02. 屏蔽標準的輸入輸出/初始化內核log系統 */ // We must have some place other than / to create the device nodes for
    // kmsg and null, otherwise we won't be able to remount / read-only
    // later on. Now that tmpfs is mounted on /dev, we can actually talk
    // to the outside world.
    open_devnull_stdio();    // 重定向標準輸入輸出到/dev/_null_  -->  定義在system/core/init/Util.cpp中
    // init進程經過klog_init函數,提供輸出log信息的設備  -->  定義在system/core/libcutils/Klog.c中
    klog_init();      // 對klog進行初始化         
    klog_set_level(KLOG_NOTICE_LEVEL);  // NOTICE level

  繼續分析源碼,接下來要作的就是初始化屬性域:

int main(int argc, char** argv) {
    /* 01. 建立文件系統目錄並掛載相關的文件系統 */
    /* 02. 屏蔽標準的輸入輸出/初始化內核log系統 */
/* 03. 初始化屬性域 */
NOTICE("init %s started!\n", is_first_stage ? "first stage" : "second stage"); if (!is_first_stage) { // 引入SELinux機制後,經過is_first_stage區分init運行狀態 // Indicate that booting is in progress to background fw loaders, etc. close(open("/dev/.booting", O_WRONLY | O_CREAT | O_CLOEXEC, 0000)); /* 檢測/dev/.booting文件是否可讀寫建立*/
        property_init();        // 初始化屬性域 --> 定義於system/core/init/Property_service.cpp

        // If arguments are passed both on the command line and in DT,
        // properties set in DT always have priority over the command-line ones.
        process_kernel_dt();
        process_kernel_cmdline();     // 處理內核命令行
        // Propagate the kernel variables to internal variables
        // used by init as well as the current required properties.
        export_kernel_boot_props();
    }

  看一下property_init方法:位於system/core/init/Property_service.cpp中

void property_init() {
    if (__system_property_area_init()) {         // 調用此函數初始化屬性域
        ERROR("Failed to initialize property area\n");
        exit(1);
    }
}

  繼續分析main函數:

int main(int argc, char** argv) {
    /* 01. 建立文件系統目錄並掛載相關的文件系統 */
    /* 02. 屏蔽標準的輸入輸出/初始化內核log系統 */
    /* 03. 初始化屬性域 */
/* 04. 完成SELinux相關工做 */
// Set up SELinux, including loading the SELinux policy if we're in the kernel domain. selinux_initialize(is_first_stage); // 調用selinux_initialize啓動SELinux

  詳細看一下selinux_initialize()函數:

static void selinux_initialize(bool in_kernel_domain) {     // 區份內核態和用戶態
    Timer t;      //使用Timer計時,計算selinux初始化耗時

    selinux_callback cb;
    cb.func_log = selinux_klog_callback;              // 用於打印Log的回調函數
    selinux_set_callback(SELINUX_CB_LOG, cb);
    cb.func_audit = audit_callback;                    // 用於檢查權限的回調函數
    selinux_set_callback(SELINUX_CB_AUDIT, cb);

    if (in_kernel_domain) {        // 內核態處理流程,第一階段in_kernel_domain爲true  
        INFO("Loading SELinux policy...\n");        // 該行log打印不出,INFO級別 
        // 用於加載sepolicy文件。該函數最終將sepolicy文件傳遞給kernel,這樣kernel就有了安全策略配置文件
        if (selinux_android_load_policy() < 0) {
            ERROR("failed to load policy: %s\n", strerror(errno));
            security_failure();
        }

        bool kernel_enforcing = (security_getenforce() == 1);      // 內核中讀取的信息
        bool is_enforcing = selinux_is_enforcing();                // 命令行中獲得的信息
        if (kernel_enforcing != is_enforcing) {
        // 用於設置selinux的工做模式。selinux有兩種工做模式:
            // 一、」permissive」,全部的操做都被容許(即沒有MAC),可是若是違反權限的話,會記錄日誌
            // 二、」enforcing」,全部操做都會進行權限檢查。在通常的終端中,應該工做於enforing模式
            if (security_setenforce(is_enforcing)) {        //設置selinux的模式,是開仍是關
                ERROR("security_setenforce(%s) failed: %s\n",
                      is_enforcing ? "true" : "false", strerror(errno));
                security_failure();    // 將重啓進入recovery mode
            }
        }

        if (write_file("/sys/fs/selinux/checkreqprot", "0") == -1) {
            security_failure();
        }

        NOTICE("(Initializing SELinux %s took %.2fs.)\n",
               is_enforcing ? "enforcing" : "non-enforcing", t.duration());   //輸出selinux的模式,與初始化耗時
    } else { 
selinux_init_all_handles(); //若是啓動第二階段,調用該函數
}
}

   回到main函數中繼續分析:

int main(int argc, char** argv) {
    /* 01. 建立文件系統目錄並掛載相關的文件系統 */
    /* 02. 屏蔽標準的輸入輸出/初始化內核log系統 */
    /* 03. 初始化屬性域 */
    /* 04. 完成SELinux相關工做 */
/* 05. 從新設置屬性 */
// If we're in the kernel domain, re-exec init to transition to the init domain now // that the SELinux policy has been loaded. if (is_first_stage) { if (restorecon("/init") == -1) { // selinux policy要求,從新設置init文件屬性
            ERROR("restorecon failed: %s\n", strerror(errno));
            security_failure();
        }
        char* path = argv[0];
        char* args[] = { path, const_cast<char*>("--second-stage"), nullptr };     //設置參數--second-stage

 

    if (execv(path, args) == -1) {        // 執行init進程,從新進入main函數
            ERROR("execv(\"%s\") failed: %s\n", path, strerror(errno));
            security_failure();
        }
    }

    // These directories were necessarily created before initial policy load
    // and therefore need their security context restored to the proper value.
    // This must happen before /dev is populated by ueventd.
    NOTICE("Running restorecon...\n");
    restorecon("/dev");
    restorecon("/dev/socket");
    restorecon("/dev/__properties__");
    restorecon("/property_contexts");

    restorecon_recursive("/sys");

    epoll_fd = epoll_create1(EPOLL_CLOEXEC);         // 調用epoll_create1建立epoll句柄 if (epoll_fd == -1) {
        ERROR("epoll_create1 failed: %s\n", strerror(errno));
        exit(1);
    }

  接着往下分析:

int main(int argc, char** argv) {
    /* 01. 建立文件系統目錄並掛載相關的文件系統 */
    /* 02. 屏蔽標準的輸入輸出/初始化內核log系統 */
    /* 03. 初始化屬性域 */
    /* 04. 完成SELinux相關工做 */·
    /* 05. 從新設置屬性 */
    /* 06. 建立epoll句柄 */
/* 07. 裝載子進程信號處理器 */
signal_handler_init(); // 裝載子進程信號處理器

    Noteinit是一個守護進程,爲了防止init的子進程成爲殭屍進程(zombie process),須要init在子進程結束時獲取子進程的結束碼,經過結束碼將程序表中的子進程移除,防止成爲殭屍進程的子進程佔用程序表的空間(程序表的空間達到上限時,系統就不能再啓動新的進程了,會引發嚴重的系統問題)。

  細化signal_handler_init()函數:

void signal_handler_init() {        // 函數定位於:system/core/init/Singal_handler.cpp // 在linux當中,父進程是經過捕捉SIGCHLD信號來得知子進程運行結束的狀況
    // Create a signalling mechanism for SIGCHLD.
    int s[2];
    // 利用socketpair建立出已經鏈接的兩個socket,分別做爲信號的讀、寫端
    if (socketpair(AF_UNIX, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0, s) == -1) {
        ERROR("socketpair failed: %s\n", strerror(errno));
        exit(1);
    }

    signal_write_fd = s[0];
    signal_read_fd = s[1];

    // Write to signal_write_fd if we catch SIGCHLD.
    struct sigaction act;
    memset(&act, 0, sizeof(act));
    // 信號處理器爲SIGCHLD_handler,其被存在sigaction結構體中,負責處理SIGCHLD消息
    act.sa_handler = SIGCHLD_handler;       // 信號處理器:SIGCHLD_handler
    act.sa_flags = SA_NOCLDSTOP;            // 僅當進程終止時才接受SIGCHLD信號
    // 調用信號安裝函數sigaction,將監聽的信號及對應的信號處理器註冊到內核中
    sigaction(SIGCHLD, &act, 0);
    // 相對於6.0的代碼,進一步做了封裝,用於終止出現問題的子進程
    ServiceManager::GetInstance().ReapAnyOutstandingChildren();

    register_epoll_handler(signal_read_fd, handle_signal);        // 定義在system/core/init/Init.cpp
}

  Linux進程經過互相發送接收消息來實現進程間的通訊,這些消息被稱爲信號。每一個進程在處理其它進程發送的信號時都要註冊處理者,處理者被稱爲信號處理器。

  注意到sigaction結構體的sa_flagsSA_NOCLDSTOP。因爲系統默認在子進程暫停時也會發送信號SIGCHLDinit須要忽略子進程在暫停時發出的SIGCHLD信號,所以將act.sa_flags 置爲SA_NOCLDSTOP,該標誌位表示僅當進程終止時才接受SIGCHLD信號。

  觀察SIGCHLD_handler具體工做:

static void SIGCHLD_handler(int) {
    /* init進程是全部進程的父進程,當其子進程終止產生SIGCHLD信號時,SIGCHLD_handler對signal_write_fd執行寫操做,因爲socketpair的綁定關係,這將觸發信號對應的signal_read_fd收到數據。*/
    if (TEMP_FAILURE_RETRY(write(signal_write_fd, "1", 1)) == -1) {
        ERROR("write(signal_write_fd) failed: %s\n", strerror(errno));
    }
}

  在裝在信號監聽器的最後,有以下函數:register_epoll_handler(signal_read_fd, handle_signal);

void register_epoll_handler(int fd, void (*fn)()) {        // 回到init.cpp中
    epoll_event ev;
    ev.events = EPOLLIN;
    ev.data.ptr = reinterpret_cast<void*>(fn);
    // epoll_fd增長一個監聽對象fd,fd上有數據到來時,調用fn處理
    // 當epoll句柄監聽到signal_read_fd中有數據可讀時,將調用handle_signal進行處理。
    if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fd, &ev) == -1) {
        ERROR("epoll_ctl failed: %s\n", strerror(errno));
    }
}

【小結】

  當init進程調用signal_handler_init後,一旦收到子進程終止帶來的SIGCHLD消息後,將利用信號處理者SIGCHLD_handlersignal_write_fd寫入信息; epoll句柄監聽到signal_read_fd收消息後,將調用handle_signal進行處理。

  

  查看handle_signal函數:

static void handle_signal() {      // --> 位於system/core/init/signal_handler.cpp中
    // Clear outstanding requests.
    char buf[32];
    read(signal_read_fd, buf, sizeof(buf));

    ServiceManager::GetInstance().ReapAnyOutstandingChildren();
}

  從代碼中能夠看出,handle_signal只是清空signal_read_fd中的數據,而後調用ServiceManager::GetInstance().ReapAnyOutstandingChildren()

  繼續分析:

// 定義於system/core/init/service.cpp中,是一個單例對象。
ServiceManager::ServiceManager() {     // 默認private屬性
}

ServiceManager& ServiceManager::GetInstance() {
    static ServiceManager instance;
    return instance;
}
void ServiceManager::ReapAnyOutstandingChildren() {
    while (ReapOneProcess()) {    // 實際調用了ReapOneProcess函數
    }
}

  接下來看下ReapOneProcess這個函數:

bool ServiceManager::ReapOneProcess() {
    int status;
    //用waitpid函數獲取狀態發生變化的子進程pid
    //waitpid的標記爲WNOHANG,即非阻塞,返回爲正值就說明有進程掛掉了
    pid_t pid = TEMP_FAILURE_RETRY(waitpid(-1, &status, WNOHANG));
    if (pid == 0) {
        return false;
    } else if (pid == -1) {
        ERROR("waitpid failed: %s\n", strerror(errno));
        return false;
    }
    // 利用FindServiceByPid函數,找到pid對應的服務。
    // FindServiceByPid主要經過輪詢解析init.rc生成的service_list,找到pid與參數一直的svc
    Service* svc = FindServiceByPid(pid);
    
    std::string name;
    if (svc) {
        name = android::base::StringPrintf("Service '%s' (pid %d)",
                                           svc->name().c_str(), pid);
    } else {
        name = android::base::StringPrintf("Untracked pid %d", pid);
    }

    if (WIFEXITED(status)) {
        NOTICE("%s exited with status %d\n", name.c_str(), WEXITSTATUS(status));
    } else if (WIFSIGNALED(status)) {
        NOTICE("%s killed by signal %d\n", name.c_str(), WTERMSIG(status));         // 輸出服務結束緣由
    } else if (WIFSTOPPED(status)) {
        NOTICE("%s stopped by signal %d\n", name.c_str(), WSTOPSIG(status));
    } else {
        NOTICE("%s state changed", name.c_str());
    }

    if (!svc) {
        return true;
    }

    if (svc->Reap()) {                 // 結束服務,相對於6.0做了進一步的封裝,重啓一些子進程,不作具體分析
        waiting_for_exec = false;
        RemoveService(*svc);           // 移除服務對應的信息
    }

    return true;
}

  繼續分析main()函數:

int main(int argc, char** argv) {
    /* 01. 建立文件系統目錄並掛載相關的文件系統 */
    /* 02. 屏蔽標準的輸入輸出/初始化內核log系統 */
    /* 03. 初始化屬性域 */
    /* 04. 完成SELinux相關工做 */·
    /* 05. 從新設置屬性 */
    /* 06. 建立epoll句柄 */
    /* 07. 裝載子進程信號處理器 */
/* 08. 啓動匹配屬性的服務端*/
property_load_boot_defaults(); // 進程調用property_load_boot_defaults進行默認屬性配置相關的工做 export_oem_lock_status(); std::string bootmode = property_get("ro.bootmode"); // 獲取啓動模式 if (strncmp(bootmode.c_str(), "ffbm", 4) == 0){ property_set("ro.logdumpd","0"); }else{ property_set("ro.logdumpd","1"); } start_property_service(); // 啓動屬性服務

  看下property_load_boot_defaults()函數:位於system/core/init/Property_service.cpp中

// property_load_boot_defaults實際上就是調用load_properties_from_file解析配置文件       /* 09. 設置默認系統屬性 */ // 而後根據解析的結果,設置系統屬性
void property_load_boot_defaults() {
    load_properties_from_file(PROP_PATH_RAMDISK_DEFAULT, NULL);
}

  接着繼續分析main:

int main(int argc, char** argv) {
    /* 01. 建立文件系統目錄並掛載相關的文件系統 */
    /* 02. 屏蔽標準的輸入輸出/初始化內核log系統 */
    /* 03. 初始化屬性域 */
    /* 04. 完成SELinux相關工做 */·
    /* 05. 從新設置屬性 */
    /* 06. 建立epoll句柄 */
    /* 07. 裝載子進程信號處理器 */
    /* 08. 設置默認系統屬性 */
    /* 09. 啓動配置屬性的服務端 */
/* 10. 匹配命令和函數之間的對應關係 */
    const BuiltinFunctionMap function_map;          // system/core/init/builtins.cpp
    Action::set_function_map(&function_map);        // 在Action中保存function_map對象,記錄了命令與函數之間的對應關係

【結尾】

  因爲init涉及的知識點是至關多,代碼之間的邏輯也是極其複雜,我在看別人的博客過程當中,最反感一篇博客要看好久,每每由於雜事而放棄堅持(確切的說,隨手把網頁關掉了),因此我就分章節分析,儘可能少源碼多講解。

  接下來,在Android啓動篇 — init原理(二)中將詳細分析init.rc的解析過程

相關文章
相關標籤/搜索