Android init原始碼分析(1)概要分析

來源:互聯網
上載者:User

標籤:android   init   

功能概述
init進程是Android核心啟動的第一個進程,其進程號(pid)為1,是Android系統所有進程的祖先,因此它肩負著系統啟動的重要責任。Android的init原始碼位於system/core/init/目錄下,伴隨Android系統多個版本的迭代,init原始碼也幾經重構。
目前Android4.4原始碼中,init目錄編譯後產生如下Android系統的三個檔案,分別是
  • /init
  • /sbin/ueventd-->/init
  • /sbin/watchdogd-->/init
其中ueventd與wathdogd均是指向/init的軟連結。(具體實現請閱讀init/Android.mk)。
在Android系統早期版本(2.2之前)只有init進程,Android2.2中將建立裝置驅動節點檔案功能獨立到ueventd進程完成,在Android4.1中則添加了watchdogd。

/init主要完成三大功能:
  1. 解析init.rc初始化Android屬性系統,並維護屬性服務
  2. 初始化Android屬性系統,並維護屬性服務
  3. 處理子進程啟動、停止、重啟動
/ueventd用於建立裝置驅動節點。/watchdogd 是看門狗服務進程。
程式碼分析分析代碼當先抓住主幹,瞭解其大致結構與流程,再逐塊深入,分析其實現細節。這樣先大局再細節的方法可以讓我們在閱讀代碼時保持頭腦的清醒,切忌不可在沒有對整體流程瞭解的情況下深入細節,那很容易導致我們迷失在代碼森林中。
接下來分析init.c的main函數。為了方便分析,將main函數代碼做了精簡,代碼如下。
int main(int argc, char **argv){    //<part 1>    if (!strcmp(basename(argv[0]), "ueventd"))        return ueventd_main(argc, argv);    if (!strcmp(basename(argv[0]), "watchdogd"))        return watchdogd_main(argc, argv);       //<part2>    umask(0);    mkdir("/dev", 0755);    mkdir("/proc", 0755);    mkdir("/sys", 0755);    mount("tmpfs", "/dev", "tmpfs", MS_NOSUID, "mode=0755");    mkdir("/dev/pts", 0755);    mkdir("/dev/socket", 0755);    mount("devpts", "/dev/pts", "devpts", 0, NULL);    mount("proc", "/proc", "proc", 0, NULL);    mount("sysfs", "/sys", "sysfs", 0, NULL);    ....    open_devnull_stdio();    klog_init();    property_init();    ....    //<part3>    INFO("reading config file\n");    init_parse_config_file("/init.rc");    ...    action_for_each_trigger("early-init", action_add_queue_tail);    ....    queue_builtin_action(queue_property_triggers_action, "queue_property_triggers");       //<part4>    for(;;) {        ...        execute_one_command();        restart_processes();        ....        nr = poll(ufds, fd_count, timeout);        if (nr <= 0)            continue;        for (i = 0; i < fd_count; i++) {            if (ufds[i].revents & POLLIN) {                if (ufds[i].fd == get_property_set_fd())                    handle_property_set_fd();                else if (ufds[i].fd == get_keychord_fd())                    handle_keychord();                else if (ufds[i].fd == get_signal_fd())                    handle_signal();            }        }    }    return 0;}
將main函數分為上述4個部分,對應part1到part4,下面分別做具體說明。
代碼<part1>通過命令列判斷argv[0]的字串內容,來區分當前程式是init,ueventd或是watchdogd。
C程式的main函數原型為 main(int argc, char* argv[]), ueventd以及watchdogd的啟動都在init.rc中描述,由init進程解析後執行fork、exec啟動,因此其入口參數的構造在init代碼中,將在init.rc解析時分析。此時我們只需要直到argv[0]中將儲存可執行檔的名字。
代碼<part2>

umaks(0)用於設定當前進程(即/init)的檔案模型建立掩碼(file mode creation mask),注意這裡的檔案是廣泛意義上的檔案,包括普通檔案、目錄、連結檔案、裝置節點等。
PS. 以上解釋摘自umask的mannual,可在linux系統中執行man 3 umask查看。
Linux C庫中mkdir與open的函數運行如下。
int mkdir(const char *pathname, mode_t mode);
int open(const char *pathname, int flags, mode_t mode);
Linux核心給每一個進程都設定了一個掩碼,當程式調用open、mkdir等函數建立檔案或目錄時,傳入open的mode會現在掩碼做運算,得到的檔案mode,才是檔案真正的mode。
譬如要建立一個目錄,並設定它的檔案許可權為0777,
mkdir("testdir", 0777)
但實際上寫入的檔案許可權卻未必是777,因為mkdir系統調用在建立testdir時,會將0777與當前進程的掩碼(稱為umask)運算,具體運算方法為 0777&~umask作為testdir的真正許可權。因此上述init中首先調用umask(0)將進程掩碼清0,這樣調用open/mkdir等函數建立檔案或目錄時,傳入的mode就會作為實際的值寫入檔案系統。

接下來建立目錄,並掛載核心檔案系統,它們是
  • tmpfs,虛擬記憶體檔案系統,該檔案系統被掛載到/dev目錄下,主要存放裝置節點檔案,使用者進程通過訪問/dev目錄下的裝置節點檔案可以與硬體驅動程式互動。
  • devpts,一種虛擬終端檔案系統
  • proc,虛擬檔案系統,被掛載到/proc目錄下,通過該檔案系統可與核心資料結構互動,查看以及設定核心參數。
  • sysfs,虛擬檔案系統,被掛載到/sys目錄下,它與proc類似,是2.6核心在吸收了proc檔案系統的設計經驗和教訓的基礎上所實現的一種較新的檔案系統,為核心提供了統一的裝置驅動模型。(引用:http://www.ibm.com/developerworks/cn/linux/l-cn-sysfs/index.html)
代碼<part2>隨後的代碼如下。
    open_devnull_stdio();    klog_init();    property_init();    get_hardware_name(hardware, &revision);    process_kernel_cmdline();    union selinux_callback cb;    cb.func_log = log_callback;    selinux_set_callback(SELINUX_CB_LOG, cb);    cb.func_audit = audit_callback;    selinux_set_callback(SELINUX_CB_AUDIT, cb);    selinux_initialize();    /* 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 populatedproperty_init(); by ueventd.     */    restorecon("/dev");    restorecon("/dev/socket");    restorecon("/dev/__properties__");    restorecon_recursive("/sys");    is_charger = !strcmp(bootmode, "charger");    INFO("property init\n");    property_load_boot_defaults();
open_devnull_stdio()該函數名字暗示將init進程的stido,包括stdin(標準輸入,檔案描述符為0)、stdout(標準輸出,檔案描述符為1)以及stderr(標準錯誤,檔案描述符號為2),全部重新導向/dev/null裝置,但是細心的讀者可能會有疑問,在代碼<part2>中雖然掛載了tmpfs檔案系統到/dev目錄下,但是並未建立任何裝置節點檔案,/dev/null此時並不存在啊,如何才能將stdio重新導向到null裝置中呢?帶著疑問我們來分析該函數實現。
void open_devnull_stdio(void){    int fd;    static const char *name = "/dev/__null__";    if (mknod(name, S_IFCHR | 0600, (1 << 8) | 3) == 0) {        fd = open(name, O_RDWR);        unlink(name);        if (fd >= 0) {            dup2(fd, 0);            dup2(fd, 1);            dup2(fd, 2);            if (fd > 2) {                close(fd);            }            return;        }    }    exit(1);}
該函數中通過mknode函數建立/dev/__null__裝置節點檔案,隨後開啟該檔案得到檔案描述符fd,然後利用dup2系統調用將檔案描述符0、1、2綁定到fd上。這個/dev/__null__看起來很奇怪,Linux系統中的null不是/dev/null麼,這兩者有什麼關係嗎?
在Linux核心為裝置節點檔案分別了一個主、次裝置號,核心實際以這兩個裝置號來標識某個裝置驅動,而並不以檔案名稱作為標識。mknod系統調用建立裝置節點檔案,其第三個參數的高8位為主裝置號,低8位次裝置號。可見/dev/__null__的主次裝置號分別是1、3。它是否就是/dev/null呢?我們需要深入核心去確認這一點。
kernel/Documentation/devices.txt 中存在如下片段
    1 char    Memory devices            1 = /dev/mem      Physical memory access            2 = /dev/kmem     Kernel virtual memory access            3 = /dev/null     Null device            4 = /dev/port     I/O port access            5 = /dev/zero     Null byte source            6 = /dev/core     OBSOLETE - replaced by /proc/kcore            7 = /dev/full     Returns ENOSPC on write            8 = /dev/random   Nondeterministic random number gen.            9 = /dev/urandom  Faster, less secure random number gen.           10 = /dev/aio      Asynchronous I/O notification interface           11 = /dev/kmsg     Writes to this come out as printk's           12 = /dev/oldmem   Used by crashdump kernels to access                      the memory of the kernel that crashed.
可見/dev/__null__與/dev/null的裝置號完全相同,它就是/dev/null的馬甲。那麼為什麼init進程不直接建立/dev/null呢? 當前我們還無法回答這個問題,要等到分析/sbin/uevnted的原理時才能明白。
還有一個疑問,為什麼要將stdio重新導向/dev/__null__裝置呢?這是因為此時Anrdoid系統上處於啟動的早期階段,可用於接收init進程標準輸出、標準錯誤的裝置節點還不存在。因此init進程一不做二不休,直接把它們重新導向到/dev/__nulll__了。
當我們學習C語言時,第一個helloworld程式是通過printf列印的,我們知道它通過標準輸出列印到終端上。printf也是我們廣大程式員最喜愛的調試方法之一。現在標準輸出被重新導向到null裝置了,如果我們想在init中添加列印語句,怎麼辦呢?帶著這樣的擔憂,我們繼續分析代碼。
klog_init()隨後klog_init()顯然是在暗示我們,雖然標準輸出沒了,但是還有方法列印log的。帶著欣喜又好奇的心情,讓我們看看klog_init是如何?的。
void klog_init(void){    static const char *name = "/dev/__kmsg__";    if (klog_fd >= 0) return; /* Already initialized */    if (mknod(name, S_IFCHR | 0600, (1 << 8) | 11) == 0) {        klog_fd = open(name, O_WRONLY);        if (klog_fd < 0)                return;        fcntl(klog_fd, F_SETFD, FD_CLOEXEC);        unlink(name);    }}
klog_init函數首先檢查klog_fd是否已經初始化。首次執行時,調用mknod建立主裝置號為1,從裝置號為11的裝置節點檔案/dev/__kmsg__,然後開啟該檔案將檔案描述符儲存到變數klog_fd中,接著調用fcntl(klog_fd, F_SETFD, FD_CLOEXEC)句作用是設定當執行execv時,關閉該檔案描述符。隨後調用unlink來刪除/dev/__kmsg__檔案,這裡比較特殊,具體解釋下。
當open某個檔案卻還沒有close它時,調用unlink並不能刪除該檔案,該檔案將在調用close後被刪除。對核心來說,當調用open開啟一個檔案,核心維護對應該檔案的資料結構,其中存在一個變數維護當前檔案的引用計數,該資料結構在使用者空間即對應檔案描述符。第一次open後,引用計數為1,調用open將使引用計數加1, 調用close將使得引用計數減1。當調用unlink系統調用時,若檔案引用計數非0,則核心並不會立刻刪除該檔案,核心會在每次close該檔案時檢查引用計數,若為0時將真正刪除檔案。
P.S.根據unlink的mannul,(man 2 unlink),其中寫道:If the name was the last link to a file but any processes still have the file open the  file  will  remain  in existence until the last file descriptor referring to it is closed.
/dev/__kmsg__檔案與/dev/kmsg的裝置節點完全相同,前者同樣是後者的馬甲。該裝置驅動節點是核心記錄檔,核心調用printk函數列印的log可以通過該裝置節點訪問,向該檔案中寫入則等同於執行核心printk。該檔案的內容可通Linux系統標準程式dmesg讀取,Android系統也提供了dmesg命令。

klog.c檔案代碼較少,在此一併分析
static int klog_level = KLOG_DEFAULT_LEVEL;int klog_get_level(void) {    return klog_level;}void klog_set_level(int level) {    klog_level = level;}#define LOG_BUF_MAX 512void klog_vwrite(int level, const char *fmt, va_list ap){    char buf[LOG_BUF_MAX];    if (level > klog_level) return;    if (klog_fd < 0) klog_init();    if (klog_fd < 0) return;    vsnprintf(buf, LOG_BUF_MAX, fmt, ap);    buf[LOG_BUF_MAX - 1] = 0;    write(klog_fd, buf, strlen(buf));}void klog_write(int level, const char *fmt, ...){    va_list ap;    va_start(ap, fmt);    klog_vwrite(level, fmt, ap);    va_end(ap);}
klog_write調用klog_vwrite函數可用於向/dev/__kmesg__中寫入日誌,第一個參數是當前log的層級,如果當前level大於klog_leve則直接返回,即無法將log寫入/dev/__kmesg__中。此外,提供了兩個函數klog_set_level與klog_get_level分別用於設定和讀取當前的klog_level,預設level為KLOG_DEFAULT_LEVEL,在klog.h中定義。
klog.h
#define KLOG_ERROR_LEVEL   3#define KLOG_WARNING_LEVEL 4#define KLOG_NOTICE_LEVEL  5#define KLOG_INFO_LEVEL    6#define KLOG_DEBUG_LEVEL   7#define KLOG_ERROR(tag,x...)   klog_write(KLOG_ERROR_LEVEL, "<3>" tag ": " x)#define KLOG_WARNING(tag,x...) klog_write(KLOG_WARNING_LEVEL, "<4>" tag ": " x)#define KLOG_NOTICE(tag,x...)  klog_write(KLOG_NOTICE_LEVEL, "<5>" tag ": " x)#define KLOG_INFO(tag,x...)    klog_write(KLOG_INFO_LEVEL, "<6>" tag ": " x)#define KLOG_DEBUG(tag,x...)   klog_write(KLOG_DEBUG_LEVEL, "<7>" tag ": " x) #define KLOG_DEFAULT_LEVEL  3  /* messages <= this level are logged */
可見預設層級為3,即KLOG_ERROR_LEVEL,只有調用KLOG_ERROR才能被輸出到/dev/__kmesg__中。
property_init();

這一句用來初始化Android的屬性系統,將在init之屬性系統中專門介紹。

get_hardware_name

get_hardware_name(hardware, &revision)通過讀取/proc/cpuinfo檔案擷取硬體資訊,以筆者的山寨機為例,該檔案內容如下。

[email protected]:/ $ cat /proc/cpuinfo                                        Processor       : ARMv7 Processor rev 1 (v7l)processor       : 0BogoMIPS        : 348.76processor       : 1BogoMIPS        : 348.76processor       : 2BogoMIPS        : 348.76processor       : 3BogoMIPS        : 348.76Features        : swp half thumb fastmult vfp edsp thumbee neon vfpv3 tls vfpv4CPU implementer : 0x41CPU architecture: 7CPU variant     : 0x0CPU part        : 0xc05CPU revision    : 1Hardware        : QRD MSM8625Q SKUDRevision        : 0000Serial          : 0000000000000000
get_hardware_name函數讀取該檔案,將Hardware欄位的值填入hardware數組中,將Revision欄位的值轉換為16進位數字填入revision變數中。

process_kernel_cmdline

接下來init程式調用函數process_kernel_cmdline解析核心啟動參數。核心通常由bootloader(啟動引導程式)載入啟動,目前廣泛使用的bootloader大都基於u-boot定製。核心允許bootloader啟動自己時傳遞參數。在核心啟動完畢之後,啟動參數可通過/proc/cmdline查看。

例如android4.4模擬器啟動後,查看其核心啟動參數,如下
[email protected]:/ # cat /proc/cmdline                                            
qemu.gles=0 qemu=1 console=ttyS0 android.qemud=ttyS1 android.checkjni=1 ndns=1

static void process_kernel_cmdline(void){    /* don't expose the raw commandline to nonpriv processes */    chmod("/proc/cmdline", 0440);    /* first pass does the common stuff, and finds if we are in qemu.     * second pass is only necessary for qemu to export all kernel params     * as props.     */    import_kernel_cmdline(0, import_kernel_nv);    if (qemu[0])        import_kernel_cmdline(1, import_kernel_nv);    /* now propogate the info given on command line to internal variables     * used by init as well as the current required properties     */    export_kernel_boot_props();}
首先修改/proc/cmdline檔案許可權,0440即表明只有root使用者或root組使用者可以讀寫該檔案,其他使用者無法訪問。隨後連續調用import_kernel_cmdline函數,第一個參數標識當前Android裝置是否是模擬器,第二個參數一個函數指標。

import_kernel_cmdline函數將/proc/cmdline內容讀入到內部緩衝區中,並將cmdline內容的以空格拆分成小段字串,依次傳遞給import_kernel_nv函數處理。以前面/proc/cmdline的輸出為例子,該字串共可以拆分成以下幾段

qemu.gles=0qemu=1console=ttyS0android.qemud=ttyS1android.checkjni=1ndns=1
因此在import_kernel_nv將會被連續調用6次,依次傳入上述字串。函數實現如下:

import_kernel_nv

static void import_kernel_nv(char *name, int for_emulator){    char *value = strchr(name, '=');    int name_len = strlen(name);    if (value == 0) return;    *value++ = 0;    if (name_len == 0) return;    if (for_emulator) {        /* in the emulator, export any kernel option with the         * ro.kernel. prefix */        char buff[PROP_NAME_MAX];        int len = snprintf( buff, sizeof(buff), "ro.kernel.%s", name );        if (len < (int)sizeof(buff))            property_set( buff, value );        return;    }    if (!strcmp(name,"qemu")) {        strlcpy(qemu, value, sizeof(qemu));    } else if (!strncmp(name, "androidboot.", 12) && name_len > 12) {        const char *boot_prop_name = name + 12;        char prop[PROP_NAME_MAX];        int cnt;        cnt = snprintf(prop, sizeof(prop), "ro.boot.%s", boot_prop_name);        if (cnt < PROP_NAME_MAX)            property_set(prop, value);    }}
import_kernel_cmdline第一次執行時,傳入import_kernel_nv的形式參數for_emulator為 0,,因此將匹配name是否為qemu,如果是,將其值儲存到qemu全域靜態緩衝區中。對於android模擬器,存在/proc/cmdline中存在“qemu=1”欄位。如果for_emulator為1,則將產生ro.kernel.{name}={value}屬性寫入Android的屬性系統中。

此時回到process_kernel_cmdline函數,繼續執行

if (qemu[0])     import_kernel_cmdline(1, import_kernel_nv);
當系統為模擬器時,qemu[0]其值為‘1‘,第二次執行import_kernel_cmdline,將再次調用6次import_kernel_nv,並且for_emulator為1,因此將產生6個屬性,現在來確定以下我們的分析。

[email protected]:/ # getprop | grep ro.kernel.                                     [ro.kernel.android.checkjni]: [1][ro.kernel.android.qemud]: [ttyS1][ro.kernel.console]: [ttyS0][ro.kernel.ndns]: [1][ro.kernel.qemu.gles]: [0][ro.kernel.qemu]: [1]
可驗證我們的分析是正確的。

export_kernel_boot_props()

接下來繼續執行process_kernel_cmdline函數的最後一句export_kernel_boot_props。由於該函數實現非常直觀,其代碼不在詳細描述。該函數用於設定幾個系統屬性,具體包括如下:
讀取ro.boot.serialno,若存在其值寫入ro.serialno,否則ro.serialno寫入空。讀取ro.boot.mode,若存在其值寫入ro.bootmode,否則ro.bootmode寫入"unkown"讀取ro.boot.baseband,若存在其值寫入ro.baseband,否則ro.baseband寫入"unkown"
讀取ro.boot.bootloader,若存在其值寫入ro.bootloader,否則ro.bootloader寫入"unkown"讀取ro.boot.console,若存在,其值寫入全域緩衝區console中讀取ro.bootmode,若存在,其值儲存到全域緩衝區bootmode中
讀取ro.boot.hardware,若存在其值寫入ro.hardware,否則將/proc/cmdline中解析出來的hardware寫入ro.hardware中。

SELinux

    union selinux_callback cb;    cb.func_log = log_callback;    selinux_set_callback(SELINUX_CB_LOG, cb);    cb.func_audit = audit_callback;    selinux_set_callback(SELINUX_CB_AUDIT, cb);    selinux_initialize();    /* 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.     */    restorecon("/dev");    restorecon("/dev/socket");    restorecon("/dev/__properties__");    restorecon_recursive("/sys");
這部分代碼是在Android4.1之後添加的,隨後伴隨Android系統更新不停迭代。這段代碼主要涉及SELinux初始化。由於SELinux與Android系統啟動關閉不大,暫不分析。

回到init函數<part2>繼續分析

    is_charger = !strcmp(bootmode, "charger");    INFO("property init\n");    property_load_boot_defaults();
第一句將利用bootmode與字串"charger"將其儲存到is_charger變數中,is_charger非0表明但前Android是以充電模式啟動,否則為正常模式。正常啟動模式與充電模式需要啟動的進程不同的,這兩種模式啟動具體啟動的程式差別將在init.rc解析時介紹。

接下來調用INFO宏列印一條log語句,此宏定義在init/log.h中,其實現如下

#define ERROR(x...)   KLOG_ERROR("init", x)#define NOTICE(x...)  KLOG_NOTICE("init", x)#define INFO(x...)    KLOG_INFO("init", x)
顯然這是一條level為KLOG_INFO_LEVEL的log語句。它是否能輸出到/dev/__kmesg__中跟當前klog level的值有關。預設情況下,klog level為3,這條語句將不會輸出到/dev/__kmsg__中。

到這裡init.c main函數之<part2>程式碼分析分析完畢。

接下來<part3>代碼涉及init進程核心功能:init.rc解析。這部分代碼邏輯我們將在獨立文章《Android init原始碼分析(2)init.rc解析》中介紹。

Android init原始碼分析(1)概要分析

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.