1. 程式人生 > >Android 8.0 系統啟動流程之init程序--第一階段(四)

Android 8.0 系統啟動流程之init程序--第一階段(四)

1、概述

    上一篇中講到,Linux系統執行完初始化操作最後會執行根目錄下的init檔案,init是一個可執行程式,它的原始碼在platform/system/core/init/init.cpp。init程序是使用者空間的第一個程序,我們熟悉的app應用程式都是以它為父程序的,init程序入口函式是main函式,這個函式做的事情還是比較多的,主要分為三個部分

  1. init程序第一階段
  2. init程序第二階段
  3. init.rc檔案解析

由於內容比較多,所以對於init程序的分析,將分為三個章節,本文只講解第一階段,第一階段主要有以下內容

  • ueventd/watchdogd跳轉及環境變數設定
  • 掛載檔案系統並建立目錄
  • 初始化日誌輸出、掛載分割槽裝置
  • 啟用SELinux安全策略
  • 開始第二階段前的準備

2、init程序入口

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

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")) { return ueventd_main(argc, argv); } if (!strcmp(basename(argv[0]), "watchdogd")) { return watchdogd_main(argc, argv); } if (REBOOT_BOOTLOADER_ON_PANIC) { //初始化重啟系統的處理訊號,內部通過sigaction 註冊訊號,當監聽到該訊號時重啟系統 InstallRebootSignalHandlers(); } //註冊環境變數PATH
// _PATH_DEFPATH 是定義在bionic/libc/include/paths.h中 add_environment("PATH", _PATH_DEFPATH); bool is_first_stage = (getenv("INIT_SECOND_STAGE") == nullptr); if (is_first_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)); // 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(); } SetInitAvbVersionInRecovery(); // Set up SELinux, loading the SELinux policy. selinux_initialize(true); // We're in the kernel domain, so re-exec init to transition to the init domain now // that the SELinux policy has been loaded. if (selinux_android_restorecon("/init", 0) == -1) { PLOG(ERROR) << "restorecon failed"; security_failure(); } 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", std::to_string(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(); } ...... }

3、ueventd/watchdogd跳轉及環境變數設定

3.1 ueventd_main

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

Android根檔案系統的映像中不存在“/dev”目錄,該目錄是init程序啟動後動態建立的。
因此,建立Android中裝置節點檔案的重任,也落在了init程序身上。為此,init程序建立子程序ueventd,並將建立裝置節點檔案的工作託付給ueventd。ueventd通過兩種方式建立裝置節點檔案。

第一種方式對應“冷插拔”(Cold Plug),即以預先定義的裝置資訊為基礎,當ueventd啟動後,統一建立裝置節點檔案。這一類裝置節點檔案也被稱為靜態節點檔案。

第二種方式對應“熱插拔”(Hot Plug),即在系統執行中,當有裝置插入USB埠時,ueventd就會接收到這一事件,為插入的裝置動態建立裝置節點檔案。這一類裝置節點檔案也被稱為動態節點檔案。

DeviceHandler CreateDeviceHandler() {
    Parser parser;

    std::vector<Subsystem> subsystems;
    parser.AddSectionParser("subsystem", std::make_unique<SubsystemParser>(&subsystems));

    using namespace std::placeholders;
    std::vector<SysfsPermissions> sysfs_permissions;
    std::vector<Permissions> dev_permissions;
    parser.AddSingleLineParser(
        "/sys/", std::bind(ParsePermissionsLine, _1, _2, &sysfs_permissions, nullptr));
    parser.AddSingleLineParser("/dev/",
                               std::bind(ParsePermissionsLine, _1, _2, nullptr, &dev_permissions));

    parser.ParseConfig("/ueventd.rc");//解析.rc檔案,這個後續再講
    parser.ParseConfig("/vendor/ueventd.rc");
    parser.ParseConfig("/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", "");
    parser.ParseConfig("/ueventd." + hardware + ".rc");

    return DeviceHandler(std::move(dev_permissions), std::move(sysfs_permissions),
                         std::move(subsystems), true);
}

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
    InitKernelLogging(argv);//初始化日誌輸出

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

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

    DeviceHandler device_handler = CreateDeviceHandler();
    UeventListener uevent_listener;

    if (access(COLDBOOT_DONE, F_OK) != 0) {
        ColdBoot cold_boot(uevent_listener, device_handler);
        cold_boot.Run();//冷啟動
    }

    // We use waitpid() in ColdBoot, so we can't ignore SIGCHLD until now.
    signal(SIGCHLD, SIG_IGN);//忽略子程序終止訊號
    // Reap and pending children that exited between the last call to waitpid() and setting SIG_IGN
    // for SIGCHLD above.
    while (waitpid(-1, nullptr, WNOHANG) > 0) {
    }
    //監聽來自驅動的uevent,進行“熱插拔”處理
    uevent_listener.Poll([&device_handler](const Uevent& uevent) {
        HandleFirmwareEvent(uevent);
        device_handler.HandleDeviceEvent(uevent);
        return ListenerAction::kContinue;
    });

    return 0;
}

3.2 watchdogd_main

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

“看門狗”本身是一個定時器電路,內部會不斷的進行計時(或計數)操作,計算機系統和”看門狗”有兩個引腳相連線,正常執行時每隔一段時間就會通過其中一個引腳向”看門狗”傳送訊號,”看門狗”接收到訊號後會將計時器清零並重新開始計時,而一旦系統出現問題,進入死迴圈或任何阻塞狀態,不能及時傳送訊號讓”看門狗”的計時器清零,當計時結束時,”看門狗”就會通過另一個引腳向系統傳送“復位訊號”,讓系統重啟。
watchdogd_main主要是定時器作用,而DEV_NAME就是那個引腳

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

    int interval = 10;
    if (argc >= 2) interval = atoi(argv[1]);//atoi作用是將字串轉變為數值

    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);
    }
}

3.3 InstallRebootSignalHandlers

定義在platform/system/core/init/init.cpp
這個函式主要作用將各種訊號量,如SIGABRT,SIGBUS等的行為設定為SA_RESTART,一旦監聽到這些訊號即執行重啟系統

static void InstallRebootSignalHandlers() {
    // 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 signal) {
        // These signal handlers are also caught for processes forked from init, however we do not
        // want them to trigger reboot, so we directly call _exit() for children processes here.
        if (getpid() != 1) {
            _exit(signal);
        }

        // 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);
}

3.4 add_environment

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

這個函式主要作用是將一個鍵值對放到一個Char陣列中,如果陣列中有key就替換,沒有就插入

/* 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], "=");
            //如果key相同,刪除對應資料
            if ((entry_key_len == key_len) && (strncmp(ENV[n], key, entry_key_len) == 0)) {
                free((char*)ENV[n]);
                ENV[n] = NULL;
            }
        }

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

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

    return -1;
}

4、 掛載檔案系統並建立目錄

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

int main(int argc, char** argv) {
    ......
    //檢視是否有環境變數INIT_SECOND_STAGE
    bool is_first_stage = (getenv("INIT_SECOND_STAGE") == nullptr);
    if (is_first_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));
        ......
    }
    ......
}    

4.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安全策略檔案

4.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為次裝置號

5、 初始化日誌輸出、掛載分割槽裝置

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

int main(int argc, char** argv) {
    ......
        if (is_first_stage) {
            ......
            InitKernelLogging(argv);
            LOG(INFO) << "init first stage started!";
            if (!DoFirstStageMount()) {
                LOG(ERROR) << "Failed to mount required partitions early ...";
                panic();//重啟系統
            }
            ......
        }
    ......
 }

5.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、1、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
}

5.1.1 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
               << ")";
  }
}

5.1.2 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是編譯斷言,如果第一個引數為true,那麼編譯就不通過,這裡是判斷kLogSeverityToKernelLogLevel陣列個數不能大於7
  static_assert(arraysize(kLogSeverityToKernelLogLevel) == android::base::FATAL + 1,
                "Mismatch in size of kLogSeverityToKernelLogLevel and values in LogSeverity");
  //開啟 /dev/kmsg 檔案
  static int klog_fd = TEMP_FAILURE_RETRY(open("/dev/kmsg", O_WRONLY | O_CLOEXEC));
  if (klog_fd == -1) return;

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

  // 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 中
}

5.2 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.
    //如果fstab/compatible的值不是android,fstab,直接跳過掛載
    if (!is_android_dt_value_expected("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();//主要是初始化特定裝置並掛載
}

5.2.1 FirstStageMount::Create()

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

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;
    }
    // Stores device_tree_fstab_->recs[] into mount_fstab_recs_ (vector<fstab_rec*>)
    // for easier manipulation later, e.g., range-base for loop.
    for (int i = 0; i < device_tree_fstab_->num_entries; i++) {
        mount_fstab_recs_.push_back(&device_tree_fstab_->recs[i]);//將掛載資訊放入陣列中存起來
    }
}

std::unique_ptr<FirstStageMount> FirstStageMount::Create() {
    if (IsDtVbmetaCompatible()) {
        return std::make_unique<FirstStageMountVBootV2>();
    } else {
        return std::make_unique<FirstStageMountVBootV1>();
    }
}

5.2.1 handle->DoFirstStageMount

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

bool FirstStageMount::DoFirstStageMount() {
    // Nothing to mount.
    if (mount_fstab_recs_.empty()) return true;

    if (!InitDevices()) return false;

    if (!MountPartitions()) return false;

    return true;
}

6、啟用SELinux安全策略

SELinux是 Linux的一個擴張強制訪問控制安全模組。在這種訪問控制體系的限制下,程序只能訪問那些在他的任務中所需要檔案

int main(int argc, char** argv) {
    ......
        if (is_first_stage) {
            ......
                   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.
        if (selinux_android_restorecon("/init", 0) == -1) {//restorecon命令用來恢復SELinux檔案屬性即恢復檔案的安全上下文
            PLOG(ERROR) << "restorecon failed";
            security_failure();//失敗則重啟系統
        }
            ......
        }
    ......
 }

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();
            }
        }

        std::string err;
        if (!WriteFile("/sys/fs/selinux/checkreqprot", "0", &err)) {
            LOG(ERROR) << err;
            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().count()).c_str(), 1);
    } else {
        selinux_init_all_handles();//第二階段時初始化處理函式
    }
}

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

這裡主要就是設定一些變數如INIT_SECOND_STAGE,INIT_STARTED_AT,為第二階段做準備,然後再次呼叫init的main函式,啟動使用者態的init程序

int main(int argc, char** argv) {
    ......
        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", std::to_string(start_ms).c_str(), 1);

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

    可能大家不明白呼叫execv方法就重新執行main方法,下面給出該方法原型解釋。

函式原型
int execv(const char *progname, char *const argv[]);   //#include <unistd.h>
功能介紹
         execv會停止執行當前的程序,並且以progname應用程序替換被停止執行的程序,程序ID沒有改變。
引數:
    progname: 被執行的應用程式。
    argv: 傳遞給應用程式的引數列表, 注意,這個陣列的第一個引數應該是應用程式名字本身,並且最後一個引數應該為NULL,不參將多個引數合併為一個引數放入陣列。
返回值:
    如果應用程式正常執行完畢,那麼execv是永遠不會返回的;當execv在調用出錯了,此時它的返回值應該是-1,具體的錯誤程式碼可以通過全域性變數errno檢視,還可以通過stderr得到具體的錯誤描述字元。

8、小結

    init程序第一階段做的主要工作是掛載分割槽,建立裝置節點和一些關鍵目錄,初始化日誌輸出系統,啟用SELinux安全策略
    下一篇我將講解init程序第二階段,未完待續。。。