ARTICLE · 1103851
深入剖析安卓的开机从BootLoader到Zygote整个过程
背景
对于任何操作系统来讲,开机时间的优化都是一个很关键的工作。如果用户每次启动设备都需要等待很长的时间,那么其用户体验是很差的。
但是进行开机时间优化都需要深入理解整个安卓系统开机各个的过程源码,刚好找到一篇以前老同事分享过的开机优化相关文章,这里分享给大家,再次感谢老同事的干货分享,我们一起来学习安卓的开机从BootLoader到Zygote整个过程。
1. Android开机过程
要想进行开机速度的优化,首先需要了解开机的详细流程。开机过程从CPU上电开始,到锁屏界面显示出来结束。下图为比较简洁的开机流程图。

boot_simple.png
再来看一张比较详细的开机流程图

boot_flow_all_3.png 总的来说,开机过程分为以下六主要子过程
1.1 Boot ROM
Boot ROM是硬编码在CPU内部固定地址的一段ROM(在一些较老的系统上也可能使用外挂Boot ROM(相对CPU来说)),这块代码是由CPU制造商提供。当用户按下电源键或者系统重启之后,触发CPU上电动作,此时其它硬件还未初始化,然而这块ROM就已经可读了。CPU首先执行PBL(Primary Boot Loader,主引导加载程序,固化在ROM上)代码。在必要的硬件初始化之后,Boot ROM开始加载Bootloader到RAM中,然后PC指针跳过去执行bootloader。在加载 Bootloader之前,PBL也可以进行验证。如果验证无法通过,则不会加载运行Bootloader,从而开机失败。

Android_boot_1_boot_ROM.png A. CPU刚上电时,CPU 处于未初始化状态,还没有设定内部时钟,此时只有内部 RAM 可用。当电源稳定后会开始执行 Boot ROM 代码。Boot ROM通过系统寄存器映射到 ASIC (Application Specific Integrated Circuit, 即专用集成电路,是指应特定用户要求和特定电子系统的需要而设计、制造的集成电路)中的物理区域来找到boot media,进而可以找到Bootloader B. boot media序列确定之后,Boot ROM 加载 Bootloader到内部 RAM 中,之后Boot ROM代码会跳到Bootloader
1.2 Bootloader
Bootloader是一个特殊的独立于内核的程序,是CPU复位后进入操作系统之前执行的一段代码。Bootloader完成由硬件启动到操作系统启动的过渡,从而为操作系统提供基本的运行环境,如初始化CPU、时钟、堆栈、存储器系统等。Bootloader功能类似于PC机的BIOS程序,其代码与CPU芯片的内核结构、具体型号、应用系统的配置及使用的操作系统等因素有关,因此不可能有通用的bootloader,开发时需要用户根据具体情况进行移植。嵌入式Linux系统中常用的Bootloader有armboot、redboot、blob、U-Boot、Bios-lt、Bootldr等,其中U-Boot是当前比较流行,功能比较强大的Bootloader,可以支持多种体系结构,但相对也比较复杂。硬件初始化完成之后,Bootloader将boot.img(kernel + ramdisk(ramdisk.img中主要是存放android启动后第一个用户进程init可执行文件和init.*.rc等相关启动脚本以及sbin目录下的adbd工具))从flash上copy到RAM里面,然后CPU执行转向kernel。
Android_boot_2_bootloader.png A. Bootloader第一阶段首先会检测和设置外部RAM B. 外部 RAM可用之后,将Bootloader代码加载到外部RAM中 C. Bootloader第二阶段包含了设置文件系统,内存,网络等等。 D. Bootloader查找Linux内核并将其从boot media (或者其他地方,这取决于系统配置) 加载到 RAM 中,并且会配置一些内核启动时需要的启动参数 E. Bootloader执行完之后会跳转到 Linux 内核执行 一般也可将Bootloader程序的执行分为两个阶段,如下图所示
bootloader_1.png
执行Bootloader程序过程中,如果镜像验证失败、BootLinux (&Info) 函数启动失败或者接收到启动至 fastboot 的命令(比如使用 adb reboot bootloader进行重启、在启动时按下了电源键+下音量键组合)时,会进入到Fastboot模式(Fastboot 是一种电脑通过USB数据线对手机固件进行刷写、擦除/格式化、调试、传输各种指令的固件通信协议, 俗称线刷模式或快速引导模式)。
1.3 Kernel
Android kernel基于上游 Linux LTS (Linux Long Term Supported,长期支持) 内核。在 Google,LTS 内核会与 Android 专用补丁结合,形成所谓的“Android 通用内核 (ACK,Android Common Kernel)”。较新的 ACK(版本 5.4 及更高版本)也称为 GKI (Generic Kernel Image,通用内核镜像 )内核。 GKI项目通过统一核心内核并将 SoC 和板级支持从核心内核移至可加载模块中,解决了内核碎片化问题。GKI 内核为内核模块提供了稳定的内核模块接口 (KMI),因此模块和内核可以独立进行更新。GKI 具有以下特点:
基于 ACK 来源构建而成。 是每个架构和每个 LTS 版本的单内核二进制文件以及关联的可加载模块(目前只有适用于 android11-5.4 和 android12-5.4 的 arm64)。 已经过关联 ACK 支持的所有 Android 平台版本的测试。在 GKI 内核版本的生命周期内不会发生功能弃用。 为给定 LTS 中的驱动程序提供了稳定版 KMI。 不包含 SoC 专用代码或板卡专用代码。下图显示了 GKI 内核和供应商模块架构:
generic-kernel-image-architecture.png
由于Android的kernel实际上就是Linux kernel,只是针对移动设备做了一些优化,所以与其它Linux kernel的启动方式大同小异,都是对start_kernel函数的调用和执行。Kernel主要工作内容为设置缓存、被保护存储器、计划列表,加载驱动,启动kernel守护,挂载根目录,初始化输入输出,开启中断,初始化进程表等。当内核完成这些系统设置后,接下来在系统文件中寻找”init”文件,然后启动root进程或者系统的第一个进程。 Kernel启动过程分为两个阶段:
1)内核引导阶段。通常使用汇编语言编写,主要检查内核与当前硬件是否匹配。这部分也与硬件体系结构相关。
2)内核启动阶段。引导阶段结束前,将调用start_kernel()进入内核启动阶段。内核启动阶段相关的代码主要位于kernel/init/main.c。
A. 内存管理单元和高速缓存初始化完成之后,系统便可以使用虚拟内存和启动用户空间进程
B. 内核在根目录寻找初始化程序(/system/core/init),执行该程序以启动init进程 Kernel启动的核心函数是start_kernel函数,它完成了内核的大部分初始化工作。这个函数在最后调用了reset_init函数进行后续的初始化。
reset_init函数最主要的任务就是启动内核线程kernel_init。kernel_init函数将完成设备驱动程序的初始化,并调用init_post函数启动用户空间的init进程。到init_post函数为止,内核的初始化已经基本完成。
boot_kernel.png
1.4 init进程
用户空间的第一个进程便是init进程,进程号为1。

当系统启动完成之后,init进程会作为守护进程监视其它进程。在Linux中所有的进程都是由init进程直接或间接fork出来的。在init进程启动的过程中,会相继启动servicemanager(binder服务管理者)、Zygote进程。而Zygote又会创建system_server进程以及app进程。

对于init进程的功能分为4部分:
解析并运行所有的init.rc相关文件 根据rc文件,生成相应的设备驱动节点 处理子进程的终止(signal方式) 提供属性服务的功能 init进程涉及的主要代码文件有
system/core/init/
-main.cpp
-init.cpp
-parser.cpp
/system/core/rootdir/
-init.rc
init进程的入口为main.cpp类的main方法。
// system/core/init/main.cpp
intmain(int argc, char** argv){
#if __has_feature(address_sanitizer)
__asan_set_error_report_callback(AsanReportCallback);
#endif
// 创建设备节点、权限设定等
if(!strcmp(basename(argv[0]), "ueventd")) {
return ueventd_main(argc, argv);
}
if(argc > 1){
// 初始化日志系统
if(!strcmp(argv[1], "subcontext")){
android::base::InitLogging(argv, &android::base::KernelLogger);
const BuiltinFunctionMap function_map;
return SubcontextMain(argc, argv, &function_map);
}
// 2.创建安全增强型Linux(SELinux)
if(!strcmp(argv[1], "selinux_setup")){
return SetupSelinux(argv);
}
// 3.解析init.rc文件、提供服务、创建epoll与处理子进程的终止等
if (!strcmp(argv[1], "second_stage")){
return SecondStageMain(argc, argv);
}
}
// 1.挂载相关文件系统
return FirstStageMain(argc, argv);
}
主要执行了三步
FirstStageMain
SetupSelinux
SecondStageMain
FirstStageMain
init进程启动的第一步,主要是挂载相关的文件系统
// system/core/init/first_stage_init.cpp
intFirstStageMain(int argc, char** argv){
if(REBOOT_BOOTLOADER_ON_PANIC){
InstallRebootSignalHandlers();
}
boot_clock::time_point start_time = boot_clock::now();
std::vector<std::pair<std::string, int>> errors;
#define CHECKCALL(x) \
if (x != 0) errors.emplace_back(#x " failed", errno);
umask(0);
// 创建于挂载相关文件系统
CHECKCALL(clearenv());
CHECKCALL(setenv("PATH", _PATH_DEFPATH, 1));
// Get the basic filesystem setup we need put together in the initramdisk
// on / and then we'll let the rc file figure out the rest.
CHECKCALL(mount("tmpfs", "/dev", "tmpfs", MS_NOSUID, "mode=0755"));
CHECKCALL(mkdir("/dev/pts", 0755));
CHECKCALL(mkdir("/dev/socket", 0755));
CHECKCALL(mount("devpts", "/dev/pts", "devpts", 0, NULL));
#define MAKE_STR(x) __STRING(x)
CHECKCALL(mount("proc", "/proc", "proc", 0, "hidepid=2,gid=" MAKE_STR(AID_READPROC)));
#undef MAKE_STR
// 原始命令不可暴露给没有特权的进程
CHECKCALL(chmod("/proc/cmdline", 0440));
gid_t groups[] = {AID_READPROC};
CHECKCALL(setgroups(arraysize(groups), groups));
CHECKCALL(mount("sysfs", "/sys", "sysfs", 0, NULL));
CHECKCALL(mount("selinuxfs", "/sys/fs/selinux", "selinuxfs", 0, NULL));
// tmpfs已经挂载在/dev下,并且已生成/dev/kmsg,故可以与外界通信
// 初始化日志系统
InitKernelLogging(argv);
// 进入下一步
constchar* path = "/system/bin/init";
constchar* args[] = {path, "selinux_setup", nullptr};
execv(path, const_cast<char**>(args));
// 只有在错误发生的情况下execv()函数才会返回
PLOG(FATAL) << "execv(\"" << path << "\") failed";
return1;
}
主要通过mount挂载对应的文件系统,mkdir创建对应的文件目录,并配置相应的访问权限。 需要注意的是,这些文件只是在应用运行的时候存在,一旦应用运行结束就会随着应用一起消失。 挂载的文件系统主要有四类:
tmpfs: 一种虚拟内存文件系统,它会将所有的文件存储在虚拟内存中。由于tmpfs是驻留在RAM的,因此它的内容是不持久的。断电后,tmpfs 的内容就消失了,这也是被称作tmpfs的根本原因。
devpts: 为伪终端提供了一个标准接口,它的标准挂接点是/dev/pts。只要pty(pseudo-tty, 虚拟终端)的主复合设备/dev/ptmx被打开,就会在/dev/pts下动态的创建一个新的pty设备文件。
proc: 也是一个虚拟文件系统,它可以看作是内核内部数据结构的接口,通过它我们可以获得系统的信息,同时也能够在运行时修改特定的内核参数。
sysfs: 与proc文件系统类似,也是一个不占有任何磁盘空间的虚拟文件系统。它通常被挂接在/sys目录下。 在FirstStageMain还会通过InitKernelLogging(argv)来初始化log日志系统。
此时Android还没有自己的系统日志,采用kernel的log系统,打开的设备节点/dev/kmsg, 那么可通过cat /dev/kmsg来获取内核log。
最后会通过execv方法传递对应的path与下一阶段的参数selinux_setup。SetupSelinux
// system/core/init/selinux.cpp
intSetupSelinux(char** argv){
//初始化本阶段内核日志
InitKernelLogging(argv);
if (REBOOT_BOOTLOADER_ON_PANIC) {
InstallRebootSignalHandlers();
}
// 初始化 SELinux,加载 SELinux 策略
SelinuxSetupKernelLogging();
SelinuxInitialize();
// 再次调用 main 函数,并传入 second_stage 进入第二阶段
// 而且此次启动就已经在 SELinux 上下文中运行
if (selinux_android_restorecon("/system/bin/init", 0) == -1) {
PLOG(FATAL) << "restorecon failed of /system/bin/init failed";
}
// 进入下一步
constchar* path = "/system/bin/init";
constchar* args[] = {path, "second_stage", nullptr};
execv(path, const_cast<char**>(args));
// execv() only returns if an error happened, in which case we
// panic and never return from this function.
PLOG(FATAL) << "execv(\"" << path << "\") failed";
return1;
}
这阶段主要是初始化 SELinux。SELinux 是安全加强型 Linux,能够很好的对全部进程强制执行访问控制,从而让 Android 更好的保护和限制系统服务、控制对应用数据和系统日志的访问,提高系统安全性。 接下来调用execv进入到最后阶段SecondStageMain。
SecondStageMain
// system/core/init/init.cpp
intSecondStageMain(int argc, char** argv){
SetStdioToDevNull(argv);
// 初始化本阶段内核日志
InitKernelLogging(argv);
// 系统属性初始化
property_init();
// 建立 Epoll
Epoll epoll;
// 注册信号处理
InstallSignalFdHandler(&epoll);
// 加载默认的系统属性
property_load_boot_defaults(load_debug_prop);
// 启动属性服务
StartPropertyService(&epoll);
subcontexts = InitializeSubcontexts();
//加载系统启动脚本"/init.rc"
ActionManager& am = ActionManager::GetInstance();
ServiceList& sm = ServiceList::GetInstance();
LoadBootScripts(am, sm);
// 触发early-init,,init,late-init流程
am.QueueEventTrigger("early-init");
am.QueueEventTrigger("init");
am.QueueBuiltinAction(InitBinder, "InitBinder");
am.QueueEventTrigger("late-init");
//解析启动脚本
while (true) {
// 执行 Action
am.ExecuteOneCommand();
// 还有就是重启死掉的子进程
auto next_process_action_time = HandleProcessActions();
}
}
SecondStageMain的主要工作总结
使用epoll对init子进程的信号进行监听 初始化系统属性,使用mmap共享内存 开启属性服务,并注册到epoll中 加载系统启动脚本"init.rc" 解析启动脚本,启动相关服务 重点介绍下init.rc文件的解析
//system/core/init/init.cpp
staticvoidLoadBootScripts(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);
}
}
通过ParseConfig来解析init.rc配置文件。.rc文件以行为单位,以空格为间隔,以#开始代表注释行。.rc文件主要包含Action、Service、Command、Options、Import,其中对于Action和Service的名称都是唯一的,对于重复的命名视为无效。
init.rc中的Action、Service语句都有相应的类来解析,即ActionParser、ServiceParser。
以下为init.rc配置文件的部分内容。
// system/core/rootdir/init.rc
import /init.environ.rc
import /init.usb.rc
import /init.${ro.hardware}.rc
import /init.${ro.zygote}.rc
import /init.trace.rc
on early-init
start ueventd
mkdir /mnt 0775 root system
on init
mount tmpfs none /sys/fs/cgroup mode=0750,uid=0,gid=1000
mkdir /sys/fs/cgroup/memory 0750 root system
mount cgroup none /sys/fs/cgroup/memory memory
on property:sys.boot_from_charger_mode=1
class_stop charger
trigger late-init
service ueventd /sbin/ueventd
class core
critical
seclabel u:r:ueventd:s0
service logd /system/bin/logd
class core
socket logd stream 0666 logd logd
socket logdr seqpacket 0666 logd logd
socket logdw dgram 0222 logd logd
seclabel u:r:logd:s0
service console /system/bin/sh
class core
console
disabled
user shell
seclabel u:r:shell:s0
service adbd /sbin/adbd --root_seclabel=u:r:su:s0
class core
socket adbd stream 660 system system
disabled
seclabel u:r:adbd:s0
service servicemanager /system/bin/servicemanager
class core
user system
group system
critical
onrestart restart healthd
onrestart restart zygote
onrestart restart media
onrestart restart surfaceflinger
onrestart restart drm
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的配置中,在late-init阶段启动了Zygote进程。
更多vip干货独享知识讨论,面试题讨论,及面试定制指导上岸fw工程师服务,课程优惠购买成为vip学员进入vip群,积极讨论各种行业难点痛点疑难问题,答疑服务等。
请联系马哥微信:
