Android O: init进程启动流程分析(阶段一)

Posted ZhangJianIsAStark

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android O: init进程启动流程分析(阶段一)相关的知识,希望对你有一定的参考价值。

目的
最近打算回顾一下以前研究Framework时遇到的问题。
自己发现android演进到8.0后,许多流程又都发生了改变,
于是打算在之前博客的基础上,结合新的代码重新梳理一遍。

本篇博客主要记录一下Android 8.0中的init流程。

背景
当linux内核启动之后,运行的第一个进程是init。
这个进程是一个守护进程,它的生命周期贯穿整个linux 内核运行的始终,
linux中所有其它的进程的共同始祖均为init进程。

Android系统是运作在linux内核上的,为了启动并运行整个android系统,
google实现了android系统的init进程。

Android init进程的入口文件在system/core/init/init.cpp中。
在main函数中,按顺序主要进行了以下工作:

一、判断及增加环境变量

int main(int argc, char** argv) 
    //根据参数,判断是否需要启动的ueventd和watchdogd
    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) 
        install_reboot_signal_handlers();
    

    //增加环境变量
    add_environment("PATH", _PATH_DEFPATH);

二、创建文件系统目录并挂载相关的文件系统

//第一次进入时,is_first_stage为true,对应于初始化的第一阶段
bool is_first_stage = (getenv("INIT_SECOND_STAGE") == nullptr);

if (is_first_stage) 
    //用于记录启动时间
    boot_clock::time_point start_time = boot_clock::now();

    //清除屏蔽字(file mode creation mask),保证新建的目录的访问权限不受屏蔽字影响。
    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));

    //这个是8.0新增的, 收紧了cmdline目录的权限
    // Don't expose the raw commandline to unprivileged processes.
    chmod("/proc/cmdline", 0440);

    //8.0增加了个用户组
    gid_t groups[] =  AID_READPROC ;
    setgroups(arraysize(groups), groups);

    mount("sysfs", "/sys", "sysfs", 0, NULL);

    //以下这部分也是8.0新增的
    mount("selinuxfs", "/sys/fs/selinux", "selinuxfs", 0, NULL);

    //提前创建了kmsg设备节点文件,用于输出log信息
    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));
    ...............

如上所示,该部分主要用于创建和挂载启动所需的文件目录。
需要注意的是,在编译Android系统源码时,在生成的根文件系统中,
并不存在这些目录,它们是系统运行时的目录,即当系统终止时,就会消失。

在init初始化过程中,Android分别挂载了tmpfs,devpts,proc,sysfs这4类文件系统。

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内核引入的,
它把连接在系统上的设备和总线组织成为一个分级的文件,使得它们可以在用户空间存取。

三、屏蔽标准的输入输出并设置Kernel logger

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

前文生成/dev目录后,init进程将调用InitKernelLogging函数,屏蔽标准的输入输出。
InitKernelLogging函数会将标准输入、标准输出、标准错误输出全部重定向到__null__设备中,
然后设置Kernel logger。

3.1 屏蔽标准的输入输出
InitKernelLogging函数定义于system/core/init/log.cpp中。

void InitKernelLogging(char* argv[]) 
    // Make stdin/stdout/stderr all point to /dev/null.
    int fd = open("/sys/fs/selinux/null", O_RDWR);

    //若开启失败,则记录log
    if (fd == -1) 
        int saved_errno = errno;
        android::base::InitLogging(argv, &android::base::KernelLogger, RebootAborter);
        errno = saved_errno;
        PLOG(FATAL) << "Couldn't open /sys/fs/selinux/null";
    

    dup2(fd, 0);
    dup2(fd, 1);
    dup2(fd, 2);
    if (fd > 2) close(fd);

    android::base::InitLogging(argv, &android::base::KernelLogger, RebootAborter);

这里需要说明的是,dup2函数的作用是用来复制一个文件的描述符,
通常用来重定向进程的stdin、stdout和stderr。
它的函数原形是:

int dup2(int oldfd, int targetfd)

该函数执行后,targetfd将变成oldfd的复制品。

因此上述过程其实就是:创建出__null__设备后,将0、1、2绑定到__null__设备上。
因此init进程调用InitKernelLogging函数后,通过标准的输入输出无法输出信息。

3.2 设置kernel logger
我们来跟进一下InitLogging函数,该函数定义于system/core/base/logging.cpp中。

//此处设置的是KernelLogger
void InitLogging(char* argv[], LogFunction&& logger, AbortFunction&& aborter) 
    //设置logger
    SetLogger(std::forward<LogFunction>(logger));
    SetAborter(std::forward<AbortFunction>(aborter));

    if (gInitialized) 
        return;
    

    gInitialized = true;
    ............
    const char* tags = getenv("ANDROID_LOG_TAGS");
    if (tags == nullptr) 
        return;
    

    //根据TAG决定最小记录等级
    std::vector<std::string> specs = Split(tags, " ");
    for (size_t i = 0; i < specs.size(); ++i) 
    // "tag-pattern:[vdiwefs]"
    std::string spec(specs[i]);
    if (spec.size() == 3 && StartsWith(spec, "*:")) 
        switch (spec[2]) 
            case 'v':
                gMinimumLogSeverity = VERBOSE;
                continue;
            ...........
        
    .............

当需要输出日志时,KernelLogger函数就会被调用:

#if defined(__linux__)
void KernelLogger(android::base::LogId, android::base::LogSeverity severity,
                  const char* tag, const char*, unsigned int, const char* msg) 
    ............
    //打开log节点
    static int klog_fd = TEMP_FAILURE_RETRY(open("/dev/kmsg", O_WRONLY | O_CLOEXEC));
    if (klog_fd == -1) return;

    //决定log等级
    int level = kLogSeverityToKernelLogLevel[severity];

    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;
    //通过iovec将log发送到dev/kmsg
    TEMP_FAILURE_RETRY(writev(klog_fd, iov, 1));

#endif

四、挂载一些分区设备
我们继续回到init进程的main函数:

if (is_first_stage) 
    .........
    //挂载特定的分区设备
    if (!DoFirstStageMount()) 
        LOG(ERROR) << "Failed to mount required partitions early ...";
        //panic会尝试reboot
        panic();
    
    .........

我们跟进DoFirstStageMount看看:

// Mounts /system, /vendor, and/or /odm if they are present in the fstab provided by device tree.
bool DoFirstStageMount() 
    // Skips first stage mount if we're in recovery mode.
    if (IsRecoveryMode()) 
        LOG(INFO) << "First stage mount skipped (recovery mode)";
        return true;
    

    // Firstly checks if device tree fstab entries are compatible.
    if (!is_android_dt_value_expected("fstab/compatible", "android,fstab")) 
        LOG(INFO) << "First stage mount skipped (missing/incompatible fstab in device tree)";
        return true;
    

    //满足上述条件时,就会调用FirstStageMount的DoFirstStageMount函数
    std::unique_ptr<FirstStageMount> handle = FirstStageMount::Create();
    if (!handle) 
        LOG(ERROR) << "Failed to create FirstStageMount";
        return false;
    
    //主要是初始化特定设备并挂载
    return handle->DoFirstStageMount();

五、安全相关的初始化工作
接下来,init进程会进行一些安全相关的工作:

if (is_first_stage) 
    ........
    //此处应该是初始化安全框架:Android Verified Boot
    //AVB主要用于防止系统文件本身被篡改,还包含了防止系统回滚的功能,
    //以免有人试图回滚系统并利用以前的漏洞
    SetInitAvbVersionInRecovery();

    // Set up SELinux, loading the SELinux policy.
    selinux_initialize(true);
    .......

AVB相关的信息目前还不太了解,不做深入分析。
此处,我们看看selinux_initialize相关的工作:

static void selinux_initialize(bool in_kernel_domain) 
    Timer t;

    selinux_callback cb;
    //用于打印log的回调函数
    cb.func_log = selinux_klog_callback;
    selinux_set_callback(SELINUX_CB_LOG, cb);
    //用于检查权限的回调函数
    cb.func_audit = audit_callback;
    selinux_set_callback(SELINUX_CB_AUDIT, cb);

    //从注释来看,init进程的运行是区分用户态和内核态的
    //first_stage运行在内核态
    if (in_kernel_domain) 
        LOG(INFO) << "Loading SELinux policy";
        //用于加载sepolicy文件
        //该函数最终将sepolicy文件传递给kernel,这样kernel就有了安全策略配置文件,后续的MAC才能开展起来。
        if (!selinux_load_policy()) 
        panic();
        

    //内核中读取的信息
        bool kernel_enforcing = (security_getenforce() == 1);
        //命令行中得到的数据
        bool is_enforcing = selinux_is_enforcing();
        if (kernel_enforcing != is_enforcing) 
        //用于设置selinux的工作模式。selinux有两种工作模式:
        //1、”permissive”,所有的操作都被允许(即没有MAC),但是如果违反权限的话,会记录日志
        //2、”enforcing”,所有操作都会进行权限检查。在一般的终端中,应该工作于enforing模式
        if(security_setenforce(is_enforcing)) 
        ........
        //将重启进入recovery mode
        security_failure();
        
    

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

        // init's first stage can't set properties, so pass the time to the second stage.
        setenv("INIT_SELINUX_TOOK", std::to_string(t.duration_ms()).c_str(), 1);
     else 
        //在second stage调用时,初始化所有的handle
        selinux_init_all_handles();
    

六、第一阶段收尾工作
现在我们再来看看init过程第一阶段的收尾工作:

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.
    //按selinux policy要求,重新设置init文件属性
    if (restorecon("/init") == -1) 
        PLOG(ERROR) << "restorecon failed";
        //失败的话会reboot
        security_failure();
    

    //设置变量INIT_SECOND_STAGE为true
    setenv("INIT_SECOND_STAGE", "true", 1);

    static constexpr uint32_t kNanosecondsPerMillisecond = 1e6;
    uint64_t start_ms = start_time.time_since_epoch().count() / kNanosecondsPerMillisecond;
    //记录初始化时的时间
    setenv("INIT_STARTED_AT", StringPrintf("%" PRIu64, start_ms).c_str(), 1);

    char* path = argv[0];
    char* args[] =  path, nullptr ;
    //再次调用init的main函数,启动用户态的init进程
    execv(path, args);

    // execv() only returns if an error happened, in which case we
    // panic and never fall through this conditional.
    PLOG(ERROR) << "execv(\\"" << path << "\\") failed";
    // 内核态的进程不应该退出,若退出则会重启
    security_failure();

第一阶段结束时,会复位一些信息,并设置一些环境变量,
最后启动用户态的用户态的init进程,进入init阶段二。

我们在下一篇博客继续分析init阶段二相关的流程。

以上是关于Android O: init进程启动流程分析(阶段一)的主要内容,如果未能解决你的问题,请参考以下文章

Android O: init进程启动流程分析(阶段一)

Android O: zygote进程分析

Android系统启动流程分析

Android6.0系统启动流程分析一:init进程

Android 7.0系统启动流程分析

Android 7.0系统启动流程分析