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了。


二.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);
#endifsigaction(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_STAGEboot_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,其如下:

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

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) { //若开启失败则记录logint 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方法中有句调用,如下:

 android::base::InitLogging(argv, &android::base::KernelLogger, InitAborter);//初始化log

这句话的作用就是将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 offstatic 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数组个数不能大于7static_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(INFO) << "init first stage started!";

其在内核中的打印如下:

14,1169,11337125,-;init: init first stage started!

让我们对该流程分析一下,其中函数传递的参数是log level。


  最后一步调用 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中如下所示:

static LogSeverity gMinimumLogSeverity = INFO;

至于 gMinimumLogServerity 可以被设定的值,依然可以从 KernelLogger 找到答案,代码如下:

void KernelLogger(android::base::LogId, android::base::LogSeverity severity,const char* tag, const char*, unsigned int, const char* msg) {// clang-format offstatic 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)中拿到的,具体过程如下:

  从上图可以得知,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 fswait /dev/block/platform/soc/${ro.boot.bootdevice}symlink /dev/block/platform/soc/${ro.boot.bootdevice} /dev/block/bootdevicemount_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 offstatic 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 onreturn builtin_functions;
}

 &emsp从上面可以看出,mount_all 命令对应的是 do_mount_all 函数,/vendor/etc/fstab.qcom是do_mount_all 函数的传入参数。do_mount_all 的解析流程如下:

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

Android 9(P)之init进程启动源码分析指南之一相关推荐

  1. Android 9 (P)之init进程启动源码分析指南之三

          Android 9 (P)之init进程启动源码分析指南之三 Android 9 (P)系统启动及进程创建源码分析目录: Android 9 (P)之init进程启动源码分析指南之一 An ...

  2. Android 9 (P) Zygote进程启动源码分析指南二

         Android 9 Zygote进程启动源码分析指南二 Android 9 (P) 系统启动及进程创建源码分析目录: Android 9 (P)之init进程启动源码分析指南之一 Andro ...

  3. Android之vold进程启动源码分析

    1.Vold (Volume Daemon)介绍 vold进程接收来自内核的外部设备消息,用于管理和控制Android平台外部存储设备,包括SD插拨.挂载.卸载.格式化等:当外部设备发生变化时,内核通 ...

  4. Android之rild进程启动源码分析

    Android 电话系统框架介绍 在android系统中rild运行在AP上,AP上的应用通过rild发送AT指令给BP,BP接收到信息后又通过rild传送给AP.AP与BP之间有两种通信方式: 1. ...

  5. ServiceManager 进程启动源码分析

    Service Manager是整个Binder机制的守护进程,用来管理开发者创建的各种Server,并且向Client提供查询Server远程接口的功能.Service Manager作为本地服务由 ...

  6. Android系统10 RK3399 init进程启动(三十八) 属性Selinux实战编程

    配套系列教学视频链接: 安卓系列教程之ROM系统开发-百问100ask 说明 系统:Android10.0 设备: FireFly RK3399 (ROC-RK3399-PC-PLUS) 前言 上一节 ...

  7. 【Android 启动过程】Activity 启动源码分析 ( ActivityThread 流程分析 二 )

    文章目录 前言 一.ActivityManagerService.attachApplicationLocked 二.ActivityStackSupervisor.attachApplication ...

  8. 【Android 启动过程】Activity 启动源码分析 ( ActivityThread -> Activity、主线程阶段 二 )

    文章目录 前言 一.ActivityThread 类 handleLaunchActivity -> performLaunchActivity 方法 二.Instrumentation.new ...

  9. 【Android 启动过程】Activity 启动源码分析 ( ActivityThread -> Activity、主线程阶段 一 )

    文章目录 前言 一.ClientTransactionHandler.scheduleTransaction 二.ActivityThread.H 处理 EXECUTE_TRANSACTION 消息 ...

最新文章

  1. C++: 构造函数和析构函数
  2. JavaScript代码规范
  3. ASP.NET中Image控件不能自动刷新
  4. Python Django Cookie的设置和获取相关属性
  5. 小米11pro和vivox60哪个好
  6. 学习3D图形引擎中使用的基本数学
  7. RabbitMQ学习笔记(3)----RabbitMQ Worker的使用
  8. 蓝桥杯 ADV-149 算法提高 特殊的质数肋骨
  9. 想听懂用户的声音,至少得先学会数据分析吧
  10. IIS发布web网站
  11. LDO与电压基准源的精度对比
  12. 你可能修了一个假的“不净观”
  13. 我的爬虫入门作(一)
  14. SOFA Weekly | SOFAJRaft 发布、SOFAJRaft 源码解析文章合集
  15. 盘点2018程序员才懂的100个段子/搞笑图(上篇)
  16. 【Matlab】错误使用 classify (line 233) The pooled covariance matrix of TRAINING must be positive definite.
  17. vs工程中哪些文件可以删除
  18. OpenResty 连接Redis
  19. 2020年数学建模国赛A题:炉温曲线
  20. Intel PinTools 安装使用教程

热门文章

  1. 淘宝二手优必选舵机保姆级驱动教程,看不懂来打我(自行修改ID,有HAL库驱动函数)
  2. 制作纯净版WinPE1.0
  3. 搭建repo服务器管理多个git工程
  4. java刷票代码_Java 刷票器
  5. halcon中如何生成椭圆_《zw版·Halcon-delphi系列原创教程》 Halcon分类函数005·graphics-obj,基本绘图单元,包括线段、矩形、椭圆、圆形...
  6. [Android] ListView实现隔行变色(一)
  7. 利用python预测交通拥堵_Python pyecharts 绘制的交通拥堵情况地图
  8. 金山30而立,怀念“第一程序员求伯君”
  9. Mingw下使用FTD2XX进行FTDI的开发
  10. docker环境搭建(生信学习)