前言
本篇内容会适度发散,浅浅涉及到Kernel的源码,这对于了解Android系统的开端是有帮助的。
Kernel代码位于Google仓库中,可以直接git下载。打开之后可以看到很多目录,其中:common(Android通用内核)、mediatek(MTK平台内核)、msm(高通平台内核)。如果对内核感兴趣的伙伴,可以参照官网文档下载完整代码并编译。同时也可以在线浏览Kernel源码。
借用Gityuan的一张系统启动架构图
一切的开端
从上图中可以看到,开机之后会首先加载Boot Loader。Boot Loader通过一系列指令将内核初始化代码拷贝到内存中并交给CPU执行。其中就包含静态初始化0号进程的代码。此时它还叫做init_task,等它完成各种init工作之后,就会变为一个idle进程,在CPU没有进程需要运行时就运行它(空转)。
0号进程的工作分为两个阶段:引导阶段和启动阶段。引导阶段与设备强相关,基本为汇编语言,我们跳过,直接从启动阶段的start_kernel函数开始。
c
// kernel\common\init\main.c
void start_kernel(void)
{
... ...
mm_core_init(); //内存管理初始化
... ...
sched_init(); //进程调度器初始化
... ...
rest_init(); //剩下的初始化,这个函数中创建了Init(pid=1)和kthreadd(pid=2)两个进程
}
c
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为函数指针,因此我们看看这个函数做了什么操作
c
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);
}
... ...
}
ramdisk_execute_command这个字符串指向的正是AOSP代码中system/core/init模块编译出来的二进制文件:init。如果你的手机root过,就能看到系统根目录下有init程序的链接,指向/system/bin/init。
init的main函数
铺垫了一圈之后,终于来到AOSP代码中,开始之前,先生成工程配置文件,方便加载到IDE中。
sh
aidegen system/core/init -n -s -i s
上一节提到最终运行了init可执行文件,那么就从它的main函数开始吧。
cpp
// 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]才是第一个参数。
FirstStageMain
0号进程启动init程序的时候没有传参,因此会进入FirstStageMain。
cpp
// 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函数的实现位于bionic\libc\bionic\exec.cpp中,但实际上文件中只是封装了一下,最终还是通过系统调用,去到内核中的bprm_execve函数执行二进制文件。前面提到的0号进程启动init也是通过这个函数启动的。
SetupSelinux
很明显,又一次执行了init,还带了参数。这一次该轮到SetupSelinux函数了。
cpp
// 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));
... ...
}
SecondStageMain
又又执行了init,也带了参数。
cpp
// 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(动作):在什么条件下做什么动作。
csharp
# 在启动最开始阶段,向一个系统文件里写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(进程):描述一个进程或者说程序,它的入口函数是什么?挂掉之后是否需要重启?运行时机等等。
python
# 比如:名称为zygote的程序,二进制文件路径如下,且入口为main函数
service zygote /system/bin/app_process64
class main
rc语法很简单且贴近人类语言,用到时查一查,这里就不展开了。其官方解释在system/core/init/README.md文件中,另外推荐Android系统开发进阶-init.rc详解介绍的很详细。
总结
-
0号进程,分为引导阶段和启动阶段,引导阶段多为汇编语言,最终汇编语言会跳转到C语言函数start_kernel,这个函数间接执行了init二进制文件,从而创建了init进程。
-
1号进程,即init进程,作为用户空间进程,它负责的初始化工作很多,包括:
- FirstStageMain阶段:创建目录,并设置访问权限,挂载文件系统
- SetupSelinux阶段:执行安全策略初始化操作,严格控制进程的访问权限,保证系统安全性
- SecondStageMain阶段:创建epoll实例,注册子进程信号和属性服务唤醒信号,加载解析rc脚本,进入循环后,随时监测是否有关机或重启的操作,执行rc脚本中的操作。
- 2号进程,即kthreadd,作为内核空间进程,管理内核中所有线程,是他们的父进程。不属于本系列范围暂且不深入研究。
本篇着重梳理了启动过程中init进程的前世今生,但其中有很多细节,比如:epoll事件通知机制、大名鼎鼎的binder驱动是在何时加载的都值得专门研究。下一篇开始接力棒将交给zygote,又一个如雷贯耳的进程。