目录
本篇主要内容是 Android 源码中启动流程的第一部分,包含了 Linux 内核启动部分与 Android init 进程启动部分。
Linux 内核启动
为什么我会先提 Linux 的启动呢?一方面 Linux 内核是 Android 平台的基础,另一方面我最近接触了一些 Linux 的基础知识,所以希望把这些学到的东西也都记录下来。
内核的作用其实就是控制计算机硬件资源并提供程序运行环境,具体的比如有:执行程序、文件操作、内存管理及设备驱动等,而内核对外提供的接口也被称为系统调用。
既然内核这么重要,提供了各种程序运行所需的服务,那启动 Android 前肯定是需要先把内核启动起来的。具体内核如何启动,我们先来看看当我们按下开机键后都发生了什么。
计算机通电后首先会去找 ROM(只读内存),这里面被固化了一些初始化程序,这个程序也叫 BIOS,具体几步就像下面这样:
读取 BIOS(基本输入输出系统,放在 ROM 中):
- 硬件自检,也就是检查计算机硬件是否满足运行的基本条件;
- 这个程序中查看启动顺序,当然这个可以自行调整,这时就按照启动顺序去找下一阶段的启动程序在哪里;
主引导记录(BIOS 中把控制权交给启动顺序的第一位):
- 读取该设备的第一个扇区的前 512 字节,如果以特定字符结束,就说明这个设备可以用于启动,如果不是就按照刚才 BIOS 中的启动顺序将控制权交给下一个设备,这最前面 512 字节也叫主引导记录(MBR);
- MBR 中的 512 字节放不下太多东西,所以它主要是告诉计算机去哪里找操作系统(硬盘上);
- 这时通过 MBR 的分区表在硬盘上找到对应位置;
通过 boot loader 启动操作系统:
- Linux 使用的是 Grub2,它是启动管理器,会将各种 img 加载进来;
- 操作系统内核加载到内存中;
- 之后会创建初始进程(0 / 1 / 2),后面会由一号进程来加载用户态中其他内容;
而如果你熟悉 Linux,你就会知道 Linux 启动的入口函数是 start_kernel(在 init/main.c 中),它里面都做了什么比较重要的事情呢:
- 0 号进程创建(后面会演变成 idle 进程);
- 系统调用初始化;
- 内存管理系统初始化;
- 调度系统初始化;
- 其他初始化:
- 1 号进程创建(用户态);
- 2 号进程创建(内核态);
Android init 进程启动
上面提到 1 号进程,也叫 init 进程,而创建 1 号 init 进程时就会执行 Android 源码中 system/core/init 下面的 main.cpp 了,它里面会根据不同的参数调用不同的方法:
int main(int argc, char** argv) {
// 略一部分
// ueventd 主要用来创建设备节点
if (!strcmp(basename(argv[0]), "ueventd")) {
return ueventd_main(argc, argv);
}
if (argc > 1) {
// 略一部分
// selinux_setup
if (!strcmp(argv[1], "selinux_setup")) {
return SetupSelinux(argv);
}
// second_stage
if (!strcmp(argv[1], "second_stage")) {
return SecondStageMain(argc, argv);
}
}
return FirstStageMain(argc, argv);
}
复制代码
通过对 system/core/init/README.md 的阅读可以知道 main 函数的会执行多次,启动顺序是这样的 FirstStageMain -> SetupSelinux -> SecondStageMain。
所以下面分开来看一下,这三个部分都做了做了什么:
FirstStageMain
// 文件位置:system/core/init/first_stage_init.cpp
int FirstStageMain(int argc, char** argv) {
// ...
// 其实上面省略的基本是挂载文件系统、创建目录、创建文件等操作
// 比如挂载的有:tmpfs、devpts、proc、sysfs、selinuxfs 等
// 把标准输入、标准输出、标准错误重定向到 /dev/null
SetStdioToDevNull(argv);
// 初始化本阶段内核日志
InitKernelLogging(argv);
// ...
// 比如获取 “/” 的 stat(根目录的文件信息结构),还会判断是否强制正常启动,然后切换 root 目录
// 这里做了几件事:初始化设备、创建逻辑分区、挂载分区
DoFirstStageMount();
// ...
// 再次启动 main 函数,只不过这次传入的参数是 selinux_setup
const char* path = "/system/bin/init";
const char* args[] = {path, "selinux_setup", nullptr};
execv(path, const_cast<char**>(args));
}
复制代码
第一阶段更多的是文件系统挂载、目录和文件的创建,为什么要挂载,这样就可以是使用它们了,这些都完成后就再次调用 main 函数,进入 SetupSelinux 阶段。
SetupSelinux
// 文件位置:system/core/init/selinux.cpp
int SetupSelinux(char** argv) {
// 初始化本阶段内核日志
InitKernelLogging(argv);
// 初始化 SELinux,加载 SELinux 策略
SelinuxSetupKernelLogging();
SelinuxInitialize();
// 再次调用 main 函数,并传入 second_stage 进入第二阶段
// 并且这次启动就已经在 SELinux 上下文中运行
const char* path = "/system/bin/init";
const char* args[] = {path, "second_stage", nullptr};
execv(path, const_cast<char**>(args));
}
复制代码
这阶段主要做的就是初始化 SELinux,那什么是 SELinux 呢?其实就是安全增强型 Linux,这样就可以很好的对所有进程强制执行访问控制,从而让 Android 更好的保护和限制系统服务、控制对应用数据和系统日志的访问,降低恶意软件的影响。
不过 SELinux 并不是一次就初始化完成的,接下来就是再次调用 main 函数,进入最后的 SecondStageMain 阶段。
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
// ...
// 清理环境
// 将 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();
}
}
复制代码
这是整个启动阶段最重要的部分,我觉得有四个比较重要的点,它们分别是属性服务、注册信号处理 、init.rc 解析以及方法尾部的死循环。
属性服务
什么是属性服务,我觉得它更像关于这台手机的各种系统信息,通过 key / value 的形式供我们所有程序使用,下面内容就是我的模拟器进入 adb shell 后获取到的属性值,下面我从输出结果里面保留的一部分:
generic_x86:/ $ getprop
...
[dalvik.vm.heapsize]: [512m]
...
[dalvik.vm.usejit]: [true]
[dalvik.vm.usejitprofiles]: [true]
...
[init.svc.adbd]: [running]
...
[init.svc.gpu]: [running]
...
[init.svc.surfaceflinger]: [running]
...
[init.svc.zygote]: [running]
...
[ro.product.brand]: [google]
[ro.product.cpu.abi]: [x86]
...
[ro.serialno]: [EMULATOR29X2X1X0]
[ro.setupwizard.mode]: [DISABLED]
[ro.system.build.date]: [Sat Sep 21 05:19:49 UTC 2019]
...
// zygote 启动该启动哪个
[ro.zygote]: [zygote32]
[ro.zygote.disable_gl_preload]: [1]
[security.perf_harden]: [1]
[selinux.restorecon_recursive]: [/data/misc_ce/0]
...
[wifi.interface]: [wlan0]
复制代码
属性服务相关代码在 SecondStageMain 阶段其实主要做了三件事:创建共享内存、加载各种属性值以及创建属性服务的 Socket。下面是这关于这几部分的片段:
property_init {
// 创建目录 /dev/__properties__
// 会从别的地方加载并解析属性,然后写到 /dev/__properties__/property_info 里
// 在 __system_property_area_init 的调用链跟踪中,发现最终是通过 mmap 创建共享内存
}
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 设置这些属性值
}
StartPropertyService {
// 创建 Sockte
// 这个 Socket 就是用来处理系统属性的,所有进程都通过它来修改共享内存里面的系统属性
property_set_fd = CreateSocket(...);
// 开始注册监听,handle_property_set_fd 是回调处理函数
epoll->RegisterHandler(property_set_fd, handle_property_set_fd);
}
复制代码
代码上了理解起来并不那么难,只是可能要问为什么要使用共享内存?Socket 作用是什么?
首先共享内存是一种高效的进程间通信方式,本身这些属性值在内存中存在一份即可,不需要每个进程都复制一份到自己的空间中,而且由于是共享的,所以谁都能访问。但是如果谁都能随时来读写(除了只读部分的属性),那也还是会出问题,可能会出现内容不一致问题,所以大家并不是直接对共享内存进行操作,而是通过属性服务的 socket 的对其进行操作,这样就避免了所以进程直接对那块共享内存进行操作。
注册信号处理
在 SecondStageMain 阶段,其实就是注册了信号处理函数,从而可以对底层信号作出响应。对应函数是:
InstallSignalFdHandler {
// ...
// 注册信号处理函数
epoll->RegisterHandler(signal_fd, HandleSignalFd);
}
HandleSignalFd {
// ...
// ReapAnyOutstandingChildren 会对死掉的进程进行重启
SIGCHLD -> ReapAnyOutstandingChildren
SIGTERM -> HandleSigtermSignal
default -> 打印日志
}
// 子进程异常退出后要标记需要重新启动
ReapAnyOutstandingChildren {
// ...
ReapOneProcess {
// ...
service.Reap {
// ...
// 设置要重启的标志位,但这里并不是真的启动
flags_ &= (~SVC_RESTART);
flags_ |= SVC_RESTARTING;
onrestart_.ExecuteAllCommands();
}
}
}
复制代码
init.rc 解析
init.rc 是什么?它是非常重要的配置文件,而且众多 rc 文件中 init.rc 是最主要的文件,不过这里我不会讲 rc 文件的语法是怎么样的,因为 system/core/init/README.md 中已经写的很清楚了,init.rc 会根据 on 分成不同阶段,并且由 trigger 进行不同阶段的触发,而每个阶段里面就是一条条要执行指令,比如 start 后面跟的就是要启动的服务,mkdir 就是创建目录。
既然分成了多个阶段,那先来看看触发阶段是怎么样的:
// 这三个阶段是顺序下去的,这三个阶段的触发顺序是写在 SecondStageMain 代码中的
early-init -> init -> late-init
// late-init 中再去触发别的阶段
on late-init
trigger early-fs
trigger fs
trigger post-fs
trigger late-fs
trigger post-fs-data
trigger load_persist_props_action
// 这里就是 zygote-start 启动了
trigger zygote-start
trigger firmware_mounts_complete
trigger early-boot
trigger boot
复制代码
那么下面来看看 init.rc 解析在 SecondStageMain 阶段都做了啥:
// 把这阶段关于 rc 文件相关的一些重要代码提取出来
int SecondStageMain(int argc, char** argv) {
// ...
// 两个用于存储的容器
ActionManager& am = ActionManager::GetInstance();
ServiceList& sm = ServiceList::GetInstance();
// 解析 init.rc
LoadBootScripts(am, sm);
// ...
// 加入触发 early-init 语句
am.QueueEventTrigger("early-init");
// ...
// 加入触发 init 语句
am.QueueEventTrigger("init");
// ...
// 代码中还有很多 QueueBuiltinAction,插入要执行的 Action
am.QueueBuiltinAction(InitBinder, "InitBinder");
// ...
// 加入触发 late-init 语句
am.QueueEventTrigger("late-init");
}
LoadBootScripts(action_manager, service_list) {
Parser parser = CreateParser(action_manager, service_list);
// 系统属性中去找 ro.boot.init_rc 对应的值
std::string bootscript = GetProperty("ro.boot.init_rc", "");
// 没找到的话就去当前目录找 init.rc
// 当前目录就是 system/core/init/
if (bootscript.empty()) {
// 无论没有找到最终解析的任务都是交给 ParseConfig 这个方法去处理
parser.ParseConfig("/init.rc");
// ...
} else {
parser.ParseConfig(bootscript);
}
}
复制代码
其实上面的代码写主要做的就是解析 init.rc 文件中的内容,并且在加入要执行的动作。
方法尾部的死循环
这里面主要做的就是执行刚入 ActionManager 中的动作和看看是否有需要重启的进程。
while (true) {
// ...
// 执行刚才加入 ActionManager 的动作
am.ExecuteOneCommand();
// ...
// HandleProcessActions 才是真正重启进程的地方
auto next_process_action_time = HandleProcessActions();
}
HandleProcessActions {
// ...
// 对需要重启的进行重启,前面会有很多判断
auto result = s->Start();
}
复制代码
到这里大致的 init 进程启动的三个阶段基本上清晰了。
不过由于是我第一次开始阅读 AOSP 源码,本篇文章讨论的内容比较有限,其中还有很多细节的东西并没有讨论到,比如:
- Linux 启动流程的更多详细内容;
- 具体挂载的那些文件是什么,它们都有什么用途;
- 属性服务的完整读写流程是怎么样的;
- 具体 init.rc 如何解析,如何执行;
- zygote 的启动等等;
不过后续部分,比如 zygote 我会尽量在下次读完之后分享出来的。
总结与收获
如果你问我我读完这些有什么收获,我觉得下面这三点是我的主要收获:
- 在某些情况下(比如前期资源不足或者前后依赖),我们可以将大任务拆解,并合理分配好执行次序(包括顺序、串并行安排等等),进而通过多阶段任务的配合从而完成一个整体的执行目标;
- 当资源是共享的时候,最好不要不然所有人都直接对资源进行操作,而是引入中间人,大家只和中间人交互,具体资源由中间人和其交互;
- 代码跑起来很重要,但是一个合理的监控模块也非常需要,这样可以在必要的时候检测出问题并及时作出响应;
感谢与参考
以上内容,除了源码本身外,还参考了以下链接(顺序不分先后):
07 | 从BIOS到bootloader:创业伊始,有活儿老板自己上
Linux下0号进程的前世(init_task进程)今生(idle进程)----Linux进程的管理与调度(五)
深入研究源码:Android10.0系统启动流程(二)init进程
作者:Gzw丶
链接:https://juejin.cn/post/6844903965688250382
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。