2018年5月10日 星期四

Android啟動篇— init原理(一)

原文 http://www.cnblogs.com/pepsimaxin/p/6702945.html

================================================== ====== ============================================ ============
= 【原創文章】:參考部分博客內容,學習之餘進行了大量的篩減細化分析= = 【特殊申明】:避諱抄襲侵權之嫌疑,特此說明,歡迎轉載!=    
================================================= ======= =========================================== =============
************************************************** ************************* ************************* **************************************************
* Android源碼下載:https://mirrors.tuna.tsinghua.edu.cn/help/AOSP/                    * *源碼編譯可參考【牛肉麵大神之作】:http://blog.csdn.net/cjpx00008/article /details/60474883 *
************************************************** ************************* ************************* **************************************************
【開篇說明】
  在【Android啟示錄】中,提到了主要的分析對象和分享內容,拋開Android內核級的知識點,學習Android第一步便是“init”,作為天字第一號進程,代碼羞澀難懂,但是也極其重要,熟悉init的原理對後面Zygote -- SystemServer --核心服務等一些列源碼的研究是有很大作用的,所以既然說研究Android源碼,就先拿init “庖丁解牛”!
【正文開始】
  Init進程,它是一個由內核啟動的用戶級進程,當Linux內核啟動之後,運行的第一個進程是init,這個進程是一個守護進程,確切的說,它是Linux系統中用戶控件的第一個進程,所以它的進程號是1。它的生命週期貫穿整個linux內核運行的始終, linux中所有其它的進程的共同始祖均為init進程,可以通過“adb shell ps | grep init”查看進程號。
  Android init進程的入口文件在system/core/init/init.cpp中,由於init是命令行程序,所以分析init.cpp首先應從main函數開始:
複製代碼
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才為第二階段
    // 將我們需要的基本文件系統設置放在initramdisk     // 上,然後我們讓rc文件找出其餘的。 
    如果(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);                                 (x)__STRING(x)        mount(proc / proc proc 0hidepid = 2,gid =  MAKE_STR(AID_READPROC));      // 掛載devpts文件系統 
        #define MAKE_STR // 掛載proc文件系統         mount(sysfs  
 
" /sys " , " sysfs " , 0 , NULL);                                        // 掛載sysfs文件系統 
     }
複製代碼
  以上代碼主要做的工作就是:【創建文件系統目錄並掛載相關的文件系統】
複製代碼
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_set_level(KLOG_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

        // 如果在命令行和DT中都傳遞參數,
         // 在DT中設置的屬性總是優先於命令行的。
        process_kernel_dt();
        process_kernel_cmdline();     // 處理內核命令行
         // 傳播內核變量到內部變量
         // 由init以及當前必需屬性使用。
        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 " );
        退出(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 ) {
            錯誤(加載策略失敗:%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有兩種工作模式:
             // 1、”permissive”,所有的操作都被允許(即沒有MAC),但是如果違反權限的話,會記錄日誌
             // 2、”enforcing”,所有操作都會進行權限檢查。在一般的終端中,應該工作於enforing模式
            if (security_setenforce(is_enforcing)) {         // 設置selinux的模式,是開還是關 
                ERROR( " security_setenforce(%s) failed: %s\n " ,
                      is_enforcingtrue false ,strerror(errno));
                security_failure();     // 將重啟進入recovery mode 
            }
        }

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

        NOTICE((初始化SELinux%s佔用%.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 文件屬性
            錯誤(restorecon失敗:%s \ n ,strerror(errno));
            security_failure();
        }
        char * path = argv [ 0 ];
        char * args [] = {path,const_cast < char *>(“-- second-stage ),nullptr};     // 設置參數 --second級

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

    // 這些目錄必須在初始策略加載之前創建
     // 因此需要將其安全上下文恢復為適當的值。
    // 這個必須在ueventd填充/ dev之前發生。
    NOTICE(“正在運行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 ) {
        錯誤(epoll_create1失敗:%s \ n ,strerror(errno));
        退出(1 );
    }
複製代碼
  接著往下分析:
複製代碼
int main( int argc, char ** argv) {
     /* 01.創建文件系統目錄並掛載相關的文件系統*/
    /* 02. 屏蔽標準的輸入輸出/初始化內核log系統*/
    /* 03. 初始化屬性域*/
    /* 04. 完成SELinux相關工作*/·
    /* 05. 重新設置屬性*/
    /* 06.創建epoll句柄*/ 
    /* 07.裝載子進程信號處理器*/ 
    signal_handler_init();        //裝載子進程信號處理器
複製代碼
    Note  init是一個守護進程,為了防止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 ) {
        錯誤(socketpair失敗:%s \ n ,strerror(errno));
        退出(1 );
    }

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

    // 如果我們捕獲SIGCHLD,則寫入signal_write_fd。
    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_flags SA_NOCLDSTOP 由於系統默認在子進程暫停時也會發送信號SIGCHLD init 需要忽略子進程在暫停時發出的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 ) {
        錯誤(write(signal_write_fd)失敗:%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 ) {
        錯誤(epoll_ctl失敗:%s \ n ,strerror(errno));
    }
}
複製代碼
【小結】
  當 init 進程調用signal_handler_init 後,一旦收到子進程終止帶來的SIGCHLD 消息後,將利用信號處理者SIGCHLD_handler signal_write_fd 寫入信息;epoll 句柄監聽到signal_read_fd 收消息後,將調用handle_signal 進行處理。
  
  查看handle_signal函數:
複製代碼
static  void handle_signal(){       // - >位於system / core / init / signal_handler.cpp中
     // 清除未完成的請求。
    char buf [ 32 ];
    讀取(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屬性
}

ServiceManagerServiceManager :: GetInstance(){
     靜態ServiceManager實例;
    返回實例;
}
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 ){
        錯誤(waitpid失敗:%s \ n ,strerror(errno));
        返回 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);
    }

    如果(WIFEXITED(狀態)){
        NOTICE(%s退出狀態%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由信號%d \ n停止,name.c_str(),WSTOPSIG(status));
    } else {
        NOTICE(%s state changed ,name.c_str());
    }

    如果(!svc){
         return  true ;
    }

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

    返回 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的解析過程

沒有留言: