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