Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android init源代碼分析(1)概要分析

Android init源代碼分析(1)概要分析

編輯:關於Android編程

功能概述

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主要完成三大功能:
解析init.rc初始化Android屬性系統,並維護屬性服務初始化Android屬性系統,並維護屬性服務
處理子進程啟動、停止、重啟動 /ueventd用於創建設備驅動節點。 /watchdogd 是看門狗服務進程。

代碼分析

分析代碼當先抓住主干,了解其大致結構與流程,再逐塊深入,分析其實現細節。這樣先大局再細節的方法可以讓我們在閱讀代碼時保持頭腦的清醒,切忌不可在沒有對整體流程了解的情況下深入細節,那很容易導致我們迷失在代碼森林中。
接下來分析init.c的main函數。為了方便分析,將main函數代碼做了精簡,代碼如下。
int main(int argc, char **argv)
{
    //
    if (!strcmp(basename(argv[0]), "ueventd"))
        return ueventd_main(argc, argv);
    if (!strcmp(basename(argv[0]), "watchdogd"))
        return watchdogd_main(argc, argv);
   
    //
    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();
    ....
    //
    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");
   
    //
    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,下面分別做具體說明。

代碼

通過命令行判斷argv[0]的字符串內容,來區分當前程序是init,ueventd或是watchdogd。
C程序的main函數原型為 main(int argc, char* argv[]), ueventd以及watchdogd的啟動都在init.rc中描述,由init進程解析後執行fork、exec啟動,因此其入口參數的構造在init代碼中,將在init.rc解析時分析。此時我們只需要直到argv[0]中將存儲可執行文件的名字。

代碼

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) 代碼隨後的代碼如下。
    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設備,但是細心的讀者可能會有疑問,在代碼中雖然掛載了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 512
void 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文件獲取硬件信息,以筆者的山寨機為例,該文件內容如下。

shell@android:/ $ cat /proc/cpuinfo                                        
Processor       : ARMv7 Processor rev 1 (v7l)
processor       : 0
BogoMIPS        : 348.76

processor       : 1
BogoMIPS        : 348.76

processor       : 2
BogoMIPS        : 348.76

processor       : 3
BogoMIPS        : 348.76

Features        : swp half thumb fastmult vfp edsp thumbee neon vfpv3 tls vfpv4
CPU implementer : 0x41
CPU architecture: 7
CPU variant     : 0x0
CPU part        : 0xc05
CPU revision    : 1

Hardware        : QRD MSM8625Q SKUD
Revision        : 0000
Serial          : 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模擬器啟動後,查看其內核啟動參數,如下
root@generic:/ # 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=0
qemu=1
console=ttyS0
android.qemud=ttyS1
android.checkjni=1
ndns=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個屬性,現在來確定以下我們的分析。

root@generic:/ # 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函數繼續分析

    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函數之代碼分析分析完畢。

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

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved