================================================== ====== ============================================ ============
= 【原創文章】:參考部分博客內容,學習之餘進行了大量的篩減細化分析= = 【特殊申明】:避諱抄襲侵權之嫌疑,特此說明,歡迎轉載!=
================================================= ======= =========================================== =============
================================================= ======= =========================================== =============
************************************************** ************************* ************************* **************************************************
* 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函數開始:
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 ”,0,“ hidepid = 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_enforcing?“ true ”:“ 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屬性 } ServiceManager&ServiceManager :: 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的解析過程。
沒有留言:
張貼留言