(連載)Android 8.0 : 系統啓動流程之init進程(一)

這是一個連載的博文系列,我將持續爲你們提供儘量透徹的Android源碼分析 github連載地址node

前言

上一篇中講到,Linux系統執行完初始化操做最後會執行根目錄下的init文件,init是一個可執行程序, 它的源碼在platform/system/core/init/init.cpp。 以前咱們講過init進程是用戶空間的第一個進程,咱們熟悉的app應用程序都是以它爲父進程的, init進程入口函數是main函數,這個函數作的事情仍是比較多的,主要分爲三個部分linux

  • init進程第一階段
  • init進程第二階段
  • init.rc文件解析

因爲內容比較多,因此對於init的講解,我分爲三個章節來說,本文只講解第一階段,第一階段主要有如下內容android

  • ueventd/watchdogd跳轉及環境變量設置
  • 掛載文件系統並建立目錄
  • 初始化日誌輸出、掛載分區設備
  • 啓用SELinux安全策略
  • 開始第二階段前的準備

本文涉及到的文件git

platform/system/core/init/init.cpp
platform/system/core/init/ueventd.cpp
platform/system/core/init/watchdogd.cpp
platform/system/core/init/log.cpp
platform/system/core/base/logging.cpp
platform/system/core/init/init_first_stage.cpp
platform/external/selinux/libselinux/src/callbacks.c
platform/external/selinux/libselinux/src/load_policy.c
platform/external/selinux/libselinux/src/getenforce.c
platform/external/selinux/libselinux/src/setenforce.c
platform/external/selinux/libselinux/src/android/android.c
複製代碼

1、ueventd/watchdogd跳轉及環境變量設置

/* * 1.C++中主函數有兩個參數,第一個參數argc表示參數個數,第二個參數是參數列表,也就是具體的參數 * 2.init的main函數有兩個其它入口,一是參數中有ueventd,進入ueventd_main,二是參數中有watchdogd,進入watchdogd_main */
int main(int argc, char** argv) {

    /* * 1.strcmp是String的一個函數,比較字符串,相等返回0 * 2.C++中0也能夠表示false * 3.basename是C庫中的一個函數,獲得特定的路徑中的最後一個'/'後面的內容, * 好比/sdcard/miui_recovery/backup,獲得的結果是backup */
    if (!strcmp(basename(argv[0]), "ueventd")) { //當argv[0]的內容爲ueventd時,strcmp的值爲0,!strcmp爲1
    //1表示true,也就執行ueventd_main,ueventd主要是負責設備節點的建立、權限設定等一些列工做
        return ueventd_main(argc, argv);
    }

    if (!strcmp(basename(argv[0]), "watchdogd")) {//watchdogd俗稱看門狗,用於系統出問題時重啓系統
        return watchdogd_main(argc, argv);
    }

    if (REBOOT_BOOTLOADER_ON_PANIC) {
        install_reboot_signal_handlers(); //初始化重啓系統的處理信號,內部經過sigaction 註冊信號,當監聽到該信號時重啓系統
    }

    add_environment("PATH", _PATH_DEFPATH);//註冊環境變量PATH
    //#define _PATH_DEFPATH "/sbin:/system/sbin:/system/bin:/system/xbin:/odm/bin:/vendor/bin:/vendor/xbin"

複製代碼

1.1 ueventd_main

定義在platform/system/core/init/ueventd.cppgithub

Android根文件系統的映像中不存在「/dev」目錄,該目錄是init進程啓動後動態建立的。數組

所以,創建Android中設備節點文件的重任,也落在了init進程身上。爲此,init進程建立子進程ueventd,並將建立設備節點文件的工做託付給ueventd。 ueventd經過兩種方式建立設備節點文件。安全

第一種方式對應「冷插拔」(Cold Plug),即以預先定義的設備信息爲基礎,當ueventd啓動後,統一建立設備節點文件。這一類設備節點文件也被稱爲靜態節點文件。bash

第二種方式對應「熱插拔」(Hot Plug),即在系統運行中,當有設備插入USB端口時,ueventd就會接收到這一事件,爲插入的設備動態建立設備節點文件。這一類設備節點文件也被稱爲動態節點文件。數據結構

int ueventd_main(int argc, char **argv) {
    /* * init sets the umask to 077 for forked processes. We need to * create files with exact permissions, without modification by * the umask. */
    umask(000); //設置新建文件的默認值,這個與chmod相反,這裏至關於新建文件後的權限爲666

    /* Prevent fire-and-forget children from becoming zombies. * If we should need to wait() for some children in the future * (as opposed to none right now), double-forking here instead * of ignoring SIGCHLD may be the better solution. */
    signal(SIGCHLD, SIG_IGN);//忽略子進程終止信號

    InitKernelLogging(argv); //初始化日誌輸出

    LOG(INFO) << "ueventd started!";

    selinux_callback cb;
    cb.func_log = selinux_klog_callback;
    selinux_set_callback(SELINUX_CB_LOG, cb);//註冊selinux相關的用於打印log的回調函數

    ueventd_parse_config_file("/ueventd.rc"); //解析.rc文件,這個後續再講
    ueventd_parse_config_file("/vendor/ueventd.rc");
    ueventd_parse_config_file("/odm/ueventd.rc");

    /* * keep the current product name base configuration so * we remain backwards compatible and allow it to override * everything * TODO: cleanup platform ueventd.rc to remove vendor specific * device node entries (b/34968103) */
    std::string hardware = android::base::GetProperty("ro.hardware", "");
    ueventd_parse_config_file(android::base::StringPrintf("/ueventd.%s.rc", hardware.c_str()).c_str());

    device_init();//建立一個socket來接收uevent,再對內核啓動時註冊到/sys/下的驅動程序進行「冷插拔」處理,以建立對應的節點文件。

    pollfd ufd;
    ufd.events = POLLIN;
    ufd.fd = get_device_fd();//獲取device_init中建立出的socket

    while (true) {//開戶無限循環,隨時監聽驅動
        ufd.revents = 0;
        int nr = poll(&ufd, 1, -1);//監聽來自驅動的uevent
        if (nr <= 0) {
            continue;
        }
        if (ufd.revents & POLLIN) {
            handle_device_fd();//驅動程序進行「熱插拔」處理,以建立對應的節點文件。
        }
    }

    return 0;
}

複製代碼

1.2 watchdogd_main

定義在platform/system/core/init/watchdogd.cppapp

"看門狗"自己是一個定時器電路,內部會不斷的進行計時(或計數)操做,計算機系統和"看門狗"有兩個引腳相鏈接, 正常運行時每隔一段時間就會經過其中一個引腳向"看門狗"發送信號,"看門狗"接收到信號後會將計時器清零並從新開始計時, 而一旦系統出現問題,進入死循環或任何阻塞狀態,不能及時發送信號讓"看門狗"的計時器清零,當計時結束時, "看門狗"就會經過另外一個引腳向系統發送「復位信號」,讓系統重啓

watchdogd_main主要是定時器做用,而DEV_NAME就是那個引腳

int watchdogd_main(int argc, char **argv) {
    InitKernelLogging(argv);

    int interval = 10;
    /* * C++中atoi做用是將字符串轉變爲數值 */
    if (argc >= 2) interval = atoi(argv[1]);

    int margin = 10;
    if (argc >= 3) margin = atoi(argv[2]);

    LOG(INFO) << "watchdogd started (interval " << interval << ", margin " << margin << ")!";

    int fd = open(DEV_NAME, O_RDWR|O_CLOEXEC); //打開文件 /dev/watchdog
    if (fd == -1) {
        PLOG(ERROR) << "Failed to open " << DEV_NAME;
        return 1;
    }

    int timeout = interval + margin;
    /* * ioctl是設備驅動程序中對設備的I/O通道進行管理的函數,WDIOC_SETTIMEOUT是設置超時時間 */
    int ret = ioctl(fd, WDIOC_SETTIMEOUT, &timeout);
    if (ret) {
        PLOG(ERROR) << "Failed to set timeout to " << timeout;
        ret = ioctl(fd, WDIOC_GETTIMEOUT, &timeout);
        if (ret) {
            PLOG(ERROR) << "Failed to get timeout";
        } else {
            if (timeout > margin) {
                interval = timeout - margin;
            } else {
                interval = 1;
            }
            LOG(WARNING) << "Adjusted interval to timeout returned by driver: "
                         << "timeout " << timeout
                         << ", interval " << interval
                         << ", margin " << margin;
        }
    }

    while (true) {//每間隔必定時間往文件中寫入一個空字符,這就是看門狗的關鍵了
        write(fd, "", 1);
        sleep(interval);
    }
}
複製代碼

1.3 install_reboot_signal_handlers

定義在platform/system/core/init/init.cpp

這個函數主要做用將各類信號量,如SIGABRT,SIGBUS等的行爲設置爲SA_RESTART,一旦監聽到這些信號即執行重啓系統

static void install_reboot_signal_handlers() {
    // Instead of panic'ing the kernel as is the default behavior when init crashes,
    // we prefer to reboot to bootloader on development builds, as this will prevent
    // boot looping bad configurations and allow both developers and test farms to easily
    // recover.
    struct sigaction action;
    memset(&action, 0, sizeof(action));
    sigfillset(&action.sa_mask);//將全部信號加入至信號集
    action.sa_handler = [](int) {
        // panic() reboots to bootloader
        panic(); //重啓系統
    };
    action.sa_flags = SA_RESTART;
    sigaction(SIGABRT, &action, nullptr);
    sigaction(SIGBUS, &action, nullptr);
    sigaction(SIGFPE, &action, nullptr);
    sigaction(SIGILL, &action, nullptr);
    sigaction(SIGSEGV, &action, nullptr);
#if defined(SIGSTKFLT)
    sigaction(SIGSTKFLT, &action, nullptr);
#endif
    sigaction(SIGSYS, &action, nullptr);
    sigaction(SIGTRAP, &action, nullptr);
}
複製代碼

1.4 add_environment

定義在platform/system/core/init/init.cpp

這個函數主要做用是將一個鍵值對放到一個Char數組中,若是數組中有key就替換,沒有就插入,跟Java中的Map差很少

/* add_environment - add "key=value" to the current environment */
int add_environment(const char *key, const char *val) {
    size_t n;
    size_t key_len = strlen(key);

    /* The last environment entry is reserved to terminate the list */
    for (n = 0; n < (arraysize(ENV) - 1); n++) {

        /* Delete any existing entry for this key */
        if (ENV[n] != NULL) {
        /* * C++中strcspn用於返回字符所在下標,至關於String的indexof */
            size_t entry_key_len = strcspn(ENV[n], "=");
            if ((entry_key_len == key_len) && (strncmp(ENV[n], key, entry_key_len) == 0)) { //若是key相同,刪除對應數據
                free((char*)ENV[n]);
                ENV[n] = NULL;
            }
        }

        /* Add entry if a free slot is available */
        if (ENV[n] == NULL) { //若是沒有對應key,則插入數據
            char* entry;
            asprintf(&entry, "%s=%s", key, val);
            ENV[n] = entry;
            return 0;
        }
    }

    LOG(ERROR) << "No env. room to store: '" << key << "':'" << val << "'";

    return -1;
}
複製代碼

2、 掛載文件系統並建立目錄

bool is_first_stage = (getenv("INIT_SECOND_STAGE") == nullptr);//查看是否有環境變量INIT_SECOND_STAGE

    /* * 1.init的main方法會執行兩次,由is_first_stage控制,first_stage就是第一階段要作的事 */
    if (is_first_stage) {//只執行一次,由於在方法體中有設置INIT_SECOND_STAGE
        boot_clock::time_point start_time = boot_clock::now();

        // Clear the umask.
        umask(0); //清空文件權限

        // 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.
        mount("tmpfs", "/dev", "tmpfs", MS_NOSUID, "mode=0755");
        mkdir("/dev/pts", 0755);
        mkdir("/dev/socket", 0755);
        mount("devpts", "/dev/pts", "devpts", 0, NULL);
        #define MAKE_STR(x) __STRING(x)
        mount("proc", "/proc", "proc", 0, "hidepid=2,gid=" MAKE_STR(AID_READPROC));
        // Don't expose the raw commandline to unprivileged processes.
        chmod("/proc/cmdline", 0440);
        gid_t groups[] = { AID_READPROC };
        setgroups(arraysize(groups), groups);
        mount("sysfs", "/sys", "sysfs", 0, NULL);
        mount("selinuxfs", "/sys/fs/selinux", "selinuxfs", 0, NULL);
        mknod("/dev/kmsg", S_IFCHR | 0600, makedev(1, 11));
        mknod("/dev/random", S_IFCHR | 0666, makedev(1, 8));
        mknod("/dev/urandom", S_IFCHR | 0666, makedev(1, 9));

        ...

    }

   ...


}


複製代碼

2.1 mount

mount是用來掛載文件系統的,mount屬於Linux系統調用

int mount(const char *source, const char *target, const char *filesystemtype, unsigned long mountflags, const void *data);
複製代碼

參數:

source:將要掛上的文件系統,一般是一個設備名。

target:文件系統所要掛載的目標目錄。

filesystemtype:文件系統的類型,能夠是"ext2","msdos","proc","ntfs","iso9660"。。。

mountflags:指定文件系統的讀寫訪問標誌,可能值有如下

參數 含義
MS_BIND 執行bind掛載,使文件或者子目錄樹在文件系統內的另外一個點上可視。
MS_DIRSYNC 同步目錄的更新。
MS_MANDLOCK 容許在文件上執行強制鎖。
MS_MOVE 移動子目錄樹。
MS_NOATIME 不要更新文件上的訪問時間。
MS_NODEV 不容許訪問設備文件。
MS_NODIRATIME 不容許更新目錄上的訪問時間。
MS_NOEXEC 不容許在掛上的文件系統上執行程序。
MS_NOSUID 執行程序時,不遵守set-user-ID和set-group-ID位。
MS_RDONLY 指定文件系統爲只讀。
MS_REMOUNT 從新加載文件系統。這容許你改變現存文件系統的mountflag和數據,而無需使用先卸載,再掛上文件系統的方式。
MS_SYNCHRONOUS 同步文件的更新。
MNT_FORCE 強制卸載,即便文件系統處於忙狀態。
MNT_EXPIRE 將掛載點標記爲過期。

data:文件系統特有的參數

在init初始化過程當中,Android分別掛載了tmpfs,devpts,proc,sysfs,selinuxfs這5類文件系統。

tmpfs是一種虛擬內存文件系統,它會將全部的文件存儲在虛擬內存中, 若是你將tmpfs文件系統卸載後,那麼其下的全部的內容將不復存在。 tmpfs既可使用RAM,也可使用交換分區,會根據你的實際須要而改變大小。 tmpfs的速度很是驚人,畢竟它是駐留在RAM中的,即便用了交換分區,性能仍然很是卓越。 因爲tmpfs是駐留在RAM的,所以它的內容是不持久的。 斷電後,tmpfs的內容就消失了,這也是被稱做tmpfs的根本緣由。

devpts文件系統爲僞終端提供了一個標準接口,它的標準掛接點是/dev/ pts。 只要pty的主複合設備/dev/ptmx被打開,就會在/dev/pts下動態的建立一個新的pty設備文件。

proc文件系統是一個很是重要的虛擬文件系統,它能夠看做是內核內部數據結構的接口, 經過它咱們能夠得到系統的信息,同時也可以在運行時修改特定的內核參數。

與proc文件系統相似,sysfs文件系統也是一個不佔有任何磁盤空間的虛擬文件系統。 它一般被掛接在/sys目錄下。sysfs文件系統是Linux2.6內核引入的, 它把鏈接在系統上的設備和總線組織成爲一個分級的文件,使得它們能夠在用戶空間存取

selinuxfs也是虛擬文件系統,一般掛載在/sys/fs/selinux目錄下,用來存放SELinux安全策略文件

2.2 mknod

mknod用於建立Linux中的設備文件

int mknod(const char* path, mode_t mode, dev_t dev) {

}
複製代碼

參數: path:設備所在目錄 mode:指定設備的類型和讀寫訪問標誌 可能的類型

參數 含義
S_IFMT type of file ,文件類型掩碼
S_IFREG regular 普通文件
S_IFBLK block special 塊設備文件
S_IFDIR directory 目錄文件
S_IFCHR character special 字符設備文件
S_IFIFO fifo 管道文件
S_IFNAM special named file 特殊文件
S_IFLNK symbolic link 連接文件

dev 表示設備,由makedev(1, 9) 函數建立,9爲主設備號、1爲次設備號

2.3 其餘命令

mkdir也是Linux系統調用,做用是建立目錄,第一個參數是目錄路徑,第二個是讀寫權限

chmod用於修改文件/目錄的讀寫權限

setgroups 用來將list 數組中所標明的組加入到目前進程的組設置中

這裏我解釋下文件的權限,也就是相似0755這種,要理解權限首先要明白「用戶和組」的概念

Linux系統能夠有多個用戶,多個用戶能夠屬於同一個組,用戶和組的概念就像咱們人和家庭同樣,人屬於家庭的一分子,用戶屬於一個組,咱們通常在Linux終端輸入ls -al以後會有以下結果

drwxr-xr-x  7 foxleezh foxleezh   4096 2月  24 14:31 .android
複製代碼

第一個foxleezh表示全部者,這裏的foxleezh表示一個用戶,相似foxleezh這我的

第二個foxleezh表示文件全部用戶組,這裏的foxleezh表示一個組,相似foxleezh這個家庭

而後咱們來看下dwxr-xr-x,這個要分紅四部分來理解,d表示目錄(文件用 - 表示),wxr表示全部者權限,xr表示文件全部用戶組的權限,x表示其餘用戶的權限

  • w- 表示寫權限,用2表示
  • x- 表示執行權限,用1表示
  • r- 表示讀取權限,用4表示 那麼dwxr-xr-x還有種表示方法就是751,是否是感受跟0755差很少了,那0755前面那個0表示什麼意思呢?

0755前面的0跟suid和guid有關

  • suid意味着其餘用戶擁有和文件全部者同樣的權限,用4表示
  • guid意味着其餘用戶擁有和文件全部用戶組同樣的權限,用2表示

3、 初始化日誌輸出、掛載分區設備

if (is_first_stage) {

          ...

        // Now that tmpfs is mounted on /dev and we have /dev/kmsg, we can actually
        // talk to the outside world...
        InitKernelLogging(argv);

        LOG(INFO) << "init first stage started!";

        if (!DoFirstStageMount()) {
            LOG(ERROR) << "Failed to mount required partitions early ...";
            panic();//重啓系統
        }

        ...
    }
複製代碼

3.1 InitKernelLogging

定義在platform/system/core/init/log.cpp

InitKernelLogging首先是將標準輸入輸出重定向到"/sys/fs/selinux/null",而後調用InitLogging初始化log日誌系統

void InitKernelLogging(char* argv[]) {
    // Make stdin/stdout/stderr all point to /dev/null.
    int fd = open("/sys/fs/selinux/null", O_RDWR); //打開文件
    if (fd == -1) {
        int saved_errno = errno;
        android::base::InitLogging(argv, &android::base::KernelLogger);
        errno = saved_errno;
        PLOG(FATAL) << "Couldn't open /sys/fs/selinux/null";
    }
    /* * dup2(int old_fd, int new_fd) 的做用是複製文件描述符,將old複製到new,下文中將 * 0、一、2綁定到null設備上,經過標準的輸入輸出沒法輸出信息 */
    dup2(fd, 0); //重定向標準輸入stdin
    dup2(fd, 1);//重定向標準輸出stdout
    dup2(fd, 2);//重定向標準錯誤stderr
    if (fd > 2) close(fd);

    android::base::InitLogging(argv, &android::base::KernelLogger);//初始化log
}
複製代碼

3.2 InitLogging

定義在platform/system/core/base/logging.cpp

InitLogging主要工做是設置logger和aborter的處理函數,而後設置日誌系統輸出等級

void InitLogging(char* argv[], LogFunction&& logger, AbortFunction&& aborter) {
/* * C++中foo(std::forward<T>(arg))表示將arg按本來的左值或右值,傳遞給foo方法, LogFunction& 這種表示是左值,LogFunction&&這種表示是右值 */
  SetLogger(std::forward<LogFunction>(logger)); //設置logger處理函數
  SetAborter(std::forward<AbortFunction>(aborter));//設置aborter處理函數

  if (gInitialized) {
    return;
  }

  gInitialized = true;

  // Stash the command line for later use. We can use /proc/self/cmdline on
  // Linux to recover this, but we don't have that luxury on the Mac/Windows,
  // and there are a couple of argv[0] variants that are commonly used.
  if (argv != nullptr) {
    std::lock_guard<std::mutex> lock(LoggingLock());
    ProgramInvocationName() = basename(argv[0]);
  }

  const char* tags = getenv("ANDROID_LOG_TAGS");//獲取系統當前日誌輸出等級
  if (tags == nullptr) {
    return;
  }

  std::vector<std::string> specs = Split(tags, " "); //將tags以空格拆分紅數組
  for (size_t i = 0; i < specs.size(); ++i) {
    // "tag-pattern:[vdiwefs]"
    std::string spec(specs[i]);
    if (spec.size() == 3 && StartsWith(spec, "*:")) { //若是字符數爲3且以*:開頭
     //那麼根據第三個字符來設置日誌輸出等級(好比*:d,就是DEBUG級別)
      switch (spec[2]) {
        case 'v':
          gMinimumLogSeverity = VERBOSE;
          continue;
        case 'd':
          gMinimumLogSeverity = DEBUG;
          continue;
        case 'i':
          gMinimumLogSeverity = INFO;
          continue;
        case 'w':
          gMinimumLogSeverity = WARNING;
          continue;
        case 'e':
          gMinimumLogSeverity = ERROR;
          continue;
        case 'f':
          gMinimumLogSeverity = FATAL_WITHOUT_ABORT;
          continue;
        // liblog will even suppress FATAL if you say 's' for silent, but that's
        // crazy!
        case 's':
          gMinimumLogSeverity = FATAL_WITHOUT_ABORT;
          continue;
      }
    }
    LOG(FATAL) << "unsupported '" << spec << "' in ANDROID_LOG_TAGS (" << tags
               << ")";
  }
}
複製代碼

3.3 KernelLogger

定義在platform/system/core/base/logging.cpp

在InitKernelLogging方法中有句調用

android::base::InitLogging(argv, &android::base::KernelLogger);
複製代碼

這句的做用就是將KernelLogger函數做爲log日誌的處理函數,KernelLogger主要做用就是將要輸出的日誌格式化以後寫入到 /dev/kmsg 設備中

void KernelLogger(android::base::LogId, android::base::LogSeverity severity, const char* tag, const char*, unsigned int, const char* msg) {
  // clang-format off
  static constexpr int kLogSeverityToKernelLogLevel[] = {
      [android::base::VERBOSE] = 7,              // KERN_DEBUG (there is no verbose kernel log
                                                 // level)
      [android::base::DEBUG] = 7,                // KERN_DEBUG
      [android::base::INFO] = 6,                 // KERN_INFO
      [android::base::WARNING] = 4,              // KERN_WARNING
      [android::base::ERROR] = 3,                // KERN_ERROR
      [android::base::FATAL_WITHOUT_ABORT] = 2,  // KERN_CRIT
      [android::base::FATAL] = 2,                // KERN_CRIT
  };
  // clang-format on
  static_assert(arraysize(kLogSeverityToKernelLogLevel) == android::base::FATAL + 1,
                "Mismatch in size of kLogSeverityToKernelLogLevel and values in LogSeverity");
  //static_assert是編譯斷言,若是第一個參數爲true,那麼編譯就不經過,這裏是判斷kLogSeverityToKernelLogLevel數組個數不能大於7

  static int klog_fd = TEMP_FAILURE_RETRY(open("/dev/kmsg", O_WRONLY | O_CLOEXEC)); //打開 /dev/kmsg 文件
  if (klog_fd == -1) return;

  int level = kLogSeverityToKernelLogLevel[severity];//根據傳入的日誌等級獲得Linux的日誌等級,也就是kLogSeverityToKernelLogLevel對應下標的映射

  // The kernel's printk buffer is only 1024 bytes.
  // TODO: should we automatically break up long lines into multiple lines?
  // Or we could log but with something like "..." at the end?
  char buf[1024];
  size_t size = snprintf(buf, sizeof(buf), "<%d>%s: %s\n", level, tag, msg);//格式化日誌輸出
  if (size > sizeof(buf)) {
    size = snprintf(buf, sizeof(buf), "<%d>%s: %zu-byte message too long for printk\n",
                    level, tag, size);
  }

  iovec iov[1];
  iov[0].iov_base = buf;
  iov[0].iov_len = size;
  TEMP_FAILURE_RETRY(writev(klog_fd, iov, 1));//將日誌寫入到 /dev/kmsg 中
} 

複製代碼

3.3 DoFirstStageMount

定義在platform/system/core/init/init_first_stage.cpp

主要做用是初始化特定設備並掛載

bool DoFirstStageMount() {
    // Skips first stage mount if we're in recovery mode.
    if (IsRecoveryMode()) { //若是是刷機模式,直接跳過掛載
        LOG(INFO) << "First stage mount skipped (recovery mode)";
        return true;
    }

    // Firstly checks if device tree fstab entries are compatible.
    if (!is_android_dt_value_expected("fstab/compatible", "android,fstab")) { //若是fstab/compatible的值不是android,fstab,直接跳過掛載
        LOG(INFO) << "First stage mount skipped (missing/incompatible fstab in device tree)";
        return true;
    }

    std::unique_ptr<FirstStageMount> handle = FirstStageMount::Create();
    if (!handle) {
        LOG(ERROR) << "Failed to create FirstStageMount";
        return false;
    }
    return handle->DoFirstStageMount(); //主要是初始化特定設備並掛載
} 
複製代碼

3.4 handle->DoFirstStageMount

定義在platform/system/core/init/init_first_stage.cpp

這裏主要做用是去解析/proc/device-tree/firmware/android/fstab,而後獲得"/system", "/vendor", "/odm"三個目錄的掛載信息

FirstStageMount::FirstStageMount()
    : need_dm_verity_(false), device_tree_fstab_(fs_mgr_read_fstab_dt(), fs_mgr_free_fstab) {
    if (!device_tree_fstab_) {
        LOG(ERROR) << "Failed to read fstab from device tree";
        return;
    }
    for (auto mount_point : {"/system", "/vendor", "/odm"}) {
        fstab_rec* fstab_rec =
            fs_mgr_get_entry_for_mount_point(device_tree_fstab_.get(), mount_point); //這裏主要是把掛載的信息解析出來
        if (fstab_rec != nullptr) {
            mount_fstab_recs_.push_back(fstab_rec);//將掛載信息放入數組中存起來
        }
    }
} 
複製代碼

4、啓用SELinux安全策略

SELinux是「Security-Enhanced Linux」的簡稱,是美國國家安全局「NSA=The National Security Agency」 和SCC(Secure Computing Corporation)開發的 Linux的一個擴張強制訪問控制安全模塊。 在這種訪問控制體系的限制下,進程只能訪問那些在他的任務中所須要文件

if (is_first_stage) {

          ...
          
        //Avb即Android Verfied boot,功能包括Secure Boot, verfying boot 和 dm-verity, 
        //原理都是對二進制文件進行簽名,在系統啓動時進行認證,確保系統運行的是合法的二進制鏡像文件。
        //其中認證的範圍涵蓋:bootloader,boot.img,system.img
        SetInitAvbVersionInRecovery();//在刷機模式下初始化avb的版本,不是刷機模式直接跳過

        // Set up SELinux, loading the SELinux policy.
        selinux_initialize(true);//加載SELinux policy,也就是安全策略,
        

        // We're in the kernel domain, so re-exec init to transition to the init domain now
        // that the SELinux policy has been loaded.

        /* * 1.這句英文大概意思是,咱們執行第一遍時是在kernel domain,因此要從新執行init文件,切換到init domain, * 這樣SELinux policy才已經加載進來了 * 2.後面的security_failure函數會調用panic重啓系統 */
        if (restorecon("/init") == -1) { //restorecon命令用來恢復SELinux文件屬性即恢復文件的安全上下文
            PLOG(ERROR) << "restorecon failed";
            security_failure(); //失敗則重啓系統
        }

        ...
    }
複製代碼

4.1 selinux_initialize

定義在platform/system/core/init/init.cpp

static void selinux_initialize(bool in_kernel_domain) {
    Timer t;

    selinux_callback cb;
    cb.func_log = selinux_klog_callback;
    selinux_set_callback(SELINUX_CB_LOG, cb); //設置selinux的日誌輸出處理函數
    cb.func_audit = audit_callback;
    selinux_set_callback(SELINUX_CB_AUDIT, cb);//設置selinux的記錄權限檢測的處理函數

    if (in_kernel_domain) {//這裏是分了兩個階段,第一階段in_kernel_domain爲true,第二階段爲false
        LOG(INFO) << "Loading SELinux policy";
        if (!selinux_load_policy()) {  //加載selinux的安全策略
            panic();
        }

        bool kernel_enforcing = (security_getenforce() == 1); //獲取當前kernel的工做模式
        bool is_enforcing = selinux_is_enforcing(); //獲取工做模式的配置
        if (kernel_enforcing != is_enforcing) { //若是當前的工做模式與配置的不一樣,就將當前的工做模式改掉
            if (security_setenforce(is_enforcing)) {
                PLOG(ERROR) << "security_setenforce(%s) failed" << (is_enforcing ? "true" : "false");
                security_failure();
            }
        }

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

        // init's first stage can't set properties, so pass the time to the second stage.
        setenv("INIT_SELINUX_TOOK", std::to_string(t.duration_ms()).c_str(), 1);
    } else {
        selinux_init_all_handles(); //第二階段時初始化處理函數
    }
} 
複製代碼

4.2 selinux_set_callback

定義在platform/external/selinux/libselinux/src/callbacks.c

主要就是根據不一樣的type設置回調函數,selinux_log,selinux_audit這些都是函數指針

void selinux_set_callback(int type, union selinux_callback cb) {
	switch (type) {
	case SELINUX_CB_LOG:
		selinux_log = cb.func_log;
		break;
	case SELINUX_CB_AUDIT:
		selinux_audit = cb.func_audit;
		break;
	case SELINUX_CB_VALIDATE:
		selinux_validate = cb.func_validate;
		break;
	case SELINUX_CB_SETENFORCE:
		selinux_netlink_setenforce = cb.func_setenforce;
		break;
	case SELINUX_CB_POLICYLOAD:
		selinux_netlink_policyload = cb.func_policyload;
		break;
	}
} 
複製代碼

4.3 selinux_load_policy

定義在platform/system/core/init/init.cpp

這裏區分了兩種狀況,這兩種狀況只是區分從哪裏加載安全策略文件,第一個是從 /vendor/etc/selinux/precompiled_sepolicy 讀取 ,第二個是從 /sepolicy 讀取,他們最終都是調用selinux_android_load_policy_from_fd方法

static bool selinux_load_policy() {
    return selinux_is_split_policy_device() ? selinux_load_split_policy()
                                            : selinux_load_monolithic_policy();
} 
複製代碼

4.4 selinux_android_load_policy_from_fd

定義在platform/external/selinux/libselinux/src/android/android.c

這個函數主要做用是設置selinux_mnt 的值爲/sys/fs/selinux ,而後調用security_load_policy

int selinux_android_load_policy_from_fd(int fd, const char *description) {
	int rc;
	struct stat sb;
	void *map = NULL;
	static int load_successful = 0;

	/* * Since updating policy at runtime has been abolished * we just check whether a policy has been loaded before * and return if this is the case. * There is no point in reloading policy. */
	if (load_successful){
	  selinux_log(SELINUX_WARNING, "SELinux: Attempted reload of SELinux policy!/n");
	  return 0;
	}

	set_selinuxmnt(SELINUXMNT); //SELINUXMNT的值爲 /sys/fs/selinux 
	if (fstat(fd, &sb) < 0) {
		selinux_log(SELINUX_ERROR, "SELinux: Could not stat %s: %s\n",
				description, strerror(errno));
		return -1;
	}
	/* * mmap 的做用是將一個文件或者其它對象映射進內存 */
	map = mmap(NULL, sb.st_size, PROT_READ, MAP_PRIVATE, fd, 0); 
	if (map == MAP_FAILED) {
		selinux_log(SELINUX_ERROR, "SELinux: Could not map %s: %s\n",
				description, strerror(errno));
		return -1;
	}

	rc = security_load_policy(map, sb.st_size);
	if (rc < 0) {
		selinux_log(SELINUX_ERROR, "SELinux: Could not load policy: %s\n",
				strerror(errno));
		munmap(map, sb.st_size);
		return -1;
	}

	munmap(map, sb.st_size);
	selinux_log(SELINUX_INFO, "SELinux: Loaded policy from %s\n", description);
	load_successful = 1;
	return 0;
} 
複製代碼

4.5 security_load_policy

定義在platform/external/selinux/libselinux/src/load_policy.c

這個函數主要做用就是寫入data到/sys/fs/selinux,data其實就是以前找的那些策略文件,由此咱們知道,看起來selinux_load_policy調用這麼多代碼, 其實只是將策略文件拷貝到 /sys/fs/selinux 目錄下

int security_load_policy(void *data, size_t len) {
	char path[PATH_MAX];
	int fd, ret;

	if (!selinux_mnt) { //selinux_mnt的值爲 /sys/fs/selinux 
		errno = ENOENT;
		return -1;
	}

	snprintf(path, sizeof path, "%s/load", selinux_mnt);
	fd = open(path, O_RDWR); //打開 /sys/fs/selinux ,而後將data的值寫入
	if (fd < 0)
		return -1;

	ret = write(fd, data, len);
	close(fd);
	if (ret < 0)
		return -1;
	return 0;
} 
複製代碼

4.6 security_setenforce

定義在platform/external/selinux/libselinux/src/setenforce.c

selinux有兩種工做模式:

  • permissive,全部的操做都被容許(即沒有MAC),可是若是違反權限的話,會記錄日誌,通常eng模式用
  • enforcing,全部操做都會進行權限檢查。通常user和user-debug模式用

不論是security_setenforce仍是security_getenforce都是去操做/sys/fs/selinux/enforce 文件, 0表示permissive 1表示enforcing

int security_setenforce(int value) {
	int fd, ret;
	char path[PATH_MAX];
	char buf[20];

	if (!selinux_mnt) {
		errno = ENOENT;
		return -1;
	}

	snprintf(path, sizeof path, "%s/enforce", selinux_mnt);
	fd = open(path, O_RDWR); //打開 /sys/fs/selinux/enforce 文件
	if (fd < 0)
		return -1;

	snprintf(buf, sizeof buf, "%d", value);
	ret = write(fd, buf, strlen(buf)); //將value的值寫入文件
	close(fd);
	if (ret < 0)
		return -1;

	return 0;
} 
複製代碼

5、開始第二階段前的準備

這裏主要就是設置一些變量如INIT_SECOND_STAGE,INIT_STARTED_AT,爲第二階段作準備,而後再次調用init的main函數,啓動用戶態的init進程

if (is_first_stage) {

          ...

        setenv("INIT_SECOND_STAGE", "true", 1);

        static constexpr uint32_t kNanosecondsPerMillisecond = 1e6;
        uint64_t start_ms = start_time.time_since_epoch().count() / kNanosecondsPerMillisecond;
        setenv("INIT_STARTED_AT", StringPrintf("%" PRIu64, start_ms).c_str(), 1);//記錄第二階段開始時間戳

        char* path = argv[0];
        char* args[] = { path, nullptr };
        execv(path, args); //從新執行main方法,進入第二階段

        // execv() only returns if an error happened, in which case we
        // panic and never fall through this conditional.
        PLOG(ERROR) << "execv(\"" << path << "\") failed";
        security_failure();
    }
複製代碼

小結

init進程第一階段作的主要工做是掛載分區,建立設備節點和一些關鍵目錄,初始化日誌輸出系統,啓用SELinux安全策略

下一篇我將講解init進程第二階段

相關文章
相關標籤/搜索