Android系统启动系列2 init进程

一 概述

init 进程是 Linux 系统中用户空间的第一个进程,进程号为1.当板子上电,bootloader 启动后,加载并启动 kernel,kernel 启动完后,在用户空间启动 init 进程,然后再通过 init 进程,来解析并读取 init.rc 中的相关配置,从而来启动其他相关进程以及其它操作。

init 进程被赋予了很多重要工作,它的启动主要分为两个阶段:
第一阶段

  • ueventd/watchdogd跳转及环境变量设置
  • 创建并挂载文件系统
  • 初始化日志输出、挂载分区设备
  • 启用SELinux安全策略
  • 开始第二阶段前的准备

第二阶段

  • 初始化属性系统
  • 执行SELinux第二阶段并恢复一些文件安全上下文
  • 新建epoll并初始化子进程终止信号处理函数
  • 设置其他系统属性并开启属性服务

init 进程主要作用总结如下:

  • 挂载文件系统并生成相应的设备驱动节点
  • 初始化环境变量,日志输出并启用SELinux安全策略
  • 处理子进程的终止(signal方式)
  • 提供属性服务 解析rc文件启动相关进程和服务

二 kernel 如何启动 init 进程

在 kernel 进入 c 语言阶段后,会开始执行 start_kernel 函数,它负责进行 kernel 正式运行前各个功能的初始化:打印了一些信息、内核工作需要的模块的初始化被依次调用(譬如内存管理、调度系统、异常处理···),最后末尾调用了一个 rest_init 函数启动了三个进程(idle、kernel_init、kthreadd),来开启操作系统的正式运行。如下图所示:
Android系统启动系列2 init进程_第1张图片
Linux 下有3个特殊的进程,idle(swapper)进程(PID = 0)、init 进程(PID = 1)和 kthreadd(PID = 2),这三个进程是 Linux 内核的基础,后面的所有进线程都是基于这三个进程创建的.

  • idle(swapper)进程也是系统的第一个进程,运行在内核态
    idle 进程其 pid=0,它是系统创建的第一个进程,也是唯一一个没有通过 fork 产生的进程.完成加载系统后,演变为进程调度、交换,常常被称为交换进程。它是 init 进程,kthreadd 进程的父进程.可以这样总结:原始进程 (pid=0) 创建 init 进程 (pid=1),然后演化成 idle 进程 (pid=0)
  • init 进程是用户空间的第一个进程,也是用户空间所有进程的父进程
    init 进程由 idle 通过 kernel_thread 创建,在内核空间完成初始化后,加载 init 程序,并最终转变为用户空间的 init 进程,完成系统的初始化,是系统中所有其它用户空间进程的祖先进程。在系统启动完成后,init 将演变为守护进程监视系统的其它进程。
  • kthreadd 进程是内核一个守护进程,也是内核所有线程的父进程
    kthreadd 进程由 idle 通过 kernel_thread 创建,并始终运行在内核空间,负责所有内核线程的调度和管理.它的任务就是管理和调度其他内核线程 kernel_thread,它会循环执行一个 kthreadd 的函数,该函数的作用就是运行 kthread_create_list 全局链表中维护的 kthread,我们调用 kernel_thread 创建的内核线程会被加入到此链表中,因此所有的内核线程都是直接或者间接地以 kthreadd 为父进程。

2.1 start_kernel

kernel/msm-4.19/init/main.c

asmlinkage __visible void __init start_kernel(void)
{
    ....
	rest_init();
}

start_kernel 做了许多初始化,最后跳转到 rest_init,在 rest_init 中会创建 init 和 kthreadd 进程。
rest_init 方法也是定义在 kernel/msm-4.19/init/main.c 中,如下:

2.2 rest_init

/*  inline修饰的函数类型参数会被内联优化,
    noinline修饰的函数类型参数不会被内联优化.*/
static noinline void __ref rest_init(void)
{
	........ 
    /* kernel-thread 创建init进程,pid=1,
       CLONE_FS 表示子进程和父进程共享相同的文件系统,包括root,当前目录,umask,CLONE_SIGHAND,
       子进程与父进程共享相同的信号处理(signal handler)表*/
	kernel_thread(kernel_init, NULL, CLONE_FS);//创建init进程
	........
	pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);//创建kthreadd进程 
    ........
	cpu_startup_entry(CPUHP_ONLINE); //调用cpu_idle_loop使的idle进程进入自己的事件循环
}

kernel_thread 会调用 do_fork 函数用于创建进程,进程创建成功后会通过函数指针回调执行 kernel_init 函数;进程创建过程在此就不做分析,我们来看 kernel_init 函数,依然定义在 kernel/msm-4.19/init/main.c 中,代码如下:

2.3 kernel_init

static int __ref kernel_init(void *unused)
{
	kernel_init_freeable(); //进行init进程的一些初始化操作
	/* need to finish all async __init code before freeing the memory */
	async_synchronize_full();// 等待所有异步调用执行完成,,在释放内存前,必须完成所有的异步 __init 代码
	free_initmem();// 释放所有init.* 段中的内存
	mark_rodata_ro(); //arm64空实现
	system_state = SYSTEM_RUNNING;// 设置系统状态为运行状态
	numa_default_policy(); // 设定NUMA系统的默认内存访问策略
 
	flush_delayed_fput(); // 释放所有延时的struct file结构体
 
	if (ramdisk_execute_command) { //ramdisk_execute_command的值为"/init"
		if (!run_init_process(ramdisk_execute_command)) //运行根目录下的init程序
			return 0;
		pr_err("Failed to execute %s\n", ramdisk_execute_command);
	}
	if (execute_command) { //execute_command的值如果有定义就去根目录下找对应的应用程序,然后启动
		if (!run_init_process(execute_command))
			return 0;
		pr_err("Failed to execute %s.  Attempting defaults...\n",
			execute_command);
	}
	if (!run_init_process("/sbin/init") || //如果ramdisk_execute_command和execute_command定义的应用程序都没有找到,
	//就到根目录下找 /sbin/init,/etc/init,/bin/init,/bin/sh 这四个应用程序进行启动
 
	    !run_init_process("/etc/init") ||
	    !run_init_process("/bin/init") ||
	    !run_init_process("/bin/sh"))
		return 0;
 
	panic("No init found.  Try passing init= option to kernel. "
	      "See Linux Documentation/init.txt for guidance.");
}
static int run_init_process(const char *init_filename)
{
    argv_init[0] = init_filename;
    return do_execve(getname_kernel(init_filename), //do_execve就是执行一个可执行文件
        (const char __user *const __user *)argv_init,
        (const char __user *const __user *)envp_init);
}

Android系统启动系列2 init进程_第2张图片
kernel_init 主要工作是完成一些 init 的初始化操作,然后去系统根目录下依次找 ramdisk_execute_command 和 execute_command 设置的应用程序,如果这两个目录都找不到,就依次去根目录下找 /sbin/init,/etc/init,/bin/init,/bin/sh 这四个应用程序进行启动,只要这些应用程序有一个启动了,其他就不启动了.Android 系统一般会在根目录下放一个 init 的可执行文件,也就是说 Linux 系统的 init 进程在内核初始化完成后,就直接通过 run_init_process 函数执行 init 这个文件,该可执行文件的源代码在 system/core/init/main.cpp 中。

三 init 进程的启动

前面已经通过 kernel_init,启动了 init 进程,init 进程属于一个守护进程,准确的说,它是 Linux 系统中用户空间的第一个进程,它的进程号为1。它的生命周期贯穿整个 Linux 内核运行的始终。可以通过"adb shell ps |grep init" 的命令来查看 init 的进程号。Android Q(10.0) 的 init 进程入口函数由原先的 init.cpp 调整到了 main.cpp,把各个阶段的操作分离开来,使代码更加简洁明了,接下来我们就从 main 函数开始分析。

3.1 init 进程入口 main 函数

platform/system/core/init/main.cpp

/*
 * 1.第一个参数argc表示参数个数,第二个参数是参数列表,也就是具体的参数
 * 2.main函数有四个参数入口,
 *一是参数中有ueventd,进入ueventd_main
 *二是参数中有subcontext,进入InitLogging 和SubcontextMain
 *三是参数中有selinux_setup,进入SetupSelinux
 *四是参数中有second_stage,进入SecondStageMain
 *3.main的执行顺序如下:
   *  (1)ueventd_main    init进程创建子进程ueventd,
   *      并将创建设备节点文件的工作托付给ueventd,ueventd通过两种方式创建设备节点文件
   *  (2)FirstStageMain  启动第一阶段
   *  (3)SetupSelinux     加载selinux规则,并设置selinux日志,完成SELinux相关工作
   *  (4)SecondStageMain  启动第二阶段
 */
int main(int argc, char** argv) {
    //当argv[0]的内容为ueventd时,strcmp的值为0,!strcmp为1
    //1表示true,也就执行ueventd_main,ueventd主要是负责设备节点的创建、权限设定等一些列工作
    if (!strcmp(basename(argv[0]), "ueventd")) {
        return ueventd_main(argc, argv);
    } 
    //当传入的参数个数大于1时,执行下面的几个操作
    if (argc > 1) {
        //参数为subcontext,初始化日志系统,
        if (!strcmp(argv[1], "subcontext")) {
            android::base::InitLogging(argv, &android::base::KernelLogger);
            const BuiltinFunctionMap function_map;
            return SubcontextMain(argc, argv, &function_map);
        } 
        //参数为“selinux_setup”,启动Selinux安全策略
        if (!strcmp(argv[1], "selinux_setup")) {
            return SetupSelinux(argv);
        }
       //参数为“second_stage”,启动init进程第二阶段
        if (!strcmp(argv[1], "second_stage")) {
            return SecondStageMain(argc, argv);
        }
    }
    // 默认启动init进程第一阶段
    return FirstStageMain(argc, argv);
}

通过对 system/core/init/README.md 的阅读可以知道 main 函数会执行多次,启动顺序是这样的 FirstStageMain -> SetupSelinux -> SecondStageMain

3.2 ueventd_main

platform/system/core/init/ueventd.cpp

int ueventd_main(int argc, char** argv) {
    //设置新建文件的默认值,这个与chmod相反,这里相当于新建文件后的权限为666
    umask(000);  
    //初始化内核日志,位于节点/dev/kmsg, 此时logd、logcat进程还没有起来,
    //采用kernel的log系统,打开的设备节点/dev/kmsg, 那么可通过cat /dev/kmsg来获取内核log。
    android::base::InitLogging(argv, &android::base::KernelLogger); 
    //注册selinux相关的用于打印log的回调函数
    SelinuxSetupKernelLogging(); 
    SelabelInitialize(); 
    //解析xml,根据不同SOC厂商获取不同的hardware rc文件
    auto ueventd_configuration = ParseConfig({"/ueventd.rc", "/vendor/ueventd.rc",
                                              "/odm/ueventd.rc", "/ueventd." + hardware + ".rc"}); 
    //冷启动
    if (access(COLDBOOT_DONE, F_OK) != 0) {
        ColdBoot cold_boot(uevent_listener, uevent_handlers);
        cold_boot.Run();
    }
    for (auto& uevent_handler : uevent_handlers) {
        uevent_handler->ColdbootDone();
    } 
    //忽略子进程终止信号
    signal(SIGCHLD, SIG_IGN);
    // Reap and pending children that exited between the last call to waitpid() and setting SIG_IGN
    // for SIGCHLD above.
       //在最后一次调用waitpid()和为上面的sigchld设置SIG_IGN之间退出的获取和挂起的子级
    while (waitpid(-1, nullptr, WNOHANG) > 0) {
    } 
    //监听来自驱动的uevent,进行“热插拔”处理
    uevent_listener.Poll([&uevent_handlers](const Uevent& uevent) {
        for (auto& uevent_handler : uevent_handlers) {
            uevent_handler->HandleUevent(uevent); //热启动,创建设备
        }
        return ListenerAction::kContinue;
    });
    return 0;
}

Android 根文件系统的镜像中不存在 “/dev” 目录,该目录是 init 进程启动后动态创建的.因此,建立 Android 中设备节点文件的重任,也落在了 init 进程身上。为此,init 进程创建子进程 ueventd,并将创建设备节点文件的工作托付给 ueventd.

ueventd 通过两种方式创建设备节点文件

  • “冷插拔”(Cold Plug)
    即以预先定义的设备信息为基础,当 ueventd 启动后,统一创建设备节点文件。这一类设备节点文件也被称为静态节点文件
  • “热插拔”(Hot Plug)
    即在系统运行中,当有设备插入 USB 端口时,ueventd 就会接收到这一事件,为插入的设备动态创建设备节点文件。这一类设备节点文件也被称为动态节点文件

3.3 FirstStageMain

platform\system\core\init\first_stage_init.cpp

int FirstStageMain(int argc, char** argv) {
    //init crash时重启引导加载程序
    //这个函数主要作用将各种信号量,如SIGABRT,SIGBUS等的行为设置为SA_RESTART,一旦监听到这些信号即执行重启系统
    if (REBOOT_BOOTLOADER_ON_PANIC) {
        InstallRebootSignalHandlers();
    }
    //清空文件权限
    umask(0); 
    CHECKCALL(clearenv());
    CHECKCALL(setenv("PATH", _PATH_DEFPATH, 1)); 
    //在RAM内存上获取基本的文件系统,剩余的被rc文件所用
    CHECKCALL(mount("tmpfs", "/dev", "tmpfs", MS_NOSUID, "mode=0755"));//挂载tmpfs文件系统
    CHECKCALL(mkdir("/dev/pts", 0755));
    CHECKCALL(mkdir("/dev/socket", 0755));
    CHECKCALL(mount("devpts", "/dev/pts", "devpts", 0, NULL));//挂载devpts文件系统
#define MAKE_STR(x) __STRING(x)
    //挂载proc文件系统
    CHECKCALL(mount("proc", "/proc", "proc", 0, "hidepid=2,gid=" MAKE_STR(AID_READPROC)));
#undef MAKE_STR 
    // 非特权应用不能使用Andrlid cmdline
    CHECKCALL(chmod("/proc/cmdline", 0440));
    gid_t groups[] = {AID_READPROC};
    CHECKCALL(setgroups(arraysize(groups), groups));
    CHECKCALL(mount("sysfs", "/sys", "sysfs", 0, NULL));// 挂载 sysfs 文件系统
    CHECKCALL(mount("selinuxfs", "/sys/fs/selinux", "selinuxfs", 0, NULL)); 
    // 提前创建了 kmsg 设备节点文件,用于输出 log 信息
    CHECKCALL(mknod("/dev/kmsg", S_IFCHR | 0600, makedev(1, 11))); 
    if constexpr (WORLD_WRITABLE_KMSG) {
        CHECKCALL(mknod("/dev/kmsg_debug", S_IFCHR | 0622, makedev(1, 11)));
    } 
    CHECKCALL(mknod("/dev/random", S_IFCHR | 0666, makedev(1, 8)));
    CHECKCALL(mknod("/dev/urandom", S_IFCHR | 0666, makedev(1, 9))); 
    //这对于日志包装器是必需的,它在ueventd运行之前被调用
    CHECKCALL(mknod("/dev/ptmx", S_IFCHR | 0666, makedev(5, 2)));
    CHECKCALL(mknod("/dev/null", S_IFCHR | 0666, makedev(1, 3))); 
    //在第一阶段挂在tmpfs、mnt/vendor、mount/product分区。其他的分区不需要在第一阶段加载,
    //只需要在第二阶段通过rc文件解析来加载。
    CHECKCALL(mount("tmpfs", "/mnt", "tmpfs", MS_NOEXEC | MS_NOSUID | MS_NODEV,
                    "mode=0755,uid=0,gid=1000"));
    
    //创建可供读写的vendor目录
    CHECKCALL(mkdir("/mnt/vendor", 0755));
    // /mnt/product is used to mount product-specific partitions that can not be
    // part of the product partition, e.g. because they are mounted read-write.
    CHECKCALL(mkdir("/mnt/product", 0755));
 
    // 挂载APEX,这在Android 10.0中特殊引入,用来解决碎片化问题,类似一种组件方式,对Treble的增强,
    // 不写谷歌特殊更新不需要完整升级整个系统版本,只需要像升级APK一样,进行APEX组件升级
    CHECKCALL(mount("tmpfs", "/apex", "tmpfs", MS_NOEXEC | MS_NOSUID | MS_NODEV,
                    "mode=0755,uid=0,gid=0"));
 
    // /debug_ramdisk is used to preserve additional files from the debug ramdisk
    CHECKCALL(mount("tmpfs", "/debug_ramdisk", "tmpfs", MS_NOEXEC | MS_NOSUID | MS_NODEV,
                    "mode=0755,uid=0,gid=0"));
#undef CHECKCALL
     //把标准输入、标准输出和标准错误重定向到空设备文件"/dev/null"
    SetStdioToDevNull(argv);
    //在/dev目录下挂载好 tmpfs 以及 kmsg 
    //这样就可以初始化 /kernel Log 系统,供用户打印log
    InitKernelLogging(argv); 
    ... 
    /* 初始化一些必须的分区
     *主要作用是去解析/proc/device-tree/firmware/android/fstab,
     * 然后得到"/system", "/vendor", "/odm"三个目录的挂载信息
     */
    if (!DoFirstStageMount()) {
        LOG(FATAL) << "Failed to mount required partitions early ...";
    } 
    struct stat new_root_info;
    if (stat("/", &new_root_info) != 0) {
        PLOG(ERROR) << "Could not stat(\"/\"), not freeing ramdisk";
        old_root_dir.reset();
    } 
    if (old_root_dir && old_root_info.st_dev != new_root_info.st_dev) {
        FreeRamdisk(old_root_dir.get(), old_root_info.st_dev);
    }
    // 此处应该是初始化安全框架:Android Verified Boot,AVB 主要用于防止系统文件本身被篡改,
    // 还包含了防止系统回滚的功能,以免有人试图回滚系统并利用以前的漏洞
    SetInitAvbVersionInRecovery(); 
    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); 
    //再次启动 main 函数,只不过这次传入的参数是 selinux_setup
    const char* path = "/system/bin/init";
    const char* args[] = {path, "selinux_setup", nullptr};
    execv(path, const_cast<char**>(args));
    PLOG(FATAL) << "execv(\"" << path << "\") failed"; 
    return 1;
}

init 进程第一阶段做的主要工作是挂载分区,创建设备节点和一些关键目录,初始化日志输出系统,启用SELinux安全策略.

3.4 加载 SELinux 规则

platform\system\core\init\selinux.cpp

/*此函数初始化selinux,然后执行init以在init selinux中运行*/
int SetupSelinux(char** argv) {
       //初始化Kernel日志
    InitKernelLogging(argv); 
       // Debug版本init crash时重启引导加载程序
    if (REBOOT_BOOTLOADER_ON_PANIC) {
        InstallRebootSignalHandlers();
    } 
    //注册回调,用来设置需要写入kmsg的selinux日志
    SelinuxSetupKernelLogging();   
     //加载SELinux规则
    SelinuxInitialize(); 
    /*
       *我们在内核域中,希望转换到init域。在其xattrs中存储selabel的文件系统(如ext4)不需要显式restorecon,
       *但其他文件系统需要。尤其是对于ramdisk,如对于a/b设备的恢复映像,这是必需要做的一步。
       *其实就是当前在内核域中,在加载Seliux后,需要重新执行init切换到C空间的用户态
       */
    if (selinux_android_restorecon("/system/bin/init", 0) == -1) {
        PLOG(FATAL) << "restorecon failed of /system/bin/init failed";
    } 
   //准备启动init进程,传入参数second_stage
    const char* path = "/system/bin/init";
    const char* args[] = {path, "second_stage", nullptr};
    execv(path, const_cast<char**>(args)); 
    /*
       *执行 /system/bin/init second_stage, 进入第二阶段
       */
    PLOG(FATAL) << "execv(\"" << path << "\") failed"; 
    return 1;
}

SELinux 是 Security-Enhanced Linux 的简称,是美国国家安全局 NSA (The National Security Agency) 和 SCC(Secure Computing Corporation) 联合开发的一个安全增强型 Linux.这样就可以很好的对所有进程强制执行访问控制,从而让 Android 更好的保护和限制系统服务、控制对应用数据和系统日志的访问,降低恶意软件的影响。

SELinux 有两种工作模式:

  • permissive,所有的操作都被允许(即没有MAC),但是如果违反权限的话,会记录日志,一般eng模式用
  • enforcing,所有操作都会进行权限检查。一般user和user-debug模式用

不管是 security_setenforce 还是 security_getenforce 都是去操作 /sys/fs/selinux/enforce 文件, 0 表示 permissive;1 表示 enforcing.不过 SELinux 并不是一次就初始化完成的,接下来就是再次调用 main 函数,进入最后的 SecondStageMain 阶段.

小结以下,第一阶段主要完成以下内容:

  • 创建和挂载相关的文件目录
    需要注意的是,在编译 Android 系统源码时,在生成的根文件系统中,并不存在这些目录,它们是系统运行时的目录,即当系统终止时,就会消失
  • 重定向输入输出,初始化内核 Log 系统
  • 初始化和加载 SELinux 策略

下面来看第二阶段的操作.
Android系统启动系列2 init进程_第3张图片

3.5 SecondStageMain

system/core/init/init.cpp

int SecondStageMain(int argc, char** argv) {
    //  又调用了这两个方法
    SetStdioToDevNull(argv);
    //  初始化本阶段内核日志
    InitKernelLogging(argv);
    //  ...
    //  正在引导后台固件加载程序
    close(open("/dev/.booting", O_WRONLY | O_CREAT | O_CLOEXEC, 0000));
    //  系统属性初始化
    property_init();
    //  系统属性设置相关,而且下面还有很多地方都在 property_set
    //  ...
    //  清理环境
    SelinuxRestoreContext(); //将 SELinux 设置为第二阶段
    //  创建 Epoll
    Epoll epoll;
    //  注册信号处理
    InstallSignalFdHandler(&epoll);
    //  加载默认的系统属性
    property_load_boot_defaults(load_debug_prop);
    //  启动属性服务
    StartPropertyService(&epoll);
    //  重头戏,解析 init.rc 和其他 rc
    // am 和 sm 就是用来接收解析出来的数据
    //  里面基本上是要执行的 action 和要启动的 service
    LoadBootScripts(am, sm);
    //  往 am 里面添加待执行的 Action 和 Trigger
    while (true) {
        //  执行 Action
        am.ExecuteOneCommand();
        //  还有就是重启死掉的子进程
        auto next_process_action_time = HandleProcessActions();
        // 循环等待事件发生
        if (auto result = epoll.Wait(epoll_timeout); !result) {
            LOG(ERROR) << result.error();
        }
    }    
}

这是整个 init 启动阶段最重要的部分,有四个比较重要的点,它们分别是属性服务、SIGCHLD 信号处理 、init.rc 解析以及方法尾部的死循环.

3.5.1 属性服务

我们在开发和调试过程中看到可以通过 property_set 可以轻松设置系统属性,那干嘛这里还要启动一个属性服务呢?这里其实涉及到一些权限的问题,不是所有进程都可以随意修改任何的系统属性,Android 将属性的设置统一交由 init 进程管理,其他进程不能直接修改属性,而只能通知 init 进程来修改,而在这过程中,init 进程可以进行权限控制.

比如当某个进程 A,通过 property_set() 修改属性值后,init 进程会检查访问权限,当权限满足要求后,则更改相应的属性值,属性值一旦改变则会触发相应的触发器(即 rc 文件中的 on 开头的语句),在 Android Shared Memmory(共享内存区域)中有一个_system_property_area_ 区域,里面记录着所有的属性值。对于进程 A 通过 property_get() 方法,获取的也是该共享内存区域的属性值。

属性服务相关代码在 SecondStageMain 阶段其实主要做了三件事:创建共享内存、加载各种属性值以及创建属性服务的 Socket。下面是这关于这几部分的片段:

void property_init() { 
    //设置SELinux回调,进行权限控制
    selinux_callback cb;
    cb.func_audit = PropertyAuditCallback;
    selinux_set_callback(SELINUX_CB_AUDIT, cb);
    //创建目录 /dev/__properties__
    mkdir("/dev/__properties__", S_IRWXU | S_IXGRP | S_IXOTH);
    //加载一些系统属性的类别信息,最后将加载的链表写入/dev/__properties__/property_info里
    //并映射到内存
    CreateSerializedPropertyInfo();
    if (__system_property_area_init()) {
        LOG(FATAL) << "Failed to initialize property area";
    }
    if (!property_info_area.LoadDefaultPath()) {
        LOG(FATAL) << "Failed to load serialized property info file";
    }
}

该方法核心功能在执行 __system_property_area_init() 方法,创建用于跨进程的共享内存。主要工作如下:

  • 执行 open(),打开名为 ”/dev/properties“ 的共享内存文件,并设置大小为128KB;
  • 执行 mmap(),将该内存映射到 init 进程;
  • 将该内存的首地址保存在全局变量 __system_property_area __,后续的增加或者修改属性都基于该变量来计算位置。

加载各种属性值如下:

property_load_boot_defaults {
    //  代码中很多这样的代码
    load_properties_from_file("/system/build.prop", nullptr, &properties);
    load_properties_from_file("/vendor/default.prop", nullptr, &properties);
    load_properties_from_file("/vendor/build.prop", nullptr, &properties);
    load_properties_from_file("/product/build.prop", nullptr, &properties);
    load_properties_from_file("/product_services/build.prop", nullptr, &properties);
    load_properties_from_file("/factory/factory.prop", "ro.*", &properties);
    //  会调用 PropertySet 设置这些属性值
}

启动属性服务如下:

void StartPropertyService(Epoll* epoll) {
    property_set("ro.property_service.version", "2"); 
    //建立socket连接
    if (auto result = CreateSocket(PROP_SERVICE_NAME, SOCK_STREAM | SOCK_CLOEXEC | SOCK_NONBLOCK,
                                   false, 0666, 0, 0, {})) {
        property_set_fd = *result;
    } else {
        PLOG(FATAL) << "start_property_service socket creation failed: " << result.error();
    } 
    // 最大监听8个并发
    listen(property_set_fd, 8); 
    // 注册property_set_fd,当收到句柄改变时,通过handle_property_set_fd来处理
    if (auto result = epoll->RegisterHandler(property_set_fd, handle_property_set_fd); !result) {
        PLOG(FATAL) << result.error();
    }
}
Result<void> Epoll::RegisterHandler(int fd, std::function<void()> handler, uint32_t events) {
    if (!events) {
        return Error() << "Must specify events";
    }
    auto [it, inserted] = epoll_handlers_.emplace(fd, std::move(handler));
    if (!inserted) {
        return Error() << "Cannot specify two epoll handlers for a given FD";
    }
    epoll_event ev;
    ev.events = events;
    // std::map's iterators do not get invalidated until erased, so we use the
    // pointer to the std::function in the map directly for epoll_ctl.
    ev.data.ptr = reinterpret_cast<void*>(&it->second);
    // 将fd的可读事件加入到epoll_fd_的监听队列中
    if (epoll_ctl(epoll_fd_, EPOLL_CTL_ADD, fd, &ev) == -1) {
        Result<void> result = ErrnoError() << "epoll_ctl failed to add fd";
        epoll_handlers_.erase(fd);
        return result;
    }
    return {};
}

首先创建一个 socket 并返回文件描述符,然后设置最大并发数为8,其他进程可以通过这个 socket 通知 init 进程修改系统属性,最后注册 epoll 事件,也就是当监听到 property_set_fd 改变时调用 handle_property_set_fd 处理.

现在需要思考下属性服务为什么要使用共享内存?Socket 作用是什么?

首先共享内存是一种高效的进程间通信方式,本身这些属性值在内存中存在一份即可,不需要每个进程都复制一份到自己的空间中,而且由于是共享的,所以谁都能访问。但是如果谁都能随时来读写(除了只读部分的属性),那也还是会出问题,可能会出现内容不一致问题,所以大家并不是直接对共享内存进行操作,而是通过属性服务的 Socket 的对其进行操作,这样就避免了所有进程直接对那块共享内存进行操作。

不同属性执行逻辑有所不同,主要区分如下:

属性名以ctl.开头,则表示是控制消息,控制消息用来执行一些命令。例如:
setprop ctl.start bootanim 查看开机动画;
setprop ctl.stop bootanim 关闭开机动画;
setprop ctl.start pre-recovey 进入 recovery 模式;
属性名以 ro. 开头,则表示是只读的,不能设置,所以直接返回;
属性名以 persist. 开头,则需要把这些值写到对应文件;需要注意的是,persist 用于持久化保存某些属性值,当同时也带来了额外的 IO 操作。

3.5.2 SIGCHLD信号处理

在 Android 中,当一个进程退出时,会向它的父进程发送一个 SIGCHLD 信号。父进程收到该信号后,会释放分配给该子进程的系统资源;并且父进程需要调用 wait() 或 waitpid() 等待子进程结束。如果父进程没有做这种处理,且父进程初始化时也没有调用 signal(SIGCHLD, SIG_IGN) 来显示忽略对 SIGCHLD 的处理,这时子进程将一直保持当前的退出状态,不会完全退出。这样的子进程不能被调度,所做的只是在进程列表中占据一个位置,保存了该进程的 PID、终止状态、CPU 使用时间等信息;我们将这种进程称为 “Zombie” 进程,即僵尸进程。

在 Linux 中,设置僵尸进程的目的是维护子进程的一些信息,以供父进程后续查询获取。特殊的,如果一个父进程终止,那么它的所有僵尸子进程的父进程将被设置为 init 进程,并由 init 进程负责回收这些僵尸进程(init 进程将 wait() / waitpid() 它们,并清除它们在进程列表中的信息)。

由于僵尸进程仍会在进程列表中占据一个位置,而 Linux 所支持的最大进程数量是有限的;超过这个界限值后,我们就无法创建进程。所以,我们有必要清理那些僵尸进程,以保证系统的正常运作。因为 init 是一个守护进程,为了防止 init 的子进程成为僵尸进程,需要 init 在子进程结束时获取子进程的结束码,通过结束码将程序表中的子进程移除,防止成为僵尸进程的子进程占用程序表的空间.

子进程重启流程如下图所示:
Android系统启动系列2 init进程_第4张图片
信号处理主要工作如下:

  • 初始化信号 signal 句柄
  • 注册 epoll 句柄
  • 循环处理子进程
  • 处理子进程终止

注: EPOLL 类似于 POLL,是 Linux 中用来做事件触发的,跟 EventBus 功能差不多。Linux 很长的时间都在使用 select 来做事件触发,它是通过轮询来处理的,轮询的 fd 数目越多,自然耗时越多,对于大量的描述符处理,EPOLL 更有优势

static void InstallSignalFdHandler(Epoll* epoll) { 
    // SA_NOCLDSTOP使init进程只有在其子进程终止时才会受到SIGCHLD信号
    const struct sigaction act { .sa_handler = SIG_DFL, .sa_flags = SA_NOCLDSTOP };
    sigaction(SIGCHLD, &act, nullptr); //建立信号绑定关系
    sigset_t mask;
    sigemptyset(&mask);
    sigaddset(&mask, SIGCHLD);  
    if (!IsRebootCapable()) {
        // 如果init不具有 CAP_SYS_BOOT的能力,则它此时正值容器中运行
        // 在这种场景下,接收SIGTERM 将会导致系统关闭
        sigaddset(&mask, SIGTERM);
    } 
    if (sigprocmask(SIG_BLOCK, &mask, nullptr) == -1) {
        PLOG(FATAL) << "failed to block signals";
    } 
    // 注册处理程序以解除对子进程中的信号的阻止
    const int result = pthread_atfork(nullptr, nullptr, &UnblockSignals);
    if (result != 0) {
        LOG(FATAL) << "Failed to register a fork handler: " << strerror(result);
    } 
    //创建信号句柄
    signal_fd = signalfd(-1, &mask, SFD_CLOEXEC);
    if (signal_fd == -1) {
        PLOG(FATAL) << "failed to create signalfd";
    } 
    //信号注册,当signal_fd收到信号时,触发HandleSignalFd
    if (auto result = epoll->RegisterHandler(signal_fd, HandleSignalFd); !result) {
        LOG(FATAL) << result.error();
    }
}

在 Linux 当中,父进程是通过捕捉 SIGCHLD 信号来得知子进程运行结束的情况,SIGCHLD 信号会在子进程终止的时候发出,了解这些背景后,我们来看看 init 进程如何处理这个信号。首先,新建一个 sigaction 结构体,sa_handler 是信号处理函数,指向内核指定的函数指针 SIG_DFL 和 Android 9.0 及之前的版本不同,这里不再通过 socket 的读写句柄进行接收信号,改成了内核的信号处理函数 SIG_DFL。然后,sigaction(SIGCHLD, &act, nullptr) 这个是建立信号绑定关系,也就是说当监听到 SIGCHLD 信号时,由 act 这个 sigaction 结构体处理.最后 RegisterHandler 的作用就是 signal_read_fd(之前的s[1])收到信号,触发 handle_signal.终上所述,InstallSignalFdHandler 函数的作用就是:接收到 SIGCHLD 信号时触发 HandleSignalFd 进行信号处理.

Android系统启动系列2 init进程_第5张图片
platform/system/core/epoll.cpp
RegisterHandler

Result<void> Epoll::RegisterHandler(int fd, std::function<void()> handler, uint32_t events) {
    if (!events) {
        return Error() << "Must specify events";
    }
    auto [it, inserted] = epoll_handlers_.emplace(fd, std::move(handler));
    if (!inserted) {
        return Error() << "Cannot specify two epoll handlers for a given FD";
    }
    epoll_event ev;
    ev.events = events;
    // std::map's iterators do not get invalidated until erased, so we use the
    // pointer to the std::function in the map directly for epoll_ctl.
    ev.data.ptr = reinterpret_cast<void*>(&it->second);
    // 将fd的可读事件加入到epoll_fd_的监听队列中
    if (epoll_ctl(epoll_fd_, EPOLL_CTL_ADD, fd, &ev) == -1) {
        Result<void> result = ErrnoError() << "epoll_ctl failed to add fd";
        epoll_handlers_.erase(fd);
        return result;
    }
    return {};
}

RegisterHandler,信号注册,把 fd 句柄加入到 epoll_fd_ 的监听队列中

HandleSignalFd

static void HandleSignalFd() {
    signalfd_siginfo siginfo;
    ssize_t bytes_read = TEMP_FAILURE_RETRY(read(signal_fd, &siginfo, sizeof(siginfo)));
    if (bytes_read != sizeof(siginfo)) {
        PLOG(ERROR) << "Failed to read siginfo from signal_fd";
        return;
    } 
   //监控SIGCHLD信号
    switch (siginfo.ssi_signo) {
        case SIGCHLD:
            ReapAnyOutstandingChildren();
            break;
        case SIGTERM:
            HandleSigtermSignal(siginfo);
            break;
        default:
            PLOG(ERROR) << "signal_fd: received unexpected signal " << siginfo.ssi_signo;
            break;
    }
}

HandleSignalFd 监控 SIGCHLD 信号,调用 ReapAnyOutstandingChildren 来终止出现问题的子进程

void ReapAnyOutstandingChildren() {
    while (ReapOneProcess()) {
    }
}
 
static bool ReapOneProcess() {
    siginfo_t siginfo = {};
    //用waitpid函数获取状态发生变化的子进程pid
    //waitpid的标记为WNOHANG,即非阻塞,返回为正值就说明有进程挂掉了
    if (TEMP_FAILURE_RETRY(waitid(P_ALL, 0, &siginfo, WEXITED | WNOHANG | WNOWAIT)) != 0) {
        PLOG(ERROR) << "waitid failed";
        return false;
    } 
    auto pid = siginfo.si_pid;
    if (pid == 0) return false; 
    // 当我们知道当前有一个僵尸pid,我们使用scopeguard来清楚该pid
    auto reaper = make_scope_guard([pid] { TEMP_FAILURE_RETRY(waitpid(pid, nullptr, WNOHANG)); }); 
    std::string name;
    std::string wait_string;
    Service* service = nullptr; 
    if (SubcontextChildReap(pid)) {
        name = "Subcontext";
    } else {
        //通过pid找到对应的service
        service = ServiceList::GetInstance().FindService(pid, &Service::pid); 
        if (service) {
            name = StringPrintf("Service '%s' (pid %d)", service->name().c_str(), pid);
            if (service->flags() & SVC_EXEC) {
                auto exec_duration = boot_clock::now() - service->time_started();
                auto exec_duration_ms =
                    std::chrono::duration_cast<std::chrono::milliseconds>(exec_duration).count();
                wait_string = StringPrintf(" waiting took %f seconds", exec_duration_ms / 1000.0f);
            } else if (service->flags() & SVC_ONESHOT) {
                auto exec_duration = boot_clock::now() - service->time_started();
                auto exec_duration_ms =
                        std::chrono::duration_cast<std::chrono::milliseconds>(exec_duration)
                                .count();
  wait_string = StringPrintf(" oneshot service took %f seconds in background",exec_duration_ms / 1000.0f);
            }
        } else {
            name = StringPrintf("Untracked pid %d", pid);
        }
    } 
    if (siginfo.si_code == CLD_EXITED) {
        LOG(INFO) << name << " exited with status " << siginfo.si_status << wait_string;
    } else {
        LOG(INFO) << name << " received signal " << siginfo.si_status << wait_string;
    } 
    //没有找到service,说明已经结束了,退出
    if (!service) return true; 
    service->Reap(siginfo);//清除子进程相关的资源 
    if (service->flags() & SVC_TEMPORARY) {
        ServiceList::GetInstance().RemoveService(*service); //移除该service
    } 
    return true;
}

ReapOneProcess 是最终的处理函数了,这个函数先用 waitpid 找出挂掉进程的 pid,然后根据 pid 找到对应 Service,最后调用 Service 的 Reap 方法清除资源,根据进程对应的类型,决定是否重启机器或重启进程.

3.5.3 init.rc 解析

init 是用户空间的第一个进程,接下来需要来启动其他的进程。但是 init 进程如何启动其他进程呢?其他进程都是一个二进制文件,我们可以直接通过 exec 的命令方式来启动,例如 ./system/bin/init second_stage,来启动 init 进程的第二阶段。但是 Android 系统有那么多的 Native 进程,如果都通过 exec 在代码中一个个来执行进程的启动,那无疑是一个灾难性的设计。基于此 Android 推出了一个 init.rc 机制,即类似通过读取配置文件的方式,来启动不同的进程。接下来我们就来看看什么是 init.rc.

init.rc 是一个配置文件,是由 Android 初始化语言(Android Init Language)编写的脚本。它是非常重要的配置文件,而且众多 rc 文件中 init.rc 是最主要的文件,rc 文件语法是以行为单位,以空格间隔的语法,以 # 开始代表注释行。rc 文件主要包含 Action、Command、Service、Options,其中对于 Action 和 Service 的名称都是唯一的,对于重复的命名视为无效。
Android系统启动系列2 init进程_第6张图片
动作 Action

格式:
on < trigger > ##触发条件
< command1 > ## 执行命令
< command2 > ##可以执行多个命令
< command3 >

系统源码中常见的有:

#当init被触发时执行
on init
  <command>
  ...
#当属性sys.boot_completed被设置为1时执行
on property:sys.boot_completed=1
  <command1>
  ...

Action 是有名字的一系列的命令。Action 有一个 trigger(触发器),用于决定该 Action 应在何时执行。当一个事件发生并匹配了一个 Action 的 trigger,相应的 Action 将被添加到即将执行(to-be-executed)队列的尾部(除非该 Action 存在与队列上了)。每个 Action 在队列中顺序排列,每个 Action 中的 Command 将会顺序执行。init 在执行 Command 的过程中同时会执行其他活动(设备节点的创建/销毁,属性设置,进程重启)。

命令 Command

Command 是 Action 的命令列表中的命令,或者是 Service 中的选项 onrestart 的参数命令,命令将在所属事件发生时被一个个地执行.

下面列举常用的命令

class_start : 启动属于同一个 class 的所有服务;
class_stop : 停止指定类的服务
start : 启动指定的服务,若已启动则跳过;
stop : 停止正在运行的服务
setprop :设置属性值
mkdir :创建指定目录
symlink : 创建连接到 的 符号链接;
write : 向文件 path 中写入字符串;
exec: fork 并执行,会阻塞 init 进程直到程序完毕;
exprot :设定环境变量;
loglevel :设置 log 级别
hostname : 设置主机名
import :导入一个额外的 init 配置文件

服务 Service

语法格式

service <name> <pathname> [ <argument> ]*
     <option>
     <option>
     ...
  • name 表示 service 的名字
  • pathname 表示 service 所在路径,此处的 service 是可执行文件,所以一定有存储路径
  • argument 启动 service 所带的参数
  • option 对此 service的约束选项

选项 Option

Option 用来定义 Service 的行为,决定了 Service 将在何时启动,如何运行等。常用的 Option 有包括以下一些。

  • critical 关键服务,如果在四分钟内退出超过四次,手机将会重启并进入 recovery 模式。
  • disabled 这种类型的服务不会自动启动。它必须明确的使用名字启动
  • oneshot 当服务重启时执行相应命令
  • socket 创建名为/dev/socket/ name 的 socket
  • onrestart 当服务重启时执行相应命令

default: 意味着disabled=false,oneshot=false,critical=false。

rc 解析函数 LoadBootScripts
platform\system\core\init\init.cpp

static void LoadBootScripts(ActionManager& action_manager, ServiceList& service_list) {
    Parser parser = CreateParser(action_manager, service_list);
 
    std::string bootscript = GetProperty("ro.boot.init_rc", "");
    if (bootscript.empty()) {
        parser.ParseConfig("/init.rc");
        if (!parser.ParseConfig("/system/etc/init")) {
            late_import_paths.emplace_back("/system/etc/init");
        }
        if (!parser.ParseConfig("/product/etc/init")) {
            late_import_paths.emplace_back("/product/etc/init");
        }
        if (!parser.ParseConfig("/product_services/etc/init")) {
            late_import_paths.emplace_back("/product_services/etc/init");
        }
        if (!parser.ParseConfig("/odm/etc/init")) {
            late_import_paths.emplace_back("/odm/etc/init");
        }
        if (!parser.ParseConfig("/vendor/etc/init")) {
            late_import_paths.emplace_back("/vendor/etc/init");
        }
    } else {
        parser.ParseConfig(bootscript);
    }
}

如果没有特殊配置ro.boot.init_rc,则解析./init.rc把 /system/etc/init,/product/etc/init,/product_services/etc/init,/odm/etc/init,/vendor/etc/init 这几个路径加入 init.rc 之后解析的路径,在 init.rc 解析完成后,解析这些目录里的 rc 文件.

注意 Android7.0 后,init.rc 进行了拆分,每个服务都有自己的 rc 文件,他们基本上都被加载到 /system/etc/init,/vendor/etc/init,/odm/etc/init 等目录,等 init.rc 解析完成后,会来解析这些目录中的 rc 文件,进而执行相关的动作。

解析 rc 文件的时候,系统会按顺序把相关 Action 加入触发器队列,顺序为 early-init -> init -> late-init.然后在循环中,执行所有触发器队列中 Action 带 Command 的执行函数。

am.QueueEventTrigger("early-init");
am.QueueEventTrigger("init");
am.QueueEventTrigger("late-init");
...
while (true) {
if (!(waiting_for_prop || Service::is_exec_service_running())) {
            am.ExecuteOneCommand();
        }
}

3.5.4 死循环

while (true) {
        // By default, sleep until something happens.
        auto epoll_timeout = std::optional<std::chrono::milliseconds>{}; 
        if (do_shutdown && !shutting_down) {
            do_shutdown = false;
            if (HandlePowerctlMessage(shutdown_command)) {
                shutting_down = true;
            }
        } 
        //依次执行每个action中携带command对应的执行函数
        if (!(waiting_for_prop || Service::is_exec_service_running())) {
            am.ExecuteOneCommand();
        }
        if (!(waiting_for_prop || Service::is_exec_service_running())) {
            if (!shutting_down) {
                auto next_process_action_time = HandleProcessActions(); 
                // If there's a process that needs restarting, wake up in time for that.
                if (next_process_action_time) {
                    epoll_timeout = std::chrono::ceil<std::chrono::milliseconds>(
                            *next_process_action_time - boot_clock::now());
                    if (*epoll_timeout < 0ms) epoll_timeout = 0ms;
                }
            } 
            // If there's more work to do, wake up again immediately.
            if (am.HasMoreCommands()) epoll_timeout = 0ms;
        } 
        // 循环等待事件发生
        if (auto result = epoll.Wait(epoll_timeout); !result) {
            LOG(ERROR) << result.error();
        }
    }

进入无限循环,监听 epoll 以实现对注册的句柄进行实时监控,用来处理对系统属性值的修改和处理子进程的 SIGCHLD 信号.

四 总结

init 进程第一阶段做的主要工作是挂载分区,创建设备节点和一些关键目录,初始化日志输出系统,启用SELinux安全策略。
init 进程第二阶段主要工作是初始化属性系统,解析 SELinux 的匹配规则,处理子进程终止信号,启动系统属性服务,可以说每一项都很关键,如果说第一阶段是为属性系统,SELinux 做准备,那么第二阶段就是真正去把这些功能落实。init 进行第三阶段主要是解析 init.rc 来启动其他进程,进入无限循环,进行子进程实时监控。

你可能感兴趣的:(Android,framework)