天天看點

Android 9(P)之init程序啟動源碼分析指南之一     Android 9 之init程序啟動源碼分析指南之一

     Android 9 之init程序啟動源碼分析指南之一

Android 9 (P) 系統啟動及程序建立源碼分析目錄:

Android 9 (P)之init程序啟動源碼分析指南之一

Android 9 (P)之init程序啟動源碼分析指南之二

Android 9 (P)之init程序啟動源碼分析指南之三

Android 9 (P)核心服務和關鍵程序啟動

Android 9 (P)Zygote程序啟動源碼分析指南一

Android 9 (P)Zygote程序啟動源碼分析指南二

Android 9 (P)系統啟動之SystemServer大揭秘上

Android 9 (P)系統啟動之SystemServer大揭秘下

Android 9 (P)應用程序建立流程大揭秘

引言

  此時的我吃着火鍋唱着歌,進行着Android P(此P非彼P,Android 9)的适配工作。我真的隻能說每次Android版本的疊代更新,都是對我們的一次煉獄般的摧殘啊,各種适配啊,我真的想說fuck the coding。但是吐槽歸吐槽,為了我熱愛的coding事業,讓我們愉快的适配起來。本篇将從源碼角度來分析分析Android P的init程序啟動流程,這個和其它Android版本還是有蠻大差別的。

注意:本文示範的代碼是Android P高通msm8953平台源碼。涉及的源碼如下:

system/core/init/init.cpp
system/core/init/init_first_stage.cpp
system/core/fs_mgr/fs_mgr.cpp
system/core/fs_mgr/fs_mgr_fstab.cpp
system/core/init/builtins.cpp
bionic/libc/include/sys/mount.h
bionic/libc/include/sys/stat.h
system/core/init/log.cpp
system/core//base/include/android-base/logging.h
system/core/base/logging.cpp
system\core\init\selinux.cpp
           

一.開篇

  雖然本章的重點是init程序啟動流程分析,但是在這之前還是讓我們大體上來聊聊Android的整個開機過程,這樣也有助于承上啟下的作用。

開機作為Android終端工作的第一步工作,包含了很多任務。從粗粒度的劃分來看,啟動包含下面這些關鍵流程:這其中,OS 層級的,是所有 Linux 相關作業系統通用的,這都不是本篇讨論的重點不做細說。看一下大概流程圖就OK了。

Android 9(P)之init程式啟動源碼分析指南之一     Android 9 之init程式啟動源碼分析指南之一

二.Init啟動第一階段

注意:這一階段在kernel domain,可以認為此階段在核心态,至于為什麼會在後續給出答案。

  Init程序作為Android的第一個user space(使用者空間)的程序,它是所有 Android 系統 native service 的祖先,它的程序号是 1。

msm8953_64:/ # ps -A  | grep init
root             1     0   32248   5336 SyS_epoll_wait      0 S init
root           389     1    7752   2452 poll_schedule_timeout 0 S init
root           390     1    6600   1908 poll_schedule_timeout 0 S init
           

  init程序是Android系統第一個使用者程序,主要工作分為兩部分。首先會完成核心的建立和初始化這部分内容跟Linux核心相關, 其次就是使用者空間的建立和啟動這部分内容跟Android系統的啟動相關

  • init程序第一階段做的主要工作是挂載分區,建立裝置節點和一些關鍵目錄,初始化日志輸出系統,啟用SELinux安全政策
  • init程序第二階段主要工作是初始化屬性系統,解析SELinux的比對規則,處理子程序終止信号,啟動系統屬性服務,可以說每一項都很關鍵,如果說第一階段是為屬性系統,SELinux做準備,那麼第二階段就是真正去把這些落實的。
  • init.rc檔案解析

  由于内容比較多,是以對于init的講解,我分為三個章節來講,本文隻講解第一階段,第一階段主要有以下内容:

  • ueventd/watchdogd跳轉以及其它初始化
  • 挂載檔案系統并建立目錄
  • 初始化日志輸出、挂載分區裝置
  • 啟用SELinux安全政策
  • 開始第二階段前的準備

2.1 ueventd/watchdogd跳轉以及其它初始化

int main(int argc, char** argv) {
    if (!strcmp(basename(argv[0]), "ueventd")) {//ueventd主要是負責裝置節點的建立、權限設定等一些列工作
        return ueventd_main(argc, argv);
    }   

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

    if (argc > 1 && !strcmp(argv[1], "subcontext")) {
        InitKernelLogging(argv);
        const BuiltinFunctionMap function_map;
        return SubcontextMain(argc, argv, &function_map);
    }   

    if (REBOOT_BOOTLOADER_ON_PANIC) {
        InstallRebootSignalHandlers();//初始化重新開機系統的處理信号,内部通過sigaction 注冊信号,當監聽到該信号時重新開機系統
    }
	......
           

  從上面的代碼可以看到main函數開頭會根據啟動參數的差别,轉變成啟動的程序(ueventd 和 watchdogd),當然在kernel中啟動的肯定是init程序了。這裡就先不看何時會啟動ueventd/watchdogd了,隻需要要知道如下兩點:

  • ueventd程序用來管理裝置,如果有新裝置插入,就會在/dev建立對應的裝置檔案;-
  • watchdogd程序是看門狗程式,每隔一段時間通過系統調用write向核心看門狗裝置發一個資訊,以確定系統正常運作。

  檢視一下運作環境,如下:

msm8953_64:/ # ls -l  sbin/
total 1600
-rwxr-x--- 1 root shell 1684856 2020-04-30 12:03 charger
lrwxr-x--- 1 root shell       7 2020-05-08 18:26 ueventd -> ../init
lrwxr-x--- 1 root shell       7 2020-05-08 18:26 watchdogd -> ../init
msm8953_64:/ #
           

  ueventd和watchdogd都指向了init程式。init程式運作時,實際上同時運作了三個程式,之是以把ueventd和watchdogd作為init程序的軟連結,是因為這個三個程序共享了共同資源,放在同一份代碼中即可,不用額外再寫出分别針對ueventd和watchdogd的程式,這樣造成了代碼的備援,也不便于維護。但是,放在同一份代碼中如何差別目前程序是哪一個?這就是作者在main函數開頭用了兩個if語句的原因,通過程序名字判斷到底是哪個程序。

  接着繼續分析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);
        }   

        // Calling DoReboot() or LOG(FATAL) is not a good option as this is a signal handler.
        // RebootSystem uses syscall() which isn't actually async-signal-safe, but our only option
        // and probably good enough given this is already an error case and only enabled for
        // development builds.
        RebootSystem(ANDROID_RB_RESTART2, "bootloader");//進入bootloader
    };  
    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);
}
           

2.2 建立并挂載相關的檔案系統

int main(int argc, char** argv) {
	......
	/**
		前面我們說過init程序主要分為兩個階段,而這兩個階段是由is_first_stage控制,first_stage就是第一階段要做的事
	***/
	bool is_first_stage = (getenv("INIT_SECOND_STAGE") == nullptr);

    if (is_first_stage) {//隻執行一次,因為在方法體中有設定INIT_SECOND_STAGE
        boot_clock::time_point start_time = boot_clock::now();// 用于記錄啟動時間

        // Clear the umask.
        //清除檔案權限,證建立的目錄的通路權限不受屏蔽字影響
        umask(0);

        clearenv();
        setenv("PATH", _PATH_DEFPATH, 1);//設定環境變量
        // 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");//挂載tmpfs檔案系統
        mkdir("/dev/pts", 0755);
        mkdir("/dev/socket", 0755);
        mount("devpts", "/dev/pts", "devpts", 0, NULL);//挂載devpts檔案系統
        #define MAKE_STR(x) __STRING(x)
        mount("proc", "/proc", "proc", 0, "hidepid=2,gid=" MAKE_STR(AID_READPROC));//挂載proc檔案系統
        // Don't expose the raw commandline to unprivileged processes.
        chmod("/proc/cmdline", 0440);// 8.0新增, 收緊了cmdline目錄的權限
        gid_t groups[] = { AID_READPROC };
        setgroups(arraysize(groups), groups);// 8.0新增,增加使用者組
        mount("sysfs", "/sys", "sysfs", 0, NULL);//挂載sysfs檔案系統
        mount("selinuxfs", "/sys/fs/selinux", "selinuxfs", 0, NULL);//8.0之後新增,挂載selinuxfs檔案系統

        mknod("/dev/kmsg", S_IFCHR | 0600, makedev(1, 11));//建立kmsg裝置節點,使用者kernel log輸出
        if constexpr (WORLD_WRITABLE_KMSG) {
            mknod("/dev/kmsg_debug", S_IFCHR | 0622, makedev(1, 11));
        }

        mknod("/dev/random", S_IFCHR | 0666, makedev(1, 8));
        mknod("/dev/urandom", S_IFCHR | 0666, makedev(1, 9));

        // Mount staging areas for devices managed by vold
        // See storage config details at http://source.android.com/devices/storage/
        mount("tmpfs", "/mnt", "tmpfs", MS_NOEXEC | MS_NOSUID | MS_NODEV,
              "mode=0755,uid=0,gid=1000");
        // /mnt/vendor is used to mount vendor-specific partitions that can not be
        // part of the vendor partition, e.g. because they are mounted read-write.
        mkdir("/mnt/vendor", 0755);
	}
	......
}
           

  通過對上述代碼分析我們可知,該部分主要用于建立和挂載啟動所需的檔案目錄。需要注意的是,在編譯Android系統源碼時,在生成的根檔案系統中, 并不存在這些目錄,它們是系統運作時的目錄,即當系統終止時,就會消失。

  上面的代碼雖然分析完了,但是有幾個重點知識點需要着重強調一下:

2.2.1 mount函數

  這是一個标準的linux系統調用函數,其頭檔案定義在如下路徑bionic/libc/include/sys/mount.h

#include <sys/mount.h>

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:檔案系統特有的參數
  • 傳回值:成功執行時,傳回0。失敗傳回-1或者其它值。

2.2.2 mknod

  這是一個标準的linux系統調用函數,其頭檔案定義在如下路徑bionic/libc/include/sys/stat.h,其如下:

mknod用于建立Linux中的裝置檔案,其參數定義如下:

  • 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建立,下面我們從終端中截取一個來看看,可以看出kmsg的主從号是1和11.
msm8953_64:/dev # ls  -la /dev/kmsg
crw--w---- 1 root system 1,  11 1970-01-01 08:14 /dev/kmsg
           

2.2.3 檔案系統分類介紹

  在前面的代碼分析中,我們了解到了在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:一個非常重要的虛拟檔案系統,它可以看做是核心内部資料結構的接口,通過它我們可以獲得系統的資訊,同時也能夠在運作時修改特定的核心參數。
  • sysfs:與proc檔案系統類似,也是一個不占用任何磁盤空間的虛拟檔案系統。它通常被挂接在/sys目錄下。sysfs檔案系統是Linux2.6核心引入的,它把連接配接在系統上的裝置和總線組織成為一個分級的檔案,使得它們可以在使用者空間存取。
  • selinuxfs也是虛拟檔案系統,通常挂載在/sys/fs/selinux目錄下,用來存放SELinux安全政策檔案。

2.3 初始化核心Log系統

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!";
        ...
    }
           

  熟悉init子產品的童靴應該知道在Android系統中,init的log會出現在kernel log中。如下所示:

14,1169,11337125,-;init: init first stage started!
14,1169,11337125,-;init: init first stage started!
14,1170,11338428,-;init: Using Android DT directory /proc/device-tree/firmware/android/
14,1172,11355221,-;init: [libfs_mgr]fs_mgr_read_fstab_default(): failed to find device default fstab
14,1238,11879261,-;init: [libfs_mgr]Returning avb_handle with status: 2
14,1239,11879338,-;init: [libfs_mgr]AVB HASHTREE disabled on: /vendor
14,1240,11884392,-;init: [libfs_mgr]fs_mgr_do_mount_one : mounting /vendor
14,1245,11906793,-;init: [libfs_mgr]__mount(source=/dev/block/platform/soc/7824900.sdhci/by-name/vendor,target=/vendor,type=ext4)=0: Success
14,1246,11915831,-;init: Skipped setting INIT_AVB_VERSION (not in recovery mode)
14,1246,11915831,-;init: Skipped setting INIT_AVB_VERSION (not in recovery mode)
14,1247,11927642,-;init: Loading SELinux policy
14,1248,11941949,-;init: Compiling SELinux policy
7,1281,14432231,-;SELinux:  Completing initialization.
           

理論上,init是屬于 user space 的,為何 log出現在 kernel log 系統中?順帶的還有其他幾個問題:

  • kernel log與init log都有log等級,兩者有對應關系嗎?
  • kernel log可以調整loglevel來控制log輸出,init可以嗎?

下面讓我們帶着這些問題,來分析該段代碼。

2.3.1 InitKernelLogging 重定向标準的輸入輸出

  跟蹤InitKernelLogging的源碼路徑是 system/core/init/log.cpp,該函數首先是将标準輸入輸出重定向到"/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) { //若開啟失敗則記錄log
        int saved_errno = errno; 
        android::base::InitLogging(argv, &android::base::KernelLogger, InitAborter);
        errno = saved_errno; 
        PLOG(FATAL) << "Couldn't open /sys/fs/selinux/null";
    }
     /*
     * dup2(int old_fd, int new_fd) dup2函數的作用是用來複制一個檔案的描述符, 
     * 通常用來重定向程序的stdin、stdout和stderr。
     * 下面的函數将0、1、2綁定到null裝置上,通過标準的輸入輸出無法輸出資訊
     */
    dup2(fd, 0);    
    dup2(fd, 1);    
    dup2(fd, 2);    
    if (fd > 2) close(fd);
  
    android::base::InitLogging(argv, &android::base::KernelLogger, InitAborter);//初始化log
}
           

2.3.2 InitLogging 設定日志輸出等級

  跟蹤InitLogging的源碼路徑是system/core/base/logging.cpp,該函數的主要功能是設定logger和aboter的處理函數,然後設定日志系統輸出等級。

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

2.3.3 KernelLogger

  該函數定義在system/core/base/logging.cpp在InitKernelLogging方法中有句調用,如下:

這句話的作用就是将KernelLogger函數作為log日志的處理函數,KernelLogger主要作用就是将要輸出的日志格式化之後寫入到 /dev/kmsg 裝置中。

#if defined(__linux__)
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");

  static int klog_fd = TEMP_FAILURE_RETRY(open("/dev/kmsg", O_WRONLY | O_CLOEXEC));
  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 中
}
#endif
           

2.3.4 Init log輸出流程

  在前面的分析中,我們講解了init log系統的初始化,光說不練那是花架子,要真的了解init log,最嗨的方法莫過于分析log輸出流程,我們以init中的下面這個輸出為例子說明:

其在核心中的列印如下:

讓我們對該流程分析一下,其中函數傳遞的參數是log level。

Android 9(P)之init程式啟動源碼分析指南之一     Android 9 之init程式啟動源碼分析指南之一

  最後一步調用 logger 的中做了說明:LogLine 調用 logger 來實作最後一步,而 logger 在初

始化階段已經被指派為 KernelLogger。來看下這個函數的一部分:

void KernelLogger(android::base::LogId, android::base::LogSeverity severity,
                  const char* tag, const char*, unsigned int, const char* msg) {
  ......
  static int klog_fd = TEMP_FAILURE_RETRY(open("/dev/kmsg", O_WRONLY | O_CLOEXEC));//打開kmsg節點
  ......
  TEMP_FAILURE_RETRY(writev(klog_fd, iov, 1));//将日志寫入到 /dev/kmsg 中
}
           

看到這裡,前面提到問題就可以解答了:通過将 init log 寫入到 kmsg,實作了 init log 從 kernel log 輸出。

2.3.4 Init log等級

  在前面的流程圖中提到過WOULD_LOG 會 判 斷 loglevel 是 否 小 于

gMinimumLogServerity,以此決定是否輸出 log。gMinimumLogServerity 就是 init 預設的

loglevel,它的設定很簡單,隻需修改它的指派即可,代碼定義在system/core/base/logging.cpp中如下所示:

至于 gMinimumLogServerity 可以被設定的值,依然可以從 KernelLogger 找到答案,代碼如下:

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

從後面的注釋來看,這些級别跟 kernel 中 log level 是一一對應的。init 的 loglevel 最小為 2,這也是為何 kernel loglevel 設定為 1 的時候,init 的 log 就不會再輸出了。通過 init loglevel 與kernel log 對應關系的介紹以及 init loglevel 的設定,可以得出一個結論:如果想要確定添加在 init 中的 log 輸出到 kernel log 中,需要保證兩條:

  • kernel loglevel >= gMinimumLogServerity

    – LOG(loglevel) <= gMinimumLogServerity

到這裡 2.3 章節開頭提出的兩個問題就有答案了。了解 init log 系統,有利于手機開發過程中debug,某些時候可能預設的 loglevel 太低, log 出不來,這個時候就可以根據上面提到的方法,來修改 kernel loglevel 和 gMinimumLogServerity,進而擷取更多的 log 資訊。在 BringUP 階段和項目初始階段,建議調整 log 等級調為DEBUG,即 gMinimumLogServerity= DEBUG。

2.4 檔案系統挂載

  android 有很多分區,如"system",“userdata”,“cache”,AndroidO 上還新增了 vendor/odm等新的分區,它們是何時挂載的?如何挂載的?接下去進行分析。

  在 Android8.0 以前,挂載是通過觸發 do_mount_all 來做的。從 Andriod8.0 開始,以前由do_mount_all 來做的事情現在分成了兩部分,新增了 FirstStageMount,将 system/vendor/odm分區挂載放在 DoFirstStageMount階段來做;而其它分區的挂載,仍然在 do_mount_all 階段。

2.4.1 DoFirstStageMount

  在init的main函數中,通過 INIT_SECOND_STAGE 來區分第一/第二階段,FirstStageMount就在第一階段被調用。精簡流程如下:

int main(int argc, char** argv) {
	......
	bool is_first_stage = (getenv("INIT_SECOND_STAGE") == nullptr);
	if (is_first_stage) {//init第一階段
		......
		if (!DoFirstStageMount()) {//DoFirstStageMount入口
            LOG(FATAL) << "Failed to mount required partitions early ...";
        }
		......
	}
	......
}
           

  為何從Android O開始要這麼早就開始做檔案系統挂載呢?Android的媽咪谷歌給出了官方解釋:

All Treble-enabled devices must enable first stage mount to make sure init can load SELinux policy fragments that are spread across system and vendor partitions (this also enables loading of kernel modules as soon as possible after kernel boot).

  用咋中國人的話來說就是在打開了 Treble 的裝置上,為了確定 init 能及時導入 SELinux 的配置檔案,需要盡快的将 system/vendor 等分區挂載上。配置檔案在分區中的目錄主要如下所示:

msm8953_64:/ # ls /system/etc/selinux
mapping                              plat_property_contexts
plat_and_mapping_sepolicy.cil.sha256 plat_seapp_contexts
plat_file_contexts                   plat_sepolicy.cil
plat_hwservice_contexts              plat_service_contexts
plat_mac_permissions.xml             selinux_denial_metadata
msm8953_64:/ # ls /vendor/etc/selinux
plat_pub_versioned.cil                       vendor_mac_permissions.xml
plat_sepolicy_vers.txt                       vendor_property_contexts
precompiled_sepolicy                         vendor_seapp_contexts
precompiled_sepolicy.plat_and_mapping.sha256 vendor_sepolicy.cil
vendor_file_contexts                         vndservice_contexts
vendor_hwservice_contexts
msm8953_64:/ #
           

  如果有過Android N開發經驗就比較好了解了,在Android N上面SELinux的配置檔案存放在 boot.img 中,在核心初始化過程中,boot.img中的檔案已經挂載到rootfs了,相應的,配置檔案也就可以從rootfs讀取了。而 AndroidO 開始,SELinux配置檔案放到了 vendor/system 分區,如果仍然按照do_mount_all 階段來挂載這兩個分區,SELinux來不及做初始化。

  檔案系統挂載是需要挂載資訊的,這個資訊通常儲存在fstab結構體中,在do_mount_all階段,挂載資訊會從fstab開頭的檔案中擷取,但是在這一階段,fstab資訊是boot.img的dt(devices tree)中拿到的,具體過程如下:

Android 9(P)之init程式啟動源碼分析指南之一     Android 9 之init程式啟動源碼分析指南之一

  從上圖可以得知,FirstMountStage 通過擷取 dt 中的 fstab,完成 system/vendor 分區的挂載,而 fstab 資訊如何寫入 dt 檔案,可以參考 Google 的介紹:

https://source.android.com/devices/architecture/kernel/modular-kernels

2.4.2 do_mount_all

  通過前面的分析我們可知在檔案系統挂載的第一階段,system/vendor分區已經被成功挂載,而其它分區的挂載則通過do_mount_all 來實作。下面讓我們接着分析該流程:

  對init.rc有一定了解的童靴都應該知道:init 程序會根據 init.rc 的規則啟動程序或者服務。init.rc通 過 “import /init.xxx.rc” 語句導入平台 的 規 則 。 在目前msm8953_64上device/qcom/msm8953_64/init.target.rc中就有如下規則:

on fs
    wait /dev/block/platform/soc/${ro.boot.bootdevice}
    symlink /dev/block/platform/soc/${ro.boot.bootdevice} /dev/block/bootdevice

    mount_all /vendor/etc/fstab.qcom
           

  mount_all是一條指令,/vendor/etc/fstab.qcom是傳入的參數,讓我們看看fstab.qcom的内容,如下所示:

/dev/block/bootdevice/by-name/system        /            ext4    ro,barrier=1,discard                        wait,avb
/dev/block/bootdevice/by-name/userdata      /data        ext4    noatime,nosuid,nodev,barrier=1,noauto_da_alloc,discard  wait,                  forceencrypt=footer,quota,reservedsize=128M
/devices/platform/soc/7864900.sdhci/mmc_host*        /storage/sdcard1 vfat  nosuid,nodev         wait,voldmanaged=sdcard1:auto,noemulatedsd,    encryptable=footer
/devices/platform/soc/7000000.ssusb/7000000.dwc3/xhci-hcd.0.auto*  /storage/usbotg  vfat  nosuid,nodev  wait,voldmanaged=usbotg:auto
/devices/soc/7864900.sdhci/mmc_host*        /storage/sdcard1 vfat  nosuid,nodev         wait,voldmanaged=sdcard1:auto,noemulatedsd,             encryptable=footer
/devices/soc/7000000.ssusb/7000000.dwc3/xhci-hcd.0.auto*  /storage/usbotg  vfat  nosuid,nodev  wait,voldmanaged=usbotg:auto
/dev/block/bootdevice/by-name/config        /frp         emmc    defaults                                    defaults
/dev/block/bootdevice/by-name/misc          /misc        emmc    defaults                                    defaults
/dev/block/bootdevice/by-name/cache         /cache       ext4    noatime,nosuid,nodev,barrier=1              wait
/dev/block/bootdevice/by-name/modem         /vendor/firmware_mnt    vfat    ro,shortname=lower,uid=0,gid=1000,dmask=227,fmask=337,context=u:    object_r:firmware_file:s0 wait
/dev/block/bootdevice/by-name/dsp           /vendor/dsp         ext4    ro,nosuid,nodev,barrier=1                   wait
/dev/block/bootdevice/by-name/persist       /mnt/vendor/persist ext4   noatime,nosuid,nodev,barrier=1               wait
           

 &emsp在init程序中會通過 ActionManager 來解析“mount_all 指令“,找到指令所對應的解析函數。這個指令解析函數的對應關系,定義在system/core/init/builtins.cpp如下所示:

const BuiltinFunctionMap::Map& BuiltinFunctionMap::map() const {
    constexpr std::size_t kMax = std::numeric_limits<std::size_t>::max();
    // clang-format off
    static const Map builtin_functions = {
        {"bootchart",               {1,     1,    {false,  do_bootchart}}},
        {"chmod",                   {2,     2,    {true,   do_chmod}}},
        {"chown",                   {2,     3,    {true,   do_chown}}},
        {"class_reset",             {1,     1,    {false,  do_class_reset}}},
        {"class_restart",           {1,     1,    {false,  do_class_restart}}},
        {"class_start",             {1,     1,    {false,  do_class_start}}},
        {"class_stop",              {1,     1,    {false,  do_class_stop}}},
        {"copy",                    {2,     2,    {true,   do_copy}}},
        {"domainname",              {1,     1,    {true,   do_domainname}}},
        {"enable",                  {1,     1,    {false,  do_enable}}},
        {"exec",                    {1,     kMax, {false,  do_exec}}},
        {"exec_background",         {1,     kMax, {false,  do_exec_background}}},
        {"exec_start",              {1,     1,    {false,  do_exec_start}}},
        {"export",                  {2,     2,    {false,  do_export}}},
        {"hostname",                {1,     1,    {true,   do_hostname}}},
        {"ifup",                    {1,     1,    {true,   do_ifup}}},
        {"init_user0",              {0,     0,    {false,  do_init_user0}}},
        {"insmod",                  {1,     kMax, {true,   do_insmod}}},
        {"installkey",              {1,     1,    {false,  do_installkey}}},
        {"load_persist_props",      {0,     0,    {false,  do_load_persist_props}}},
        {"load_system_props",       {0,     0,    {false,  do_load_system_props}}},
        {"loglevel",                {1,     1,    {false,  do_loglevel}}},
        {"mkdir",                   {1,     4,    {true,   do_mkdir}}},
        // TODO: Do mount operations in vendor_init.
        // mount_all is currently too complex to run in vendor_init as it queues action triggers,
        // imports rc scripts, etc.  It should be simplified and run in vendor_init context.
        // mount and umount are run in the same context as mount_all for symmetry.
        {"mount_all",               {1,     kMax, {false,  do_mount_all}}},
        {"mount",                   {3,     kMax, {false,  do_mount}}},
        {"umount",                  {1,     1,    {false,  do_umount}}},
        {"readahead",               {1,     2,    {true,   do_readahead}}},
        {"restart",                 {1,     1,    {false,  do_restart}}},
        {"restorecon",              {1,     kMax, {true,   do_restorecon}}},
        {"restorecon_recursive",    {1,     kMax, {true,   do_restorecon_recursive}}},
        {"rm",                      {1,     1,    {true,   do_rm}}},
        {"rmdir",                   {1,     1,    {true,   do_rmdir}}},
        {"setprop",                 {2,     2,    {true,   do_setprop}}},
        {"setrlimit",               {3,     3,    {false,  do_setrlimit}}},
        {"start",                   {1,     1,    {false,  do_start}}},
        {"stop",                    {1,     1,    {false,  do_stop}}},
        {"swapon_all",              {1,     1,    {false,  do_swapon_all}}},
        {"symlink",                 {2,     2,    {true,   do_symlink}}},
        {"sysclktz",                {1,     1,    {false,  do_sysclktz}}},
        {"trigger",                 {1,     1,    {false,  do_trigger}}},
        {"verity_load_state",       {0,     0,    {false,  do_verity_load_state}}},
        {"verity_update_state",     {0,     0,    {false,  do_verity_update_state}}},
        {"wait",                    {1,     2,    {true,   do_wait}}},
        {"wait_for_prop",           {2,     2,    {false,  do_wait_for_prop}}},
        {"write",                   {2,     2,    {true,   do_write}}},
    };
    // clang-format on
    return builtin_functions;
}
           

 &emsp從上面可以看出,mount_all 指令對應的是 do_mount_all 函數,/vendor/etc/fstab.qcom是do_mount_all 函數的傳入參數。do_mount_all 的解析流程如下:

Android 9(P)之init程式啟動源碼分析指南之一     Android 9 之init程式啟動源碼分析指南之一

2.5 Selinux Init初始化

int main(int argc, char** argv) {
    /* ------------ 第一階段 ------------ BEGIN------------ */
    /* 01. 建立檔案系統目錄并挂載相關的檔案系統 */
    /* 02. 重定向輸入輸出/核心Log系統 */
    /* 03. 挂載一些分區裝置 */
    if (is_first_stage) {                                                                
        ... ...

        // 此處應該是初始化安全架構:Android Verified Boot
        // AVB主要用于防止系統檔案本身被篡改,還包含了防止系統復原的功能,
        // 以免有人試圖復原系統并利用以前的漏洞
        SetInitAvbVersionInRecovery();

        // Set up SELinux, loading the SELinux policy.
        SelinuxSetupKernelLogging();//參照InitKernelLogging,将加載SELinux的日志在核心裡面列印出來
        SelinuxInitialize();//初始化SELinux資訊

        ... ...
    }
    ... ...
           

2.5.1 SelinuxSetupKernelLogging

  代碼邏輯如下,位置在system\core\init\selinux.cpp中

// This function sets up SELinux logging to be written to kmsg, to match init's logging.
void SelinuxSetupKernelLogging() {
    selinux_callback cb; 
    cb.func_log = selinux_klog_callback;
    selinux_set_callback(SELINUX_CB_LOG, cb);//設定selinux的日志輸出處理函數
}
           

  selinux_set_callback定義在external/selinux/libselinux/src/callbacks.c主要就是根據不同的type設定回調函數,selinux_log,selinux_audit這些都是函數指針:

/* callback getting function */
union selinux_callback
selinux_get_callback(int type)
{
    union selinux_callback cb; 

    switch (type) {
    case SELINUX_CB_LOG:
        cb.func_log = selinux_log;
        break;
    case SELINUX_CB_AUDIT:
        cb.func_audit = selinux_audit;
        break;
    case SELINUX_CB_VALIDATE:
        cb.func_validate = selinux_validate;
        break;
    case SELINUX_CB_SETENFORCE:
        cb.func_setenforce = selinux_netlink_setenforce;
        break;
    case SELINUX_CB_POLICYLOAD:
        cb.func_policyload = selinux_netlink_policyload;
        break;
    default:
        memset(&cb, 0, sizeof(cb));
        errno = EINVAL;
        break;
    }
    return cb;
}
           

2.5.2 SelinuxInitialize

  Selinux Init初始化階段在核心中的列印資訊如下,關于SELinux的更多詳細知識可以參見如下系列文章Android SELinux開發入門指南,這裡我們隻簡單介紹一下SELinux是「Security-Enhanced Linux」的簡稱,是美國國家安全局「NSA=The National Security Agency」和SCC(Secure Computing Corporation)開發的 Linux的一個擴張強制通路控制安全子產品。在這種通路控制體系的限制下,程序隻能通路那些在他的任務中所需要檔案。

14,1246,12102659,-;init: Skipped setting INIT_AVB_VERSION (not in recovery mode)
14,1246,12102659,-;init: Skipped setting INIT_AVB_VERSION (not in recovery mode)
14,1247,12111637,-;init: Loading SELinux policy
7,1248,12181871,-;SELinux: 16384 avtab hash slots, 42349 rules.
7,1249,12255397,-;SELinux: 16384 avtab hash slots, 42349 rules.
7,1250,12255477,-;SELinux:  1 users, 4 roles, 2229 types, 0 bools, 1 sens, 1024 cats
7,1251,12255506,-;SELinux:  93 classes, 42349 rules
7,1252,12266038,-;SELinux:  Completing initialization.
           

  SELinux是從Android 4.4導入的(這個東西嗎,我認為雖然使用了安全性提高了,但是也是殺敵一千自損一百,非常影響開發效率),Android5.0 開始全面啟用的安全相關子產品,代碼邏輯如下,位置在system\core\init\selinux.cpp中。

void SelinuxInitialize() {
    Timer t;

    LOG(INFO) << "Loading SELinux policy";
    if (!LoadPolicy()) {//加載政策檔案
        LOG(FATAL) << "Unable to load SELinux policy";
    }   

    bool kernel_enforcing = (security_getenforce() == 1); //核心中讀取資訊
    bool is_enforcing = IsEnforcing(); // 指令行中得到的資料
    if (kernel_enforcing != is_enforcing) {//擷取 selinux 模式
         // 用于設定selinux的工作模式。selinux有兩種工作模式:
        // 1、”permissive”,所有的操作都被允許(即沒有MAC),但是如果違反權限的話,會記錄日志
        // 2、”enforcing”,所有操作都會進行權限檢查。在一般的終端中,應該工作于enforing模式
        if (security_setenforce(is_enforcing)) {
            PLOG(FATAL) << "security_setenforce(%s) failed" << (is_enforcing ? "true" : "false");
        }   
    }   

    if (auto result = WriteFile("/sys/fs/selinux/checkreqprot", "0"); !result) {
        LOG(FATAL) << "Unable to write to /sys/fs/selinux/checkreqprot: " << result.error();
    }   

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

2.5.3 LoadPolicy

  該代碼定義在system\core\init\selinux.cpp中,邏輯如下:

constexpr const char plat_policy_cil_file[] = "/system/etc/selinux/plat_sepolicy.cil";

bool IsSplitPolicyDevice() {
    return access(plat_policy_cil_file, R_OK) != -1;
}  
bool LoadPolicy() {
    return IsSplitPolicyDevice() ? LoadSplitPolicy() : LoadMonolithicPolicy();
} 
           

這裡最後調用的是LoadSplitPolicy加載政策檔案,這裡就不對過多的代碼分析了,因為量太大了,本人也沒有完全掌握,能力有限,我們隻從整體把握流程,細節就不扣了。

2.5.3 security_setenforce

  調用selinux_is_enforcing設定 Selinux 模式。首先檢測 kernelcmdline 是否設定了androidboot.selinux = permissive;當 cmdline 設定了 permissive 時會設定 Selinux 模式為 permissive;否則設定為 enforing 模式。是以在調試開機流程時可以通過修改cmdline 或者直接修改上面的函數修改 Selinux 模式。在手機開機的情況下還可以通過 setenforce的方式改變 Selinux 模式,但這種方式重新開機後就不再起效。Selinux 模式有兩種:

  • enforcing:強制模式,SELinux 運作中,且已經正确的開始限制 domain/type
  • permissive:寬容模式,SELinux 運作中,不過僅會有警告訊息并不會實際限制domain/type 的存取

同時SELinux是一個很複雜的系統,而 AndroidO 通過 Treble 架構,将 SELinux做了 split,使得它的規則更加繁瑣。在這裡就不再做更多的介紹了,隻需了解 SELinux是在哪個階段啟動、如何修改SELinux的模式和增添對應規則即可。

該段源碼定義在external/selinux/libselinux/src/setenforce.c中,邏輯如下:

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 | O_CLOEXEC);
    if (fd < 0)
        return -1; 

    snprintf(buf, sizeof buf, "%d", value);
    ret = write(fd, buf, strlen(buf));
    close(fd);
    if (ret < 0)
        return -1; 

    return 0;
}
           

security_getenforce是去操作/sys/fs/selinux/enforce 檔案, 0表示permissive 1表示enforcing。

2.6 第一階段收尾和第二階段準備工作

int main(int argc, char** argv) {
    /* ------------ 第一階段 ------------ BEGIN------------ */
    /* 01. 建立檔案系統目錄并挂載相關的檔案系統 */
    /* 02. 重定向輸入輸出/核心Log系統 */
    /* 03. 挂在一些分區裝置 */
    /* 04. 完成SELinux相關工作 */
    if (is_first_stage) {                                                                
        ... ...
        // We're in the kernel domain, so re-exec init to transition to the init domain now
        // that the SELinux policy has been loaded.
        /*
        *我們執行第一遍時是在kernel domain,是以要重新執行init檔案,切換到init domain,
         * 這樣SELinux policy才已經加載進來了,這就是我在前面說為什麼第一階段在核心态
        */
        if (selinux_android_restorecon("/init", 0) == -1) {
            PLOG(FATAL) << "restorecon failed of /init failed";
        }

        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);// 再次調用init的main函數,啟動使用者态的init程序

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

  這裡主要就是設定一些變量INIT_SECOND_STAGE,INIT_STARTED_AT,為第二階段做準備,然後再次調用init的main函數,啟動使用者态的init程序。好了init第一階段分析完了。

總結

  随着Android版本越高,init的工作量也是越來越大了,分析起來不得不使出吃奶的力氣了,在init程序的第一階段主要工作是挂載分區,建立裝置節點和一些關鍵目錄,初始化日志輸出系統,啟用SELinux安全政策并為第二階段工作做準備。

寫在最後

  Android P之init程序啟動源碼分析指南之一的告一段落了,不容易啊分析起來,在接下來的篇章我們将繼續講解init啟動的第二階段相關工作。如果對給位有幫助歡迎點贊一個,如果寫得有問題也歡迎多多指正。未完待續,下個篇章Android P之init程序啟動源碼分析指南之二見!

參閱部落格:

https://www.cnblogs.com/pepsimaxin/articles/9442948.html

https://www.jianshu.com/p/befff3d70309