1 加载init.rc文件
在之前init进程启动的第二阶段SecondStageMain执行
代码如下
路径 init.cpp
ActionManager& am = ActionManager::GetInstance();
ServiceList& sm = ServiceList::GetInstance();
//1 加载解析init.rc脚本
//2 将解析出来的事件放入am中
//3 将解析出来的service放入sm中
LoadBootScripts(am, sm);
2 了解init.rc文件规则
init.rc文件由多个部分组成,每个部分包含一定的执行命令行
主要有5种类型
Action、Command、Service、Option、Import
2.1 1 Action
on <trigger>
<command>
<command>
...
这里trigger是事件触发器的条件
command是满足事件触发条件执行的命令
eg
on init
sysclktz 0
# Mix device-specific information into the entropy pool
copy /proc/cmdline /dev/urandom
copy /system/etc/prop.default /dev/urandom
symlink /proc/self/fd/0 /dev/stdin
symlink /proc/self/fd/1 /dev/stdout
symlink /proc/self/fd/2 /dev/stderr
当系统init初始化时
copy /proc/cmdline /dev/urandom - 这条命令将 /proc/cmdline 中的数据复制到
/dev/urandom 设备文件中,以增加随机性,对系统的安全性和密码学操作有所帮助。
copy /system/etc/prop.default /dev/urandom - 这条命令将 /system/etc/prop.default
中的数据复制到 /dev/urandom 设备文件中,同样是为了增加随机性。
symlink /proc/self/fd/0 /dev/stdin - 这条命令创建了一个符号链接,
将 /proc/self/fd/0 映射到 /dev/stdin,以便让程序能够从标准输入读取数据。
symlink /proc/self/fd/1 /dev/stdout - 这条命令创建了一个符号链接,
将 /proc/self/fd/1 映射到 /dev/stdout,以便让程序能够向标准输出写入数据。
symlink /proc/self/fd/2 /dev/stderr - 这条命令创建了一个符号链接,
将 /proc/self/fd/2 映射到 /dev/stderr,以便让程序能够向标准错误输出写入数据
常用的系统事件有以下几种


2.2 command
内置命令
这些命令是init进程直接实现的,它们通常用于执行一些基本的系统操作,如挂载文件系统,设置系统属性,启动和停止服务等。这些命令是Android系统启动和运行的基础,它们在系统的整个生命周期中都可能被使用
eg
copy /system/etc/prop.default /dev/urandom
symlink /proc/self/fd/0 /dev/stdin
chown system system /dev/stune
chmod 0664 /dev/stune/tasks
write /dev/cpuctl/nnapi-hal/cpu.uclamp.min 1
start servicemanager
mount bpf bpf /sys/fs/bpf nodev noexec nosuid
mount:这是用于挂载文件系统的命令。
bpf:指定要挂载的文件系统类型,这里是 BPF 文件系统。
bpf:指定挂载点,即将 BPF 文件系统挂载到 /sys/fs/bpf 目录。
nodev:表示不允许特殊设备文件。
noexec:表示不允许执行任何程序文件。
nosuid:表示禁止 set-user-ID 或 set-group-ID 权限位。
总的来说,这条命令的作用是将 BPF 文件系统挂载到指定目录,并设置了一些安全选项,以确保挂载的文件系统不包含特殊设备文件,不允许执行程序文件,也不允许设置 set-user-ID 或 set-group-ID 权限位。这样可以提高系统的安全性,防止恶意使用 BPF 文件系统进行攻击或滥用。 BPF 文件系统通常用于网络数据包过滤等用途,通过设置这些安全选项可以更好地控制其行为




外部命令
这些命令是由系统提供的可执行文件实现的,它们通常用于执行一些更复杂的操作,如创建和删除目录,修改文件权限等。这些命令通常在系统启动时被执行,用于设置系统的初始状态

eg
exec - system system -- /system/bin/vdc checkpoint markBootAttempt
exec:这是一个系统命令,用于在当前进程中执行指定的命令。
-:表示执行命令时不创建新的进程,而是在当前进程中执行。
system system:这是命令的用户和组标识符,指定了要以哪个用户和组的身份执行命令。
/system/bin/vdc:这是要执行的命令路径,即 /system/bin/vdc。
checkpoint markBootAttempt:这是 /system/bin/vdc 命令的参数,具体是执行 checkpoint markBootAttempt 操作。
根据命令的内容,它的作用是在 Android 系统中使用 /system/bin/vdc 命令执行 checkpoint markBootAttempt 操作。这个操作通常用于在系统启动过程中标记启动尝试,可能与系统启动状态的记录和管理有关
exec -- /system/bin/fsverity_init --load-verified-keys
exec:这是一个系统命令,用于在当前进程中执行指定的命令。
--:表示执行命令时不切换用户和组,即保持当前的用户和组不变。
/system/bin/fsverity_init:这是要执行的命令路径,即 /system/bin/fsverity_init。
--load-verified-keys:这是 /system/bin/fsverity_init 命令的参数,具体是执行 --load-verified-keys 操作。
根据命令的内容,它的作用是在 Android 系统中使用 /system/bin/fsverity_init 命令执行 --load-verified-keys 操作。这个操作通常用于加载验证密钥,用于文件系统完整性保护和安全启动。在 Android 系统中,fsverity 是一种基于 Merkle 树和 HMAC 的完整性保护技术,可以确保文件系统中的文件不被篡改。
2.3 service
Services(服务)是一个程序,以 service开头,由init进程启动,一般运行于另外一个init的子进程,所以启动service前需要判断对应的可执行文件是否存在。init生成的子进程,定义在rc文件,其中每一个service,在启动时会通过fork方式生成子进程。Services(服务)的形式如下:
服务(services)是指那些须要在系统初始化时就启动或退出时自己主动重新启动的程序.
eg
service ueventd /system/bin/ueventd
class core
critical
seclabel u:r:ueventd:s0
shutdown critical
service ueventd /system/bin/ueventd:定义了一个名为 "ueventd" 的服务,并指定了其执行的程序路径为 "/system/bin/ueventd"。
class core:指定了服务的类别为 "core",这可能与系统核心功能相关。
critical:表示这个服务是关键的,对系统的正常运行至关重要。
seclabel u:r:ueventd:s0:指定了服务的安全标签,即 SELinux 标签,用于控制服务对系统资源的访问权限。
shutdown critical:表示在系统关机时,这个服务是关键的,需要优先关闭。
综合起来,这个命令的作用是定义了一个名为 "ueventd" 的关键系统服务,在系统启动时会启动该服务,并且在系统关机时将其优先关闭。这个服务可能与设备事件的处理和管理有关。在 Android 系统中,ueventd 负责处理设备插拔事件等,确保系统能够正确识别和响应外部设备的变化。
service console /system/bin/sh
class core
console
disabled
user shell
group shell log readproc
seclabel u:r:shell:s0
setenv HOSTNAME console
service console /system/bin/sh:定义了一个名为 "console" 的服务,并指定了其执行的程序路径为 "/system/bin/sh",即 shell。
class core:指定了服务的类别为 "core",这可能与系统核心功能相关。
console:表示这是一个控制台相关的服务。
disabled:表示该服务在启动时是被禁用的,需要手动启用。
user shell:指定了该服务的用户为 "shell"。
group shell log readproc:指定了该服务所属的组,包括 "shell"、"log" 和 "readproc"。
seclabel u:r:shell:s0:指定了服务的安全标签,即 SELinux 标签,用于控制服务对系统资源的访问权限。
setenv HOSTNAME console:设置了环境变量 HOSTNAME 的值为 "console"。
综合起来,这个命令的作用是定义了一个名为 "console" 的服务,它与系统控制台相关,并使用 shell 程序作为实现。该服务在启动时处于禁用状态,需要手动启用。此服务可能与系统的控制台交互和管理有关。
2.4 option
options是Service的修订项。它们决定一个Service何时以及如何运行。
services中可用选项如下表
eg
service console /system/bin/sh
class core
console
disabled
user shell
group shell log readproc
seclabel u:r:shell:s0
setenv HOSTNAME console

2.5 import
import 导入其他的init.xx.rc文件。
作用
每个系统如何加载这些不同目录的.rc文件,需要具体看从init.rc开始的import语句。 其中,import /init.${ro.hardware}.rc 较为常见。通过cat proc/cpuinfo可以查看ro.hardware的值=Hardware的值;一般在cpuinfo文件的末尾。
eg
import /init.environ.rc
import /system/etc/init/hw/init.usb.rc
import /init.${ro.hardware}.rc
import /vendor/etc/init/hw/init.${ro.hardware}.rc
import /system/etc/init/hw/init.usb.configfs.rc
import /system/etc/init/hw/init.${ro.zygote}.rc
3 init.rc解析加载流程
代码分析
//1 加载解析init.rc脚本
//2 将解析出来的事件放入am中
//3 将解析出来的service放入sm中
LoadBootScripts(am, sm);
static void LoadBootScripts(ActionManager& action_manager, ServiceList& service_list) {
Parser parser = CreateParser(action_manager, service_list);
//Android 系统属性,用于指定系统启动时执行的初始化脚本。
//1 创建rc文件解析器
//2 获取"ro.boot.init_rc"属性值(默认rc文件路径)
//3 如果默认为空 按序加载/system/etc/init/hw/init.rc
// /vendor/etc/init
// /odm/etc/init
// /product/etc/init
std::string bootscript = GetProperty("ro.boot.init_rc", "");
if (bootscript.empty()) {
parser.ParseConfig("/system/etc/init/hw/init.rc");
if (!parser.ParseConfig("/system/etc/init")) {
late_import_paths.emplace_back("/system/etc/init");
}
// late_import is available only in Q and earlier release. As we don't
// have system_ext in those versions, skip late_import for system_ext.
parser.ParseConfig("/system_ext/etc/init");
if (!parser.ParseConfig("/vendor/etc/init")) {
late_import_paths.emplace_back("/vendor/etc/init");
}
if (!parser.ParseConfig("/odm/etc/init")) {
late_import_paths.emplace_back("/odm/etc/init");
}
if (!parser.ParseConfig("/product/etc/init")) {
late_import_paths.emplace_back("/product/etc/init");
}
} else {
parser.ParseConfig(bootscript);
}
}
bool Parser::ParseConfigFileInsecure(const std::string& path) {
//1 读取rc文件路径的内容
//2 解析数据
std::string config_contents;
if (!android::base::ReadFileToString(path, &config_contents)) {
return false;
}
ParseData(path, &config_contents);
return true;
}
void Parser::ParseData(const std::string& filename, std::string* data) {
data->push_back('\n');
data->push_back('\0');
//解析状态结构体
parse_state state;
//解析行数
state.line = 0;
//解析数据 ptr指向首个元素
state.ptr = data->data();
state.nexttoken = 0;
//定义块解析器
SectionParser* section_parser = nullptr;
int section_start_line = -1;
std::vector<std::string> args;
// If we encounter a bad section start, there is no valid parser object to parse the subsequent
// sections, so we must suppress errors until the next valid section is found.
bool bad_section_found = false;
auto end_section = [&] {
bad_section_found = false;
//section_parser表示当前是否需要解析器解析块
if (section_parser == nullptr) return;
//结束当次解析
if (auto result = section_parser->EndSection(); !result.ok()) {
//输出当次错误信息
parse_error_count_++;
LOG(ERROR) << filename << ": " << section_start_line << ": " << result.error();
}
section_parser = nullptr;
section_start_line = -1;
};
for (;;) {
switch (next_token(&state)) {
// next _token的函数原理是,针对state->ptr的指针进行解析,依次向后读取data数组中的内容
// 如果读取到"\n","0"的话,返回T_EOF和T_NEWLINE
// 如果读取出来的是一个词的话,则将内容保存在args的数组中,内容依次向后
// // 如果是文件读取结束
case T_EOF:
//1 结束当前处理
//2 对每个块处理器进行资源清理操作 EndFile
end_section();
for (const auto& [section_name, section_parser] : section_parsers_) {
section_parser->EndFile();
}
return;
case T_NEWLINE: {
//读取到/n换行
state.line++;
if (args.empty()) break;
// If we have a line matching a prefix we recognize, call its callback and unset any
// current section parsers. This is meant for /sys/ and /dev/ line entries for
// uevent.
//line_callbacks_ 保存行前缀和对应的回调函数
//这里通过find_if遍历line_callbacks_查找与行前缀匹配
//的行回调
auto line_callback = std::find_if(
line_callbacks_.begin(), line_callbacks_.end(),
[&args](const auto& c) { return android::base::StartsWith(args[0], c.first); });
if (line_callback != line_callbacks_.end()) {
//找到了就结束当前块处理
//保证执行顺序
end_section();
//这里执行行回调函数处理传入的参数
if (auto result = line_callback->second(std::move(args)); !result.ok()) {
//失败的话记录追加错误行
parse_error_count_++;
LOG(ERROR) << filename << ": " << state.line << ": " << result.error();
}
} else if (section_parsers_.count(args[0])) {
//1 如果未找到合适的行回调处理函数
//2 通过section_parsers_.count(args[0])
//3 查看当前行前缀是否有块处理器处理
//4 结束当前块处理
//5 根据行前缀获取块解析器
//6 块解析器处理行
end_section();
section_parser = section_parsers_[args[0]].get();
section_start_line = state.line;
if (auto result =
section_parser->ParseSection(std::move(args), filename, state.line);
!result.ok()) {
parse_error_count_++;
LOG(ERROR) << filename << ": " << state.line << ": " << result.error();
section_parser = nullptr;
bad_section_found = true;
}
} else if (section_parser) {
//1 判断当前块解析器是否存在
//2 存在直接用当前解析器解析行
if (auto result = section_parser->ParseLineSection(std::move(args), state.line);
!result.ok()) {
parse_error_count_++;
LOG(ERROR) << filename << ": " << state.line << ": " << result.error();
}
} else if (!bad_section_found) {
parse_error_count_++;
LOG(ERROR) << filename << ": " << state.line
<< ": Invalid section keyword found";
}
//7 处理完当前行后 要清理参数 为后续init.rc的内容读取
//创造条件
args.clear();
break;
}
case T_TEXT:
//1 如果不是行结束符 换行符 那就将当前行的元素存到参数列表中
args.emplace_back(state.text);
break;
}
}
}
4 init进程整体的运作
系统图

流程图

解析流程图

事件trigger和属性trigger会被
描述
init进程启动流程
1 FirstStageMain 第一阶段,创建挂载系统启动需要的目录
2 FirstStageMain 第一阶段 加载内核模块
3 FirstStageMain 第一阶段 授予文件权限
4 FirstStageMain 第一阶段 执行init文件 进入selinux_setup
5 SetupSelinux 预编译文件编译 读取selinux policy 加载策略文件
6 SetupSelinux 设置selinux的启动状态
7 SetupSelinux 执行init文件 进入second_stage
8 SecondStageMain 第二阶段 加载系统属性和对应的selinux安全上下文
9 SecondStageMain 第二阶段 注册sigchild信号处理函数
10 SecondStageMain 第二阶段 StartPropertyService打开属性服务
11 SecondStageMain 第二阶段 LoadBootScripts 加载init.rc脚本文件
12 SecondStageMain 第二阶段 添加Action到行为队列中
13 SecondStageMain 第二阶段 进入无线循环 按序处理Action 每次处理后检查是否需要重启进程,监听sigCHLD信号回收僵尸进程,监听系统属性变化 处理一种被称为"chorded keyboard"的键盘输入事件
当无事件时epoll_wait会堵塞当前线程
整体解析流程
1 解析配置文件
判断ro.boot.init_rc属性的值(文件路径)是否存在
存在解析ro.boot.init_rc属性的值
不存在解析
遍历一下目录解析配置文件
/system/etc/init
/vendor/etc/init
/odm/etc/init
/product/etc/init
2 将配置文件的内容读取出来解析ParseData(path, &config_contents)
3 解析文件内容时
next_token(&state)获取状态
T_EOF
读取文件结束符
执行各个section的清理工作
T_NEWLINE
读取行结束符
根据行前缀获取响应的sectionPaser来处理
ActionParser 会创建Action将事件Trigger和PropertyTrigger
添加到Action对象
ServiceParser会创建Service对象来将服务的路径启动参数添加到service对象
如果section_parser当前以存在
那么代表当前解析的section块下的command
将command统一放到Action下
将command统一放到Service下
T_TEXT
代表文字结束
会将对应文字表示的参数放到参数队列
4
每次section块加载完
执行endSection函数将Action放到ActionManager管理
ActionParser->action_manager_->actions_
将Service放到Service_list管理
ServiceParser->service_list_->services_
5 am.QueueBuiltinAction(SetupCgroupsAction, "SetupCgroups");
该函数在创建action的同时,把该事件触发添加到ActionManager的事件队列中
6 QueueEventTrigger("early-init")把early-init加入到event_queue_的队列中
7 ExecuteOneCommand 会遍历Action和event_queue的第一个event比较当event trigger和property trigger一致的话就可以添加到当前执行的Action集合,然后遍历当前执行的action集合获取里面的command来invoke到对应的函数执行
5 参考链接
隔壁老李头 - 简书 (jianshu.com)
Android启动初始化init.rc详解 - 知乎 (zhihu.com)