Android Init 进程第一阶段 FirstStageMain 详细分析
FirstStageMain 是 Android 启动过程中从内核空间过渡到用户空间的第一个进程,在最小的 initramdisk 环境中建立基础系统设施,为后续启动阶段奠定基础。
大致流程如下:
完整执行流程详细分析
1. 紧急重启信号处理
if (REBOOT_BOOTLOADER_ON_PANIC) {
InstallRebootSignalHandlers();
}
分析:
- 目的:建立安全防护网,防止系统卡死在崩溃状态
- 信号覆盖:SIGABRT、SIGBUS、SIGSEGV 等关键信号
- 生产/开发差异:
- 生产版本:启用,确保设备可恢复性
- 开发版本:可能禁用,便于调试崩溃现场
- 实现机制:
void InstallRebootSignalHandlers() { struct sigaction action; memset(&action, 0, sizeof(action)); action.sa_handler = [](int signal) { LOG(ERROR) << "Received fatal signal " << signal; sync(); // 确保数据刷入存储 RebootSystem(ANDROID_RB_RESTART2, "bootloader"); }; sigaction(SIGABRT, &action, nullptr); sigaction(SIGSEGV, &action, nullptr); sigaction(SIGBUS, &action, nullptr); }
2. 启动时间记录和错误收集框架
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);
分析:
- 精确计时:使用
boot_clock记录启动耗时,用于性能监控和优化 - 集中错误管理:统一收集所有初始化错误,避免分散的错误处理
- 宏技巧:
#x字符串化操作符,将代码转换为可读的错误消息- 保存
errno提供详细的系统错误信息
- 延迟报告:不立即处理错误,而是收集后统一报告,便于问题诊断
3. 基础环境初始化
umask(0);
CHECKCALL(clearenv());
CHECKCALL(setenv("PATH", _PATH_DEFPATH, 1));
解析:
umask(0)
- 作用机理:清除文件创建掩码,设置为 0000
- 影响范围:确保新创建的文件具有精确的权限位(0777 & ~umask)
- 必要性:避免从内核继承不可预测的 umask 设置
- 安全考虑:后续会针对具体文件设置适当权限
clearenv()
- 安全哲学:从绝对干净的状态开始,避免环境变量污染
- 内核继承:清除从内核通过
execve传递的所有环境变量 - 确定性:确保启动环境的一致性,不受外部因素影响
setenv("PATH", _PATH_DEFPATH, 1)
- 最小化原则:PATH 只包含
/sbin:/system/bin:/system/xbin - 安全限制:防止执行未经验证位置的可执行文件
- 渐进式扩展:后续阶段会根据需要扩展 PATH 环境变量
4. 虚拟文件系统建立
4.1 挂载 tmpfs 到 /dev
CHECKCALL(mount("tmpfs", "/dev", "tmpfs", MS_NOSUID, "mode=0755"));
技术分析:
tmpfs 特性利用:
- 内存驻留:数据存储在 RAM 中,访问速度快
- 动态大小:按需分配内存,不预占固定空间
- 易失性:重启后数据丢失,符合临时文件的特性
挂载必要性:
- initramfs 通常是只读的,无法创建设备节点
- 需要可写空间来动态管理设备文件
- 为 udev 或 mdev 类似的设备管理提供基础设施
安全标志解析:
MS_NOSUID:- 防止在
/dev目录中通过 SUID/SGID 程序进行权限提升 - 阻断潜在的攻击向量
- 防止在
mode=0755:- root 用户:读、写、执行权限
- 其他用户:读、执行权限,防止普通用户创建设备文件
4.2 创建设备目录结构
CHECKCALL(mkdir("/dev/pts", 0755)); // 伪终端从设备
CHECKCALL(mkdir("/dev/socket", 0755)); // 进程间通信套接字
CHECKCALL(mkdir("/dev/dm-user", 0755)); // 设备映射器用户空间接口
目录作用详解:
/dev/pts - 伪终端从设备系统
- 架构设计: 应用程序 → /dev/ptmx (主设备) → 内核PTY驱动 → /dev/pts/N (从设备) ← Shell进程
- ADB集成:每个
adb shell会话创建一个新的/dev/pts/N设备 - 终端仿真:终端应用、SSH 服务器等依赖此系统
- 权限管理:从设备通常权限为 0620,确保只有相应会话用户可访问
/dev/socket - Unix Domain Sockets
- 进程间通信:提供基于文件的进程间通信机制
- 系统服务使用:
Zygote:应用进程孵化器SurfaceFlinger:图形合成服务netd:网络管理守护进程
- 安全模型:通过文件系统权限控制访问权限
/dev/dm-user - 设备映射器用户空间接口
- Android 11+ 新特性:用户空间与内核设备映射器的通信通道
- F2FS 透明压缩: 文件系统 → dm-user设备 → 用户空间压缩守护进程 → 压缩数据存储
- 增量文件系统:支持动态系统更新的基座+增量合并
- 性能优化:在用户空间实现复杂的存储功能,避免内核复杂度
4.3 挂载核心虚拟文件系统
CHECKCALL(mount("devpts", "/dev/pts", "devpts", 0, NULL));
CHECKCALL(mount("proc", "/proc", "proc", 0, "hidepid=2,gid=" MAKE_STR(AID_READPROC)));
CHECKCALL(mount("sysfs", "/sys", "sysfs", 0, NULL));
CHECKCALL(mount("selinuxfs", "/sys/fs/selinux", "selinuxfs", 0, NULL));
各文件系统分析:
procfs (/proc) - 进程信息接口
- 进程信息暴露:
/proc/pid/status:进程状态信息/proc/pid/fd/:打开的文件描述符/proc/pid/maps:内存映射信息
- 系统信息接口:
/proc/cpuinfo:CPU 架构和信息/proc/meminfo:内存使用情况/proc/version:内核版本信息
- 安全配置解析:
hidepid=2:- 级别 0:传统模式,所有进程可见
- 级别 1:用户只能看到自己的进程
- 级别 2:用户只能看到自己的进程,且隐藏其他进程的统计信息
gid=AID_READPROC:AID_READPROC= 1009,特殊系统组- 系统监控工具(ps, top)属于此组,可以查看所有进程
- 防止恶意软件扫描系统进程信息进行攻击
sysfs (/sys) - 系统设备管理
- 设备发现机制: /sys/ ├── class/ # 设备类别目录 │ ├── net/ # 网络设备 (eth0, wlan0) │ ├── input/ # 输入设备 (event0, mouse0) │ ├── backlight/ # 背光设备 │ └── leds/ # LED 设备 ├── bus/ # 总线设备 │ ├── usb/ # USB 设备 │ ├── pci/ # PCI 设备 │ └── platform/ # 平台设备 └── devices/ # 物理设备树
- 运行时配置:通过读写文件调整内核参数和设备行为
- 电源管理:控制设备电源状态和性能参数
selinuxfs (/sys/fs/selinux) - 安全策略接口
- 策略管理:SELinux 策略加载和查询接口
- 状态信息:提供 SELinux 当前状态和强制模式信息
- 为第二阶段准备:第二阶段的
selinux_setup会使用此接口
5. 创建设备节点
CHECKCALL(mknod("/dev/kmsg", S_IFCHR | 0600, makedev(1, 11)));
CHECKCALL(mknod("/dev/random", S_IFCHR | 0666, makedev(1, 8)));
CHECKCALL(mknod("/dev/urandom", S_IFCHR | 0666, makedev(1, 9)));
CHECKCALL(mknod("/dev/ptmx", S_IFCHR | 0666, makedev(5, 2)));
CHECKCALL(mknod("/dev/null", S_IFCHR | 0666, makedev(1, 3)));
设备节点详解:
/dev/kmsg - 内核消息接口
- 设备号:
makedev(1, 11)- 主设备号 1(内存设备),次设备号 11 - 权限 0600:只有 root 可以访问,保护内核日志安全
- 使用场景:
- init 进程日志输出
- 内核 printk 消息读取
- 系统调试信息收集
/dev/random & /dev/urandom - 随机数源
- 密码学安全:为系统加密操作提供熵源
- 权限 0666:所有用户可访问,许多库和应用依赖随机数
- 技术区别:
random:阻塞型,熵池不足时阻塞,适合高安全性场景urandom:非阻塞型,始终返回数据,适合性能敏感场景
- 设备号:主设备号 1,次设备号 8 和 9
/dev/ptmx - 伪终端主设备
- 设备号:
makedev(5, 2)- 主设备号 5(tty 设备),次设备号 2 - 终端会话工厂:打开此设备会自动分配新的 pts 从设备
- ADB Shell 基础:
adb shell命令的核心依赖
/dev/null - 空设备
- 数据黑洞:丢弃所有写入数据,读取立即返回 EOF
- 标准流重定向:用于丢弃不需要的输出流
- 设备号:
makedev(1, 3)- 主设备号 1,次设备号 3
6. 准备挂载点结构
6.1 为 vold 准备挂载区域
CHECKCALL(mount("tmpfs", "/mnt", "tmpfs", MS_NOEXEC | MS_NOSUID | MS_NODEV,
"mode=0755,uid=0,gid=1000"));
CHECKCALL(mkdir("/mnt/vendor", 0755));
CHECKCALL(mkdir("/mnt/product", 0755));
/mnt 目录架构分析:
/mnt/ (tmpfs, 安全隔离环境)
├── vendor/ # 供应商特定挂载点
│ ├── firmware/ # 硬件固件
│ ├── persist/ # 持久化配置
│ └── ... # 其他供应商特定数据
├── product/ # 产品特定挂载点
│ ├── media/ # 产品媒体文件
│ ├── app/ # 产品预装应用
│ └── ... # 其他产品定制内容
├── expand/ # 可扩展存储 (vold 运行时创建)
├── asec/ # 安全容器 (vold 运行时创建)
├── obb/ # 游戏数据包 (vold 运行时创建)
└── runtime/ # 运行时权限挂载 (vold 运行时创建)
安全标志解析:
MS_NOEXEC:禁止执行其中的程序,防止代码注入攻击MS_NOSUID:忽略 SUID/SGID 权限位,防止权限提升MS_NODEV:禁止访问设备文件,限制设备访问能力uid=0,gid=1000:root 用户,system 组(AID_SYSTEM)
6.2 调试和资源保留
CHECKCALL(mount("tmpfs", "/debug_ramdisk", "tmpfs", MS_NOEXEC | MS_NOSUID | MS_NODEV,
"mode=0755,uid=0,gid=0"));
CHECKCALL(mount("tmpfs", kSecondStageRes, "tmpfs", MS_NOEXEC | MS_NOSUID | MS_NODEV,
"mode=0755,uid=0,gid=0"));
用途分析:
/debug_ramdisk
- 调试信息保存:在根切换前保存调试数据
- 崩溃分析:保留系统崩溃时的现场信息
- 性能监控:存储启动性能指标数据
kSecondStageRes(通常是 /.backup)
- 阶段间资源传递:保持 ramdisk 中的数据在根切换后仍可访问
- 资源配置保存:存储需要在第二阶段使用的初始化配置
- 回滚支持:在启动失败时提供恢复所需的数据
7. 日志系统初始化
SetStdioToDevNull(argv);
InitKernelLogging(argv);
SetStdioToDevNull() 实现
void SetStdioToDevNull() {
// 打开 /dev/null
int fd = open("/dev/null", O_RDWR);
if (fd == -1) {
// 在 /dev/null 不可用时,这是严重错误
abort();
}
// 重定向标准输入、输出、错误
dup2(fd, STDIN_FILENO); // 标准输入 ← /dev/null
dup2(fd, STDOUT_FILENO); // 标准输出 → /dev/null
dup2(fd, STDERR_FILENO); // 标准错误 → /dev/null
// 关闭原始文件描述符
if (fd > STDERR_FILENO) {
close(fd);
}
}
- 目的:防止未初始化的标准流导致意外行为或数据竞争
- 必要性:在控制台完全建立前,确保标准流有确定的行为
- 安全考虑:避免敏感信息泄漏到未初始化的输出流
InitKernelLogging(argv) 分析
void InitKernelLogging(char** argv) {
// 初始化日志标签
log_id id = LOG_ID_MAIN;
const char* tag = basename(argv[0]);
// 打开内核日志设备
int fd = open("/dev/kmsg", O_WRONLY | O_CLOEXEC);
if (fd == -1) {
return; // 日志初始化失败,但继续启动
}
// 设置日志输出到内核环状缓冲区
android_set_log_frontend(LOGGER_KERNEL, fd);
// 根据内核参数设置日志级别
std::string loglevel;
if (fs_mgr_get_boot_config("loglevel", &loglevel)) {
SetLogLevelFromString(loglevel);
}
}
- 输出到 kmsg:所有 LOG() 宏输出到
/dev/kmsg - 日志等级控制:根据
loglevel=内核参数设置详细程度 - 标签标识:使用程序名作为日志标签,便于过滤和分析
8. 错误检查和处理
if (!errors.empty()) {
for (const auto& [error_string, error_errno] : errors) {
LOG(ERROR) << error_string << " " << strerror(error_errno);
}
LOG(FATAL) << "Init encountered errors starting first stage, aborting";
}
错误处理策略分析:
统一错误报告机制
- 批量处理:收集所有错误后统一输出,避免分散的错误信息
- 上下文保留:同时记录错误描述和系统 errno,提供完整上下文
- 人类可读:使用
strerror()将 errno 转换为可读描述
严重性分级
// CHECKCALL 宏处理的错误类型
- EACCES: 权限不足
- ENOENT: 文件或目录不存在
- ENOTDIR: 路径不是目录
- EIO: I/O 错误
- ENOMEM: 内存不足
// 这些错误通常表示系统配置错误或硬件问题
终止策略
- 立即终止:关键错误导致立即终止,防止后续未知行为
- 明确信息:提供清晰的错误消息,指导问题诊断
- FATAL 级别:确保错误消息被看到,即使日志系统有问题
9. 内核模块加载
auto want_console = ALLOW_FIRST_STAGE_CONSOLE ? FirstStageConsole(cmdline, bootconfig) : 0;
auto want_parallel = bootconfig.find("androidboot.load_modules_parallel = \"true\"") != std::string::npos;
boot_clock::time_point module_start_time = boot_clock::now();
int module_count = 0;
if (!LoadKernelModules(IsRecoveryMode() && !ForceNormalBoot(cmdline, bootconfig),
want_console, want_parallel, module_count)) {
// 模块加载失败处理
}
### 10. 设备创建和早期控制台
```cpp
bool created_devices = false;
if (want_console == FirstStageConsoleParam::CONSOLE_ON_FAILURE) {
if (!IsRecoveryMode()) {
created_devices = DoCreateDevices();
if (!created_devices) {
LOG(ERROR) << "Failed to create device nodes early";
}
}
StartConsole(cmdline);
}
DoCreateDevices() 分析
bool DoCreateDevices() {
// 1. 初始化基础设备
if (!InitDevices()) {
LOG(ERROR) << "Failed to initialize base devices";
return false;
}
// 2. 挂载 metadata 分区(如果存在)
auto metadata_partition = std::find_if(fstab_.begin(), fstab_.end(),
[](const auto& entry) {
return entry.mount_point == "/metadata";
});
if (metadata_partition != fstab_.end()) {
if (MountPartition(metadata_partition, true /* erase_same_mounts */)) {
// 必须在切换根目录前完成 DSU 密钥复制
CopyDsuAvbKeys();
} else {
LOG(WARNING) << "Failed to mount metadata partition";
}
}
// 3. 创建逻辑分区
if (!CreateLogicalPartitions()) {
LOG(ERROR) << "Failed to create logical partitions";
return false;
}
return true;
}
InitDevices() 设备初始化
bool FirstStageMount::InitDevices() {
std::set<std::string> devices;
GetSuperDeviceName(&devices);
// 获取所有的dm verity设备
if (!GetDmVerityDevices(&devices)) {
return false;
}
// 初始化设备映射器
if (!InitRequiredDevices(std::move(devices))) {
return false;
}
if (IsDmLinearEnabled()) {
auto super_symlink = "/dev/block/by-name/"s + super_partition_name_;
if (!android::base::Realpath(super_symlink, &super_path_)) {
PLOG(ERROR) << "realpath failed: " << super_symlink;
return false;
}
}
return true;
}
// InitRequiredDevices(std::move(devices))会调用到此方法初始化一个DeviceMapper设备
bool BlockDevInitializer::InitDeviceMapper() {
return InitMiscDevice("device-mapper");
}
bool BlockDevInitializer::InitMiscDevice(const std::string& name) {
const std::string dm_path = "/devices/virtual/misc/" + name;
bool found = false;
auto dm_callback = [this, &dm_path, &found](const Uevent& uevent) {
if (uevent.path == dm_path) {
device_handler_->HandleUevent(uevent);
found = true;
return ListenerAction::kStop;
}
return ListenerAction::kContinue;
};
uevent_listener_.RegenerateUeventsForPath("/sys" + dm_path, dm_callback);
return true;
}
CreateLogicalPartitions() 逻辑分区创建
bool CreateLogicalPartitions() {
// 获取超级分区信息
std::string super_device = fs_mgr_get_super_partition_name();
if (super_device.empty()) {
LOG(INFO) << "No super partition found, skipping logical partitions";
return true; // 不是错误,传统分区系统
}
// 创建逻辑分区表
LpMetadata metadata = ReadPartitionMetadata(super_device);
if (!metadata) {
LOG(ERROR) << "Failed to read partition metadata";
return false;
}
// 为每个分区创建设备映射器设备
for (const auto& partition : metadata.partitions) {
if (!CreateLogicalPartition(partition, super_device)) {
LOG(ERROR) << "Failed to create logical partition: " << partition.name;
return false;
}
}
return true;
}
StartConsole() 控制台启动
void StartConsole(const std::string& cmdline) {
// 解析控制台参数
std::string console_device = GetConsoleDeviceFromCmdline(cmdline);
if (console_device.empty()) {
console_device = "/dev/console"; // 默认控制台
}
// 设置控制台
if (!SetupConsole(console_device)) {
LOG(ERROR) << "Failed to setup console on " << console_device;
return;
}
// 启动 shell(在控制台上)
StartConsoleShell();
LOG(INFO) << "Console started on " << console_device;
}
11. 配置和策略准备
11.1 复制 ramdisk 属性
if (access(kBootImageRamdiskProp, F_OK) == 0) {
std::string dest = GetRamdiskPropForSecondStage();
std::string dir = android::base::Dirname(dest);
// 创建目标目录
std::error_code ec;
if (!fs::create_directories(dir, ec) && !!ec) {
LOG(FATAL) << "Can't mkdir " << dir << ": " << ec.message();
}
// 复制属性文件
if (!fs::copy_file(kBootImageRamdiskProp, dest, ec)) {
LOG(FATAL) << "Can't copy " << kBootImageRamdiskProp << " to "
<< dest << ": " << ec.message();
}
LOG(INFO) << "Copied ramdisk prop to " << dest;
}
分析:
- boot image 属性:存储在 boot image ramdisk 中的构建属性
- 第二阶段访问:复制到第二阶段可以访问的持久化位置
- 构建信息传递:确保构建信息在根切换后仍然可用
11.2 调试模式支持
if (access("/force_debuggable", F_OK) == 0) {
constexpr const char adb_debug_prop_src[] = "/adb_debug.prop";
constexpr const char userdebug_plat_sepolicy_cil_src[] = "/userdebug_plat_sepolicy.cil";
std::error_code ec;
// 复制 ADB 调试属性
if (access(adb_debug_prop_src, F_OK) == 0 &&
!fs::copy_file(adb_debug_prop_src, kDebugRamdiskProp, ec)) {
LOG(WARNING) << "Can't copy " << adb_debug_prop_src << " to "
<< kDebugRamdiskProp << ": " << ec.message();
}
// 复制 userdebug SELinux 策略
if (access(userdebug_plat_sepolicy_cil_src, F_OK) == 0 &&
!fs::copy_file(userdebug_plat_sepolicy_cil_src, kDebugRamdiskSEPolicy, ec)) {
LOG(WARNING) << "Can't copy " << userdebug_plat_sepolicy_cil_src << " to "
<< kDebugRamdiskSEPolicy << ": " << ec.message();
}
// 设置环境变量供第二阶段使用
setenv("INIT_FORCE_DEBUGGABLE", "true", 1);
}
调试功能分析:
ADB 调试支持
- adb_debug.prop:包含
ro.debuggable=1和ro.adb.secure=0 - adb root:在 unlocked 设备上允许
adb root命令 - 开发工具:启用完整的开发工具链支持
SELinux 策略调整
- userdebug 策略:更宽松的 SELinux 策略,便于调试
- 权限放宽:允许调试操作所需的额外权限
- 审计日志:更详细的 SELinux 拒绝日志
环境变量传递
- INIT_FORCE_DEBUGGABLE:通知第二阶段 init 使用调试配置
- 策略选择:影响 SELinux 策略加载和服务启动
12. 根目录切换准备和执行
if (ForceNormalBoot(cmdline, bootconfig)) {
mkdir("/first_stage_ramdisk", 0755);
PrepareSwitchRoot();
// 绑定挂载目标目录
if (mount("/first_stage_ramdisk", "/first_stage_ramdisk", nullptr, MS_BIND, nullptr) != 0) {
PLOG(FATAL) << "Could not bind mount /first_stage_ramdisk to itself";
}
SwitchRoot("/first_stage_ramdisk");
}
ForceNormalBoot() 强制正常启动
bool ForceNormalBoot(const std::string& cmdline, const std::string& bootconfig) {
// 检查 bootconfig 参数
if (bootconfig.find("androidboot.force_normal_boot = \"1\"") != std::string::npos) {
return true;
}
// 检查内核命令行参数
if (cmdline.find("androidboot.force_normal_boot=1") != std::string::npos) {
return true;
}
return false;
}
使用场景:
- 系统更新后:确保更新后启动到新系统而不是恢复模式
- 启动循环恢复:打破可能的启动循环
- 工厂测试:在测试环境中强制正常启动
PrepareSwitchRoot() 根切换准备
void PrepareSwitchRoot() {
// 创建新根目录结构
const std::vector<std::string> new_root_dirs = {
"/first_stage_ramdisk/system",
"/first_stage_ramdisk/vendor",
"/first_stage_ramdisk/product",
"/first_stage_ramdisk/data",
"/first_stage_ramdisk/dev",
"/first_stage_ramdisk/proc",
"/first_stage_ramdisk/sys"
};
for (const auto& dir : new_root_dirs) {
if (mkdir(dir.c_str(), 0755) != 0 && errno != EEXIST) {
PLOG(FATAL) << "Failed to create directory: " << dir;
}
}
// 设置挂载传播类型
if (mount(nullptr, "/", nullptr, MS_SLAVE | MS_REC, nullptr) != 0) {
PLOG(WARNING) << "Failed to set mount propagation to slave";
}
}
SwitchRoot() 根切换执行
void SwitchRoot(const std::string& new_root) {
// 切换到新根目录
if (chdir(new_root.c_str()) != 0) {
PLOG(FATAL) << "Could not chdir to new root: " << new_root;
}
// 执行 pivot_root
if (pivot_root(".", "old_root") != 0) {
PLOG(FATAL) << "pivot_root failed";
}
// 切换到新根目录
if (chdir("/") != 0) {
PLOG(FATAL) << "chdir to new root failed";
}
// 卸载旧根目录
if (umount2("/old_root", MNT_DETACH) != 0) {
PLOG(ERROR) << "Failed to unmount old root";
}
// 移除旧根目录
if (rmdir("/old_root") != 0) {
PLOG(WARNING) << "Failed to remove old root directory";
}
LOG(INFO) << "Successfully switched root to " << new_root;
}
根切换技术分析:
pivot_root vs chroot
-
pivot_root:
- 系统级根切换,影响整个挂载命名空间
- 可以完全卸载旧根文件系统
- 现代 Linux 推荐方式
-
chroot:
- 进程级根切换,只影响当前进程
- 旧根文件系统仍然保持挂载
- 传统方式,逐渐被淘汰
绑定挂载的重要性
mount("/first_stage_ramdisk", "/first_stage_ramdisk", nullptr, MS_BIND, nullptr);
- 自引用挂载:确保目录在自身内部可见
- pivot_root 要求:
pivot_root要求目标是一个挂载点 - 一致性保证:避免根切换后的路径解析问题
13. 第一阶段挂载执行
if (!DoFirstStageMount(!created_devices)) {
LOG(FATAL) << "Failed to mount required partitions early ...";
}
DoFirstStageMount() 分析
bool DoFirstStageMount(bool create_devices) {
// 1. 读取第一阶段 fstab
Fstab fstab;
if (!ReadFirstStageFstab(&fstab)) {
LOG(ERROR) << "Failed to read first stage fstab";
return false;
}
// 2. 条件性创建设备
if (create_devices && !DoCreateDevices()) {
LOG(ERROR) << "Failed to create devices for first stage mount";
return false;
}
// 3. 创建适当的挂载器实例
auto mount_helper = FirstStageMount::Create();
if (!mount_helper) {
LOG(ERROR) << "Failed to create first stage mount helper";
return false;
}
// 4. 执行挂载
return mount_helper->DoFirstStageMount(create_devices);
}
FirstStageMount::Create() 工厂方法
std::unique_ptr<FirstStageMount> FirstStageMount::Create() {
Fstab fstab;
if (!ReadFirstStageFstab(&fstab)) {
return nullptr;
}
// 根据系统特性选择挂载策略
if (IsDmVerityEnabled() && !IsRecoveryMode()) {
if (IsAvbEnabled()) {
// Android 验证启动 v2.0
return std::make_unique<FirstStageMountVBootV2>(std::move(fstab));
} else {
// 传统 dm-verity
return std::make_unique<FirstStageMountVerity>(std::move(fstab));
}
} else {
// 基础版本,无验证
return std::make_unique<FirstStageMount>(std::move(fstab));
}
}
挂载策略实现差异
FirstStageMountVBootV2 (AVB 2.0)
bool FirstStageMountVBootV2::DoFirstStageMount(bool create_devices) {
// 1. 初始化 AVB 处理
if (!InitAvbHandle()) {
return false;
}
// 2. 验证分区
if (!VerifyPartitions()) {
return false;
}
// 3. 设置 dm-verity
if (!SetUpDmVerity()) {
return false;
}
// 4. 挂载分区
return MountPartitions();
}
FirstStageMount (基础版本)
bool FirstStageMount::DoFirstStageMount(bool create_devices) {
// 直接挂载分区,无验证
return MountPartitions();
}
14. 系统根目录切换的关键步骤
在 FirstStageMount::MountPartitions() 中,首先调用 TrySwitchSystemAsRoot():
bool FirstStageMount::MountPartitions() {
// 1. 首先尝试系统根目录切换
if (!TrySwitchSystemAsRoot()) {
LOG(ERROR) << "Failed to switch to system-as-root";
return false;
}
// 2. 然后挂载其他分区
return MountRemainingPartitions();
}
TrySwitchSystemAsRoot() 详细实现
bool FirstStageMount::TrySwitchSystemAsRoot() {
// 检查是否支持 System-as-Root
if (!IsSystemAsRootSupported()) {
LOG(INFO) << "System-as-root not supported, using traditional layout";
return true; // 不支持也不算失败
}
// 查找系统分区条目
auto system_entry = GetSystemRootEntry();
if (system_entry == fstab_.end()) {
LOG(ERROR) << "No system partition found for system-as-root";
return false;
}
// 挂载系统分区到临时位置
if (!MountSystemForRootSwitch(system_entry)) {
return false;
}
// 执行根切换
return ExecuteSystemRootSwitch();
}
System-as-Root 架构优势
传统布局:
/ (rootfs, initramfs)
├── system/ # 系统分区挂载点
├── vendor/ # 供应商分区挂载点
├── data/ # 数据分区挂载点
└── ...
System-as-Root 布局:
/ (system 分区)
├── system/ # 系统子目录(绑定挂载或兼容性链接)
├── vendor/ # 供应商分区挂载点
├── data/ # 数据分区挂载点
└── ...
优势:
- 安全性:系统分区只读,防止篡改
- 一致性:统一的分区布局
- 简化性:减少 initramfs 的复杂性
- 验证:更好的验证启动支持
15. 资源清理和状态设置
15.1 释放 ramdisk 资源
struct stat old_root_info;
if (stat("/", &old_root_info) != 0) {
PLOG(ERROR) << "Could not stat(\"/\"), not freeing ramdisk";
old_root_dir.reset();
}
struct stat new_root_info;
if (stat("/", &new_root_info) != 0) {
PLOG(ERROR) << "Could not stat(\"/\"), not freeing ramdisk";
old_root_dir.reset();
}
if (old_root_dir && old_root_info.st_dev != new_root_info.st_dev) {
FreeRamdisk(old_root_dir.get(), old_root_info.st_dev);
}
资源释放分析:
设备号比较
st_dev:文件所在设备的设备号- 根切换前:initramfs 的设备号
- 根切换后:系统分区的设备号
- 设备号不同:说明成功执行了根切换
FreeRamdisk 实现
void FreeRamdisk(DIR* dir, dev_t ramdisk_dev) {
struct dirent* entry;
while ((entry = readdir(dir)) != nullptr) {
// 跳过 . 和 ..
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) {
continue;
}
std::string path = std::string("/") + entry->d_name;
struct stat st;
if (stat(path.c_str(), &st) == 0 && st.st_dev == ramdisk_dev) {
// 删除 ramdisk 中的文件
if (S_ISDIR(st.st_mode)) {
DeleteRecursive(path);
} else {
unlink(path.c_str());
}
}
}
LOG(INFO) << "Freed ramdisk resources";
}
15.2 设置启动时间戳
setenv(kEnvFirstStageStartedAt,
std::to_string(start_time.time_since_epoch().count()).c_str(), 1);
性能监控分析:
时间戳格式
boot_clock::time_point:基于启动单调时钟time_since_epoch().count():从启动开始的纳秒数- 精度:纳秒级精度,用于精确性能分析
环境变量传递
- 第二阶段使用:第二阶段 init 可以读取此变量计算总启动时间
- 性能分析:用于启动时间优化和性能监控
- 调试支持:在启动问题时提供时间参考点
16. 移交控制权到第二阶段
const char* path = "/system/bin/init";
const char* args[] = {path, "selinux_setup", nullptr};
auto fd = open("/dev/kmsg", O_WRONLY | O_CLOEXEC);
dup2(fd, STDOUT_FILENO);
dup2(fd, STDERR_FILENO);
close(fd);
execv(path, const_cast<char**>(args));
// execv() 只有出错时才会返回
PLOG(FATAL) << "execv(\"" << path << "\") failed";
控制权移交分析:
标准流重定向
// 打开内核日志设备
int fd = open("/dev/kmsg", O_WRONLY | O_CLOEXEC);
// 重定向标准输出和错误
dup2(fd, STDOUT_FILENO); // fd → 1 (stdout)
dup2(fd, STDERR_FILENO); // fd → 2 (stderr)
// 关闭原始文件描述符
close(fd);
- 确保日志:所有后续输出都进入内核日志系统
- 调试支持:即使在控制台不可用时也能获取日志
- 一致性:保持整个启动过程的日志输出一致性
execv() 系统调用
execv("/system/bin/init", {"init", "selinux_setup", nullptr});
- 进程替换:用新的程序映像完全替换当前进程
- 参数传递:
"init":argv[0],通常是程序名"selinux_setup":argv[1],指示进入启动的第二阶段
- 环境继承:当前环境变量(包括第一阶段设置的时间戳)被继承
- 不会返回:成功时当前进程被完全替换,控制流不会返回
错误处理
// execv() 只有失败时才会返回
if (errno == ENOENT) {
LOG(ERROR) << "Second stage init not found: " << path;
} else if (errno == EACCES) {
LOG(ERROR) << "Permission denied executing: " << path;
} else {
PLOG(ERROR) << "Failed to execute second stage init";
}
// 此处会触发 FATAL 错误,系统无法继续启动
第一阶段的核心
建立的基础设施:
-
完整的设备管理系统
- 动态设备节点创建和管理
- 设备映射器框架初始化
- 逻辑分区设备支持
-
虚拟文件系统体系
- procfs:进程和系统信息接口
- sysfs:硬件设备管理接口
- devpts:伪终端系统
- selinuxfs:安全策略接口
-
日志和调试系统
- 内核日志系统初始化
- 控制台设备建立
- 分级错误报告机制
-
存储和文件系统
- 核心分区挂载(system、vendor 等)
- tmpfs 临时文件系统
- 挂载点命名空间准备
-
安全基础
- 设备权限设置
- 进程隔离准备
- SELinux 接口建立
- 验证启动支持
启动时间线总结
Linux 内核启动
↓
执行 /init (FirstStageMain)
├── 紧急处理设置 (信号处理器)
├── 基础环境初始化 (umask, PATH, 环境变量)
├── 虚拟文件系统建立
│ ├── /dev (tmpfs, 设备节点)
│ ├── /proc (进程信息, hidepid=2安全)
│ ├── /sys (设备信息)
│ └── /sys/fs/selinux (安全策略)
├── 设备节点创建 (kmsg, null, random, ptmx等)
├── 挂载点准备 (/mnt, /debug_ramdisk)
├── 日志系统初始化 (重定向到 kmsg)
├── 内核模块加载 (并行优化)
├── 设备创建和验证设置 (Device-mapper, 逻辑分区)
├── 配置和策略准备 (ramdisk属性, 调试支持)
├── 根目录切换准备 (pivot_root)
├── 第一阶段分区挂载 (多策略选择)
├── 系统根目录切换 (TrySwitchSystemAsRoot)
├── 资源清理和状态设置 (时间戳, 环境变量)
└── 执行第二阶段 init (selinux_setup)
↓
SELinux 初始化阶段...