1、引言
众嗦粥汁,无论是电脑还是手机,亦或是其他终端设备,其从上电到完全启动都有一套复杂的流程。本来是打算使用相对简单一些的android4.4版本代码来学习这一整个流程,但是由于一部分工作生产环境从android4.4的kitkak切换到了9.0的pie,所以直接用9.0的代码来学习,学习代码和熟悉9.0环境两不误,哦耶!
拿android系统的设备来说,由于其底层是linux内核,其从开机上电后,会先后经历加载boot、启动kernel等过程。(当然这个过程的流程也是很复杂的,由于对硬件和bootloader相关知识不太了解,所以在这里就不瞎说了。先挖个坑,等以后有相关知识储备后,再回来单独写文章填坑吧。)当kernel启动成功后,会去拉起android系统的1号进程,也就是我们这边文章的主角Init。
//TODO Android从开机上电到拉起init之间的流程学习
声明:文章中的代码是AOSP Android源码,代码路径会在每个代码块的第一行注明。本文是学习各位前辈大佬的博客或相关著作后,对比android源码的学习记录总结,不做任何商务用途。这里以及后续文章中参考或学习过的博文,会尽可能打上链接并且在文章开头逐一列举:
- 《深入理解Android:卷1》—— 邓凡平@著
- 《Android系统源代码情景分析》—— 罗升阳@著
- 《Android 10.0系统启动之init进程-[Android取经之路]》—— IngresGe
- 《Android启动流程——1序言、bootloader引导与Linux启动》—— 简书@隔壁老李头
2、Init的main函数
Init进程作为一个命令行程序,先找到它的main函数,由于该main函数相当长,所以将会根据其中各阶段的功能划分为几个模块逐一解析。
2.1、命令与参数解析
Init作为一个命令行程序,kernel会以带绝对路径的方式启动它。通常来说init可执行文件在目标设备的系统根目录下("/init"),kernel也是优先在该目录下寻找init可执行文件。如果找不到的话,会转而去"/sbin/"等目录 下寻找并启动它。需要说明的时,此时kernel拉起的init进程还处于内核态。
// pie\system\core\init\init.cpp
int main(int argc, char** argv) {
if (!strcmp(basename(argv[0]), "ueventd")) { //以sbin/ueventd"拉起init时,启动ueventd
return ueventd_main(argc, argv);
}
if (!strcmp(basename(argv[0]), "watchdogd")) { //以"sbin/watchdogd"拉起init时,启动watchdogd
return watchdogd_main(argc, argv);
}
if (argc > 1 && !strcmp(argv[1], "subcontext")) { //第二个参数是"subcontext"时
InitKernelLogging(argv); //屏蔽init进程的标准输入、输出、错误输出
const BuiltinFunctionMap function_map;
return SubcontextMain(argc, argv, &function_map); //TODO
}
//开发调试阶段,init发生崩溃时重新加载bootloader,以避免循环加载错误配置
if (REBOOT_BOOTLOADER_ON_PANIC) {
InstallRebootSignalHandlers();
}
}
main函数一上来我就有点看不懂了,大家都知道C/C++程序main函数的argv[0]是命令行程序本身的绝对路径("/init"),通过basename()函数获取到程序名("init")。既然要想启动init程序,也那么basename(argv[0])的值一定是"init"。那为什么程序前两个if逻辑中,会觉得这个值会是"ueventd"或“watchdogd”,并启动相应的ueventd和watchdogd进程呢?
迷惑之际,感觉这应该和编译脚本有关系,果然从init程序的编译脚本中发现了端倪:
// pie\system\core\init\Android.bp
…………
cc_binary {
…………
name: "init",
defaults: ["init_defaults"],
required: [
"e2fsdroid",
"mke2fs",
"sload_f2fs",
"make_f2fs",
],
static_executable: true,
srcs: ["main.cpp"],
symlinks: [
"sbin/ueventd",
"sbin/watchdogd",
],
}
…………
从最后得到symlinks就可以看出,"sbin/ueventd"和"sbin/watchdogd"这两个程序是链接到init程序上的,这一点从目标设备也可以得到验证:
root@xxx:/ # ls -l /sbin/ueventd
lrwxrwxrwx 1 root root 7 1970-01-01 08:00 /sbin/ueventd -> ../init
root@xxx:/ # ls -l /sbin/watchdogd
lrwxrwxrwx 1 root root 7 1970-01-01 08:00 /sbin/watchdogd -> ../init
这样就很好理解了,当我们启动"sbin/ueventd"或者"sbin/watchdogd"的时候,实际上拉起的是init程序,init程序的main函数解析到我们要启动的实际上是ueventd或watchdogd,就会执行相对应的ueventd_main()或watchdogd_main()函数。
回到main函数中,当拉起init程序时所带的参数大于二并且第二个参数为"subcontext"时,先执行InitKernelLogging(argv)语句:
// pie\system\core\init\log.cpp
void InitKernelLogging(char* argv[]) {
int fd = open("/sys/fs/selinux/null", O_RDWR);
if (fd == -1) {
int saved_errno = errno;
android::base::InitLogging(argv, &android::base::KernelLogger, InitAborter);
errno = saved_errno;
PLOG(FATAL) << "Couldn't open /sys/fs/selinux/null";
}
dup2(fd, 0); //屏蔽标准输入
dup2(fd, 1); //屏蔽标准输出
dup2(fd, 2); //屏蔽错误输出
if (fd > 2) close(fd);
//初始化日志系统,设置日志等级
android::base::InitLogging(argv, &android::base::KernelLogger, InitAborter);
}
open()函数以可读可写模式打开文件"/sys/fs/selinux/null",该函数返回的是0~255之间可用文件描述符的最小值。通过学习文章《linux之dup和dup2函数解析》可以知道,启动init进程时,该进程默认会有3个文件描述符存在:
- 0:与进程的标准输入相关联;
- 1:与进程的标准输出相关联;
- 2:与进程的标准错误输出相关联;
代码中三次对dup2()函数的调用,将init进程本来用于标准输入输出的3个文件描述符,全部指向了文件"/sys/fs/selinux/null",即屏蔽了init进程的标准输入输出。简单来说就是init程序中的打印是看不到的,看不到怎么办嘞,看网上说内核空间产生的 log 通过 Linux 内核中的 log 系统进行处理,可以通过 dmesg 和/proc/kmsg 进行访问。另外,可以在/proc/进程pid/fd/目录下看到当前进程有哪些打开的文件描述符,看看init进程的情况以验证我们的说法:
root@xxx:/proc/1/fd # ls -l /proc/1/fd
total 0
lrwx------ 1 root root 64 2021-08-24 17:05 0 -> /sys/fs/selinux/null
lrwx------ 1 root root 64 2021-08-24 17:05 1 -> /sys/fs/selinux/null
lrwx------ 1 root root 64 2021-08-24 17:05 2 -> /sys/fs/selinux/null
lrwx------ 1 root root 64 2021-08-24 17:05 3 -> socket:[2189]
lrwx------ 1 root root 64 2021-08-24 17:05 4 -> anon_inode:[eventpoll]
lrwx------ 1 root root 64 2021-08-24 17:05 5 -> socket:[2190]
lrwx------ 1 root root 64 2021-08-24 17:05 6 -> socket:[2191]
lrwx------ 1 root root 64 2021-08-24 17:05 7 -> socket:[2192]
lrwx------ 1 root root 64 2020-01-01 08:00 8 -> socket:[2201]
lrwx------ 1 root root 64 2021-08-24 17:05 9 -> socket:[2203]
2.2、first stage
main函数在前几个if判断语句之后,逻辑走到了is_first_stage这一部分。
2.2.1、挂载文件系统,创建必须目录、设备节点
参考《linux 文件操作函数 mount-mknod-mkdir》可知,这一部分首先做了如下的事情:
// pie\system\core\init\init.cpp
int main(int argc, char** argv) {
bool is_first_stage = (getenv("INIT_SECOND_STAGE") == nullptr);
if (is_first_stage) {
boot_clock::time_point start_time = boot_clock::now();
umask(0); //清除屏蔽字,保证新建的目录的访问权限不受屏蔽字影响,即给予最大权限
clearenv(); //清空init进程的环境变量
setenv("PATH", _PATH_DEFPATH, 1); //init进程的环境变量中添加:
//PATH=/sbin:/system/sbin:/system/bin:/system/xbin:/odm/bin:/vendor/bin:/vendor/xbin
//将tmpfs文件系统挂载在/dev目录上,命名为tmpfs,设置目录权限为0755
mount("tmpfs", "/dev", "tmpfs", MS_NOSUID, "mode=0755");
mkdir("/dev/pts", 0755); //在/dev目录下创建pts目录,设置目录权限为0755
mkdir("/dev/socket", 0755); //在/dev目录下创建socket目录,设置目录权限为0755
mount("devpts", "/dev/pts", "devpts", 0, NULL); // 将devpts文件系统挂载到/dev/pts目录上
#define MAKE_STR(x) __STRING(x)
mount("proc", "/proc", "proc", 0, "hidepid=2,gid=" MAKE_STR(AID_READPROC)); //将proc文件系统挂载到/proc目录上
chmod("/proc/cmdline", 0440); //缩小Android原始命令可用权限范围
gid_t groups[] = { AID_READPROC }; // #define AID_READPROC 3009 /* Allow /proc read access */
setgroups(arraysize(groups), groups);
mount("sysfs", "/sys", "sysfs", 0, NULL); //将sysfs文件系统挂载到/sys目录上
mount("selinuxfs", "/sys/fs/selinux", "selinuxfs", 0, NULL); //将selinuxfs文件系统挂载到/sys/fs/selinux目录上
mknod("/dev/kmsg", S_IFCHR | 0600, makedev(1, 11)); //创建字符设备文件/dev/kmsg
if constexpr (WORLD_WRITABLE_KMSG) { //开发者模式创建字符设备文件/dev/kmsg_debug
mknod("/dev/kmsg_debug", S_IFCHR | 0622, makedev(1, 11));
}
mknod("/dev/random", S_IFCHR | 0666, makedev(1, 8)); //创建字符设备文件/dev/random
mknod("/dev/urandom", S_IFCHR | 0666, makedev(1, 9)); //创建字符设备文件/dev/urandom
//将tmpfs文件系统也挂载在/mnt目录上,但是对读、写、执行等访问权限有特殊设置
mount("tmpfs", "/mnt", "tmpfs", MS_NOEXEC | MS_NOSUID | MS_NODEV, "mode=0755,uid=0,gid=1000");
mkdir("/mnt/vendor", 0755); //创建/mnt/vendor目录,设置目录权限为0755
…………
}
}
如上所示,该部分主要用于挂载和创建启动所需的文件系统和目录。需要注意的是,在编译Android系统源码时,在生成的根文件系统中, 并不存在这些目录,它们是系统运行时的目录,即当系统终止时,就会消失。
2.2.2、重新启动/init
// pie\system\core\init\init.cpp
int main(int argc, char** argv) {
…………
if (is_first_stage) {
…………
InitKernelLogging(argv); //屏蔽内核态init进程的标准输入、输出、错误输出,初始化kernel logging系统
LOG(INFO) << "init first stage started!";
if (!DoFirstStageMount()) { //分区挂载
LOG(FATAL) << "Failed to mount required partitions early ...";
}
SetInitAvbVersionInRecovery(); //TODO
global_seccomp(); //TODO
SelinuxSetupKernelLogging(); //注册回调,用来设置需要写入kmsg的selinux日志
SelinuxInitialize(); 加载SELinux规则
if (selinux_android_restorecon("/init", 0) == -1) { //按照selinux域规则,将init进程的内核域显式切换到init域
PLOG(FATAL) << "restorecon failed of /init failed";
}
setenv("INIT_SECOND_STAGE", "true", 1); //设置环境变量INIT_SECOND_STAGE=true
static constexpr uint32_t kNanosecondsPerMillisecond = 1e6;
uint64_t start_ms = start_time.time_since_epoch().count() / kNanosecondsPerMillisecond;
setenv("INIT_STARTED_AT", std::to_string(start_ms).c_str(), 1); //记录启动时间到环境变量:INIT_STARTED_AT
char* path = argv[0];
char* args[] = { path, nullptr };
execv(path, args); // 此时在selinux规则的init域,执行C空间的/init程序,使得从内核态切换到用户态
PLOG(FATAL) << "execv(\"" << path << "\") failed"; //内核态的进程不应该退出,执行失败或崩溃时才会走到这里
}
}
总结一下第一阶段,做了这些事情:
- 挂载必需的文件系统,创建必需的目录和设备节点文件;
- 完成一些分区挂载,TODO;
- 加载selinux规则,并将域属性从内核域显式切换到init域;
- 启动C空间的/init程序,从内核态切换到用户态;
2.3、second stage
当用户态的init程序main函数执行进来时,环境变量INIT_SECOND_STAGE被设为true,所以第一阶段的代码将不再执行,转而走到了第二阶段。
// pie\system\core\init\init.cpp
int main(int argc, char** argv) {
…………
InitKernelLogging(argv); 屏蔽内核态init进程的标准输入、输出、错误输出,初始化kernel logging系统
LOG(INFO) << "init second stage started!";
keyctl_get_keyring_ID(KEY_SPEC_SESSION_KEYRING, 1); //设置进程会话密钥
close(open("/dev/.booting", O_WRONLY | O_CREAT | O_CLOEXEC, 0000)); //创建 /dev/.booting文件,表示正在启动到后台fw加载器等
property_init(); //初始化property系统
process_kernel_dt(); //处理DT属性,如果参数同时在命令行和DT中传递,则在DT中设置的属性总是优先于命令行设置的属性
process_kernel_cmdline(); //处理命令行属性
export_kernel_boot_props(); //根据kernel ro.boot.开头的几个属性,将内核属性设置到系统属性
//根据环境变量设置一些属性
property_set("ro.boottime.init", getenv("INIT_STARTED_AT"));
property_set("ro.boottime.init.selinux", getenv("INIT_SELINUX_TOOK"));
const char* avb_version = getenv("INIT_AVB_VERSION");
if (avb_version) property_set("ro.boot.avb_version", avb_version);
//这些环境变量只有init进程才会用到,后续fork其他进程时会复制init环境变量,其他进程用不到这些环境变量。所以前面保存到属性后,就直接清除掉。
unsetenv("INIT_SECOND_STAGE");
unsetenv("INIT_STARTED_AT");
unsetenv("INIT_SELINUX_TOOK");
unsetenv("INIT_AVB_VERSION");
//selinux相关的日志系统设置、上下文环境转换……
SelinuxSetupKernelLogging();
SelabelInitialize();
SelinuxRestoreContext();
epoll_fd = epoll_create1(EPOLL_CLOEXEC); //创建epoll句柄
if (epoll_fd == -1) {
PLOG(FATAL) << "epoll_create1 failed";
}
sigchld_handler_init(); //装载子进程信号处理器
//如果init没有CAP_SYS_BOOT功能,那么它是在容器中运行的。在这种情况下,接收SIGTERM将导致系统关闭。
if (!IsRebootCapable()) {
InstallSigtermHandler();
}
property_load_boot_defaults(); //加载指定文件中的默认属性值
export_oem_lock_status(); //设置相关属性
start_property_service(); //启动property服务,监听处理android系统中的属性操作
set_usb_controller(); //从“/sys/class/ UDC”中读取并设置使用中的UDC控制器
…………
}
这一阶段所作的事如上面代码中的注释所示,除了做一些日志、进程密钥、环境变量、selinux相关的设置外,有两个对系统来说比较关键的东西,那就是属性系统和子进程信号处理相关的内容。
2.3.1、属性系统
关于属性系统,我之前有写过两篇Android4.4 kitkak版本中属性系统的文章,有兴趣的话可以浏览一下:《Android4.4 property机制学习》和《《Android4.4 property机制学习》补充篇——属性树与其在内存中的存储结构》。虽说从4.4到9.0经历的版本变化比较大,但基本原理是没怎么变化的,有时间的话再回来补一下变化的内容。
2.3.2、子进程信号处理
这一部分理解的不是很明白,就不瞎说了,有兴趣可以参考《Android 10.0系统启动之init进程-[Android取经之路]》的第五模块——信号处理,讲的很详细的,后面有更深入的理解后再来补充。
2.4、*.rc文件
Android系统除了Init这个1号进程外,还拥有众多其他的重要进程,这些进程都是直接或间接由init进程fork而来。但一个一个手动去启动这些进程就太捞了,于是有了.rc配置文件的出现,通过配置文件更加合理和方便的控制进程的启动与管理。关于.rc文件的语法,大家可以参考《Android系统init进程启动及init.rc全解析》。
// pie\system\core\init\init.cpp
int main(int argc, char** argv) {
…………
const BuiltinFunctionMap function_map;
Action::set_function_map(&function_map); //在Action类中保存function_map对象,该对象记录了命令与函数之间的对应关系
subcontexts = InitializeSubcontexts(); //初始化上下文
ActionManager& am = ActionManager::GetInstance(); //获取ActionManager实例
ServiceList& sm = ServiceList::GetInstance(); //获取ServiceList实例
LoadBootScripts(am, sm); // 加载.rc配置文件,将Action和Service读取到对应的Queue和List中去
if (false) DumpState();
am.QueueEventTrigger("early-init"); //触发Action队列中以early-init为触发器的那些Action
am.QueueBuiltinAction(wait_for_coldboot_done_action, "wait_for_coldboot_done");
am.QueueBuiltinAction(MixHwrngIntoLinuxRngAction, "MixHwrngIntoLinuxRng");
am.QueueBuiltinAction(SetMmapRndBitsAction, "SetMmapRndBits");
am.QueueBuiltinAction(SetKptrRestrictAction, "SetKptrRestrict");
am.QueueBuiltinAction(keychord_init_action, "keychord_init");
am.QueueBuiltinAction(console_init_action, "console_init");
am.QueueEventTrigger("init");//触发Action队列中以init为触发器的那些Action,主要是触发所有boot Action
am.QueueBuiltinAction(MixHwrngIntoLinuxRngAction, "MixHwrngIntoLinuxRng");
std::string bootmode = GetProperty("ro.bootmode", "");
if (bootmode == "charger") { //如果是关机充电模式,则触发charger,否则正常启机模式下触发late-init
am.QueueEventTrigger("charger"); //charger模式不挂载文件系统或启动核心系统服务
} else {
am.QueueEventTrigger("late-init");
}
//此时系统属性已经加载完毕,所以触发.rc文件中以属性值为触发器的那些Action
am.QueueBuiltinAction(queue_property_triggers_action, "queue_property_triggers");
…………
}
如上所示,这里主要就是去解析.rc文件,将.rc文件中的Action顺序读取到Queue中,将Service读取到List中,然后通过触发器去启动这些Action和Service。需要说明的是,这里的触发只是根据对应的阶段事件,将对应Action顺序存入队列中,真正执行这些Action的动作在下面的2.5模块。这里先看一下LoadBootScripts()函数:
//pie\system\core\init\init.cpp
static void LoadBootScripts(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("/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);
}
}
其首先通过CreateParser()函数创建parser用于解析.rc文件,具体来说就是通过AddSectionParser()函数,根据rc文件的语法规则,将不同模块语句的首个单词字符串,与其对应类型的parser解析器实例建立key-value的键值关系,存储在std::map类型的关联容器中,用于后续对rc文件不同类型语句的解析。(这里的键值对应关系,指rc文件中出现在文件头部用于导入其他rc文件的import语句、Action结构首部出现的on语句、Service结构首部出现的service语句,与它们对应的解析类实例之间的对应关系。)
// pie\system\core\init\parser.h
std::map<std::string, std::unique_ptr<SectionParser>> section_parsers_;
// pie\system\core\init\parser.cpp
void Parser::AddSectionParser(const std::string& name, std::unique_ptr<SectionParser> parser) {
section_parsers_[name] = std::move(parser);
}
// pie\system\core\init\init.cpp
Parser CreateParser(ActionManager& action_manager, ServiceList& service_list) {
Parser parser;
parser.AddSectionParser("service", std::make_unique<ServiceParser>(&service_list, subcontexts));
parser.AddSectionParser("on", std::make_unique<ActionParser>(&action_manager, subcontexts));
parser.AddSectionParser("import", std::make_unique<ImportParser>(&parser));
return parser;
}
接着可以看到如果通过属性ro.boot.init_rc指定了rc文件的路径,则直接去解析属性指定的rc文件。否则(在Android7.0版本之后,为了方便维护,允许各个小模块编写自己独立的.rc配置文件,然后在编译打包结束后,烧录时复制到目标机器的"/system/etc/init"、"/product/etc/init"、"/odm/etc/init"、"/vendor/etc/init"这些目录下。)需要先解析目标设备根目录下的init.rc文件,然后去这几个目录下去收集.rc文件,并通过ParseConfig()函数解析它们。解析过程稍显复杂,有兴趣可以在pie\system\core\init\parser.cpp中看详细的解析过程,我们这里只需要知道这个函数干了什么就好。
2.5、while循环
main函数走到最后,这里就剩下一个while循环了:
// pie\system\core\init\init.cpp
int main(int argc, char** argv) {
…………
while (true) {
int epoll_timeout_ms = -1;
//如果更改sys.powerctl属性,则通过HandlePowerctlMessage()函数处理该属性值所代表的shutdown或reboot动作
if (do_shutdown && !shutting_down) {
do_shutdown = false;
if (HandlePowerctlMessage(shutdown_command)) {
shutting_down = true;
}
}
if (!(waiting_for_prop || Service::is_exec_service_running())) {
am.ExecuteOneCommand(); //开始顺序执行队列中的Actions
}
if (!(waiting_for_prop || Service::is_exec_service_running())) {
if (!shutting_down) {
//如果是第一次执行,则去顺序启动所有的service,否则重启那些非oneshot类型的、并且已经挂掉的service
auto next_process_restart_time = RestartProcesses();
if (next_process_restart_time) { //在设定的重启时间不阻塞,及时重启进程
epoll_timeout_ms = std::chrono::ceil<std::chrono::milliseconds>(
*next_process_restart_time - boot_clock::now())
.count();
if (epoll_timeout_ms < 0) epoll_timeout_ms = 0;
}
}
if (am.HasMoreCommands()) epoll_timeout_ms = 0; //还有action没处理完时不阻塞,及时处理
}
// 没有事件到来的话,最多阻塞epoll_timeout_ms时间
epoll_event ev;
int nr = TEMP_FAILURE_RETRY(epoll_wait(epoll_fd, &ev, 1, epoll_timeout_ms));
if (nr == -1) {
PLOG(ERROR) << "epoll_wait failed";
} else if (nr == 1) { //有事件到来,执行对应动作
((void (*)()) ev.data.ptr)();
}
}
return 0;
}
一图总结全文如下:
3、结语
由于能力有限,在不同的学习阶段理解和认知能力有许多不同,所以就先写目前所能理解的这些。以后回看的时候,再纠正问题和补充文中的TODO部分,欢迎补充和指正不足。