0号进程是内核初始化的关键,分为引导阶段和启动阶段。引导阶段以汇编语言为主,最终会跳转到C语言的start_kernel函数。该函数间接执行了init二进制文件,从而创建了init进程。
init进程作为用户空间进程,负责很多初始化工作,包括创建目录、设置访问权限、挂载文件系统、执行安全策略、加载解析rc脚本等。
epoll是一种IO多路复用机制,用于处理高并发场景。init进程使用epoll进行事件通知处理,如检测子进程信号、属性服务唤醒信号等。
本文重点介绍了init进程的相关内容,其他进程如zygote和kthreadd虽然也很重要,但不在本文的讨论范围内。
前言
本篇内容会适度发散,浅浅涉及到Kernel的源码,这对于了解Android系统的开端是有帮助的。Kernel代码位于Google仓库(https://android.googlesource.com/kernel)中,可以直接git下载。打开之后可以看到很多目录,其中:common(Android通用内核)、mediatek(MTK平台内核)、msm(高通平台内核)。如果对内核感兴趣的伙伴,可以参照官网文档(https://source.android.com/source/building-kernels)下载完整代码并编译。同时也可以在线浏览Kernel源码(https://cs.android.com/android/kernel/superproject)。借用Gityuan(https://gityuan.com/android/)的一张系统启动架构图:
从上图中可以看到,开机之后会首先加载Boot Loader。Boot Loader通过一系列指令将内核初始化代码拷贝到内存中并交给CPU执行。其中就包含静态初始化0号进程的代码。此时它还叫做init_task,等它完成各种init工作之后,就会变为一个idle进程,在CPU没有进程需要运行时就运行它(空转)。0号进程的工作分为两个阶段:引导阶段和启动阶段。引导阶段与设备强相关,基本为汇编语言,我们跳过,直接从启动阶段的start_kernel函数开始。kernel\common\init\main.c (https://android.googlesource.com/kernel/common/+/refs/heads/android-mainline/init/main.c)。// kernel\common\init\main.c
void start_kernel(void)
{
... ...
mm_core_init(); //内存管理初始化
... ...
sched_init(); //进程调度器初始化
... ...
rest_init(); //剩下的初始化,这个函数中创建了Init(pid=1)和kthreadd(pid=2)两个进程
}
static noinline void __ref __noreturn rest_init(void)
{
... ...
// 创建Init(pid=1),用户态,kernel_init为函数指针
pid = user_mode_thread(kernel_init, NULL, CLONE_FS);
... ...
// 创建kthreadd(pid=2),内核态,此进程的创建不属于本系列范围,跳过
pid = kernel_thread(kthreadd, NULL, NULL, CLONE_FS | CLONE_FILES);
... ...
}
以上代码中kernel_init为函数指针,因此我们看看这个函数做了什么操作。static char *ramdisk_execute_command = "/init";
... ...
static int __ref kernel_init(void *unused)
{
... ...
if (ramdisk_execute_command) {
//运行一个可执行程序
ret = run_init_process(ramdisk_execute_command);
if (!ret)
return 0;
pr_err("Failed to execute %s (error %d)\n",
ramdisk_execute_command, ret);
}
... ...
}
run_init_process中会通过系统调用,去到内核中的[bprm_execve函数](https://cs.android.com/android/kernel/superproject/+/common-android-mainline:common/fs/exec.c;l=1832;drc=96990c4b4d595715a893857b89e898fe26c32907;bpv=0;bpt=1)执行二进制文件。这个内核函数会启动新的进程,并以二进制文件中的main函数为起点在新进程中开始执行。
ramdisk_execute_command这个字符串指向的正是AOSP代码中system/core/init模块编译出来的二进制文件:init。如果你的手机root过,就能看到系统根目录下有init程序的链接,指向/system/bin/init。
铺垫了一圈之后,终于来到AOSP代码中,开始之前,先生成工程配置文件,方便加载到IDE中。aidegen system/core/init -n -s -i s
上一节提到最终运行了init可执行文件,那么就从它的main函数开始吧。// system\core\init\main.cpp
int main(int argc, char** argv) {
... ...
//调用init时传了参数
if (argc > 1) {
... ...
if (!strcmp(argv[1], "selinux_setup")) {
return SetupSelinux(argv);
}
if (!strcmp(argv[1], "second_stage")) {
return SecondStageMain(argc, argv);
}
}
//调用init时没有传参
return FirstStageMain(argc, argv);
}
argc表示参数数目,argv是一个字符串数组存储具体的参数。有参数的话,argc不应该是大于0吗?为什么是大于1?因为argc表示的参数数目包含了程序路径,同样argv字符串数组也保存了程序路径,因此argv[0]是程序路径,argv[1]才是第一个参数。
0号进程启动init程序的时候没有传参,因此会进入FirstStageMain。// system\core\init\first_stage_init.cpp
int FirstStageMain(int argc, char** argv) {
... ...
//经历了一些列的目录创建和Mount挂载之后
const char* path = "/system/bin/init";
const char* args[] = {path, "selinux_setup", nullptr};
... ...
execv(path, const_cast<char**>(args));
... ...
}
execv函数会再次执行init程序的main函数并带上指定参数。
又一次执行了init,还带了参数。这一次该轮到SetupSelinux函数了。// system\core\init\selinux.cpp
int SetupSelinux(char** argv) {
// 执行一系列的安全策略初始化操作,严格控制进程的访问权限,保证系统安全性
... ...
const char* path = "/system/bin/init";
const char* args[] = {path, "second_stage", nullptr};
execv(path, const_cast<char**>(args));
... ...
}
// system\core\init\init.cpp
int SecondStageMain(int argc, char** argv) {、
... ...
// 设置1号进程init进程的优先级
// DEFAULT_OOM_SCORE_ADJUST为-1000,意味着这个进程将永远不会被杀死
if (auto result =
WriteFile("/proc/1/oom_score_adj", StringPrintf("%d", DEFAULT_OOM_SCORE_ADJUST));
!result.ok()) {
LOG(ERROR) << "Unable to write " << DEFAULT_OOM_SCORE_ADJUST
<< " to /proc/1/oom_score_adj: " << result.error();
}
... ...
// ① 通过系统调用在内核中创建一个eventpoll
Epoll epoll;
if (auto result = epoll.Open(); !result.ok()) {
PLOG(FATAL) << result.error();
}
... ...
// ② init进程注册SignalFd,监听子进程信号,在子进程终止时执行回调,清除子进程相关资源
InstallSignalFdHandler(&epoll);
// init进程注册wake_main_thread_fd,监听来自属性服务的唤醒信号
InstallInitNotifier(&epoll);
... ...
// 启动属性服务,类似与Windows中的注册表,存储系统属性键值对
// setprop设置的属性就是被它管理着
StartPropertyService(&property_fd);
... ...
// ③ 构建rc脚本解析器,并加载rc文件
ActionManager& am = ActionManager::GetInstance();
ServiceList& sm = ServiceList::GetInstance();
LoadBootScripts(am, sm);
... ...
while (true) {
... ...
// 检测是否有关机或重启操作
auto shutdown_command = shutdown_state.CheckShutdown();
... ...
if (!(prop_waiter_state.MightBeWaiting() || Service::is_exec_service_running())) {
// 执行rc中的一个Action
am.ExecuteOneCommand();
}
... ...
}
}
① epoll是一种IO多路复用机制,可以用于有高并发需求的场景。典型的实现就有socket通信场景,以及此处用作事件通知处理的场景。Linux下的epoll机制相当灵活,通常将自己关心的文件描述符扔进去并带上我们的回调,当FD变化时我们就会收到回调。值得注意的是程序运行到当前行时仅仅是在内核中创建了一个事件通知机制的实例,还没有注册关注的事件。这里创建的epoll只是内核空间中eventpoll实例在用户空间中的代理,其所有方法最终都通过系统调用操作内核空间中的实例。这种模式在整个系统中屡见不鲜,用户空间到内核空间,Java对象到Native对象,无一不是这种”空壳”代理模式,隐藏细节,暴露接口。② init的子进程在终止时内核会写SignalFd,此时init进程就会收到消息,清理子进程留下的痕迹,释放资源。③ init进程使用rc脚本配置需要在启动阶段做的事情,这些事情非常繁杂且有些依赖性很强,因此使用脚本的方式配置既灵活又井井有条不容易出错。rc将启动过程中的事情分为3类:• Import(引入):引入其他rc文件,可以理解为其他rc的所有行都将在import那一行被插入。• Action(动作):在什么条件下做什么动作。# 在启动最开始阶段,向一个系统文件里写0
on early-init
write /proc/sys/kernel/sysrq 0
# 同时,也可以在Action中触发其他Action,比如:
on property:sys.boot_from_charger_mode=1
class_stop charger
# 触发了另外一个名叫late-init的动作
trigger late-init
• Service(进程):描述一个进程或者说程序,它的入口函数是什么?挂掉之后是否需要重启?运行时机等等。# 比如:名称为zygote的程序,二进制文件路径如下,且入口为main函数
service zygote /system/bin/app_process64
class main
rc语法很简单且贴近人类语言,用到时查一查,这里就不展开了。其官方解释在system/core/init/README.md文件中,另外推荐Android系统开发进阶-init.rc详解介绍的很详细。
https://qiushao.net/2020/03/01/Android%E7%B3%BB%E7%BB%9F%E5%BC%80%E5%8F%91%E8%BF%9B%E9%98%B6/init.rc%E4%BB%8B%E7%BB%8D/index.html
• 0号进程,分为引导阶段和启动阶段,引导阶段多为汇编语言,最终汇编语言会跳转到C语言函数start_kernel,这个函数间接执行了init二进制文件,从而创建了init进程。
• 1号进程,即init进程,作为用户空间进程,它负责的初始化工作很多,包括:- 1. FirstStageMain阶段:创建目录,并设置访问权限,挂载文件系统。
- 2. SetupSelinux阶段:执行安全策略初始化操作,严格控制进程的访问权限,保证系统安全性。
- 3. SecondStageMain阶段:创建epoll实例,注册子进程信号和属性服务唤醒信号,加载解析rc脚本,进入循环后,随时监测是否有关机或重启的操作,执行rc脚本中的操作。
• 2号进程,即kthreadd,作为内核空间进程,管理内核中所有线程,是他们的父进程。不属于本系列范围暂且不深入研究。本篇着重梳理了启动过程中init进程的前世今生,但其中有很多细节,比如:epoll事件通知机制、大名鼎鼎的binder驱动是在何时加载的都值得专门研究。下一篇开始接力棒将交给zygote,又一个如雷贯耳的进程。参考
内核初始化(https://ty-chen.github.io/linux-kernel-zero-process/)
Android系统启动流程之Linux内核(https://github.com/foxleezh/AOSP/issues/3)
Linux操作系统学习笔记(三)内核初始化(https://ty-chen.github.io/linux-kernel-zero-process/)
framework | init进程流程分析(https://juejin.cn/post/7135358027913232421)
Android系统开发进阶-init.rc详解(https://qiushao.net/2020/03/01/Android%E7%B3%BB%E7%BB%9F%E5%BC%80%E5%8F%91%E8%BF%9B%E9%98%B6/init.rc%E4%BB%8B%E7%BB%8D/index.html)
Epoll原理深入分析(https://rebootcat.com/2020/09/26/epoll_cookbook/)
Linux fd 系列 — eventfd 是什么?(https://juejin.cn/post/6989608237226000391)
最后推荐一下我做的网站,玩Android: wanandroid.com ,包含详尽的知识体系、好用的工具,还有本公众号文章合集,欢迎体验和收藏!
扫一扫 关注我的公众号
如果你想要跟大家分享你的文章,欢迎投稿~
┏(^0^)┛明天见!