阅读源码的文章会是一个系列,本篇主要内容是 Android 源码中启动流程的第一部分,包含了 Linux 内核启动部分与 Android init 进程启动部分。html
为何我会先提 Linux 的启动呢?一方面 Linux 内核是 Android 平台的基础,另外一方面我最近接触了一些 Linux 的基础知识,因此但愿把这些学到的东西也都记录下来。linux
内核的做用其实就是控制计算机硬件资源并提供程序运行环境,具体的好比有:执行程序、文件操做、内存管理及设备驱动等,而内核对外提供的接口也被称为系统调用。android
既然内核这么重要,提供了各类程序运行所需的服务,那启动 Android 前确定是须要先把内核启动起来的。具体内核如何启动,咱们先来看看当咱们按下开机键后都发生了什么。git
计算机通电后首先会去找 ROM(只读内存),这里面被固化了一些初始化程序,这个程序也叫 BIOS,具体几步就像下面这样:github
读取 BIOS(基本输入输出系统,放在 ROM 中):shell
主引导记录(BIOS 中把控制权交给启动顺序的第一位):json
经过 boot loader 启动操做系统:安全
而若是你熟悉 Linux,你就会知道 Linux 启动的入口函数是 start_kernel(在 init/main.c 中),它里面都作了什么比较重要的事情呢:bash
上面提到 1 号进程,也叫 init 进程,而建立 1 号 init 进程时就会执行 Android 源码中 system/core/init 下面的 main.cpp 了,它里面会根据不一样的参数调用不一样的方法:socket
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。
因此下面分开来看一下,这三个部分都作了作了什么:
// 文件位置: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 阶段。
// 文件位置: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 阶段。
// 文件位置: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 是什么?它是很是重要的配置文件,并且众多 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 源码,本篇文章讨论的内容比较有限,其中还有不少细节的东西并无讨论到,好比:
不事后续部分,好比 zygote 我会尽可能在下次读完以后分享出来的。
若是你问我我读完这些有什么收获,我以为下面这三点是个人主要收获:
以上内容,除了源码自己外,还参考了如下连接(顺序不分前后):
07 | 从BIOS到bootloader:创业伊始,有活儿老板本身上
Linux下0号进程的前世(init_task进程)此生(idle进程)----Linux进程的管理与调度(五)
深刻研究源码:Android10.0系统启动流程(二)init进程
源码自己没有歧义,不过因为每一个人基础不一样,具体理解起来可能会些不一样,因此有什么问题,也请你们多指点,多交流。
若是你以为我写的还不错的话,那就经过点赞,点赞,还 tm 是点赞的方式给我反馈吧,感谢你的支持。