Android系统启动系列2 init进程

it2023-07-25  78

一 概述

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),来开启操作系统的正式运行。如下图所示: 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); }

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 策略

下面来看第二阶段的操作.

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 在子进程结束时获取子进程的结束码,通过结束码将程序表中的子进程移除,防止成为僵尸进程的子进程占用程序表的空间.

子进程重启流程如下图所示: 信号处理主要工作如下:

初始化信号 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 进行信号处理.

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 的名称都是唯一的,对于重复的命名视为无效。 动作 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 <service_class_name>: 启动属于同一个 class 的所有服务; class_stop <service_class_name> : 停止指定类的服务 start <service_name>: 启动指定的服务,若已启动则跳过; stop <service_name>: 停止正在运行的服务 setprop :设置属性值 mkdir :创建指定目录 symlink <sym_link>: 创建连接到 的 <sym_link> 符号链接; 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 的 socketonrestart 当服务重启时执行相应命令

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 来启动其他进程,进入无限循环,进行子进程实时监控。

最新回复(0)