学习任何软硬件系统,研究系统启动过程都是一种非常有效地起步手段。上面的这张图可以帮助理解 Android 系统的启动过程。
Android 设备上电后,首先会从处理器上 ROM 的启动引导代码开始执行,片上 ROM 会需找 Boot loader 的代码,并加载到内存中。这一步由”芯片厂商”负责设计和实现。
Bootloader 又称作引导程序, 是操作系统运行之前运行的一段程序,主要有检查 RAM、初始化系统的硬件参数等功能,然后找到 Linux kernel 的代码,设置启动参数,并最终加载到内存中。U-boot 就是一种通用引导程序。
Linux 内核开始启动,初始化各种软硬件环境、加载驱动程序、挂载根文件系统、并执行 init 程序,由此开启 Android 的世界。
启动文件路径: source/kernel/init/main.c
从这一步开始,就真正的迈入了 Android 的世界,init 进程是 android 世界的天子号进程,其他所有的进程都是由 init 进程直接或间接 fork 出来的。
init 进程负责启动系统最关键的几个核心 daemen 守护进程(Zygote、ServiceManager等),Zygote 进程又创建了 dalvik 虚拟机,它是 Java 世界的基础。此外还提供了诸如属性服务(property service)等一些其他的功能。
启动文件路径: source/system/core/init/init.cpp
这篇笔记主要记录 Android 世界的启动流程,也就是 init 进程到主界面点亮的这一段过程。
kernel 运行起来之后会执行 start_kernel 函数,它负责进行 kernel 正式运行之前各个功能的初始化,在 start_kernel 函数的最后调用了 reset_init 函数启动了三个进程(idle、kernel_init、kthreadd),来进行操作系统的正式操作。
- idle 是操作系统的空闲进程,当 cpu 空闲的时候会去运行它
- kernel_init 函数作为进程被启动,但是之后它将读取根文件系统下的init 程序,这个操作将完成从内核态到用户态的转变,
这个 init 进程是所有用户态进程的父进程,它生了大量的子进程,所以init进程将永远存在,其PID是1
- kthreadd 是内核守护进程,其 PID 为 2
下面的代码是 init 进程的具体的启动逻辑:
文件路径:kernel/init/main.c
static int __ref kernel_init(void *unused)
{
// ramdisk_execute_command 这个值为 "./init"
if (ramdisk_execute_command) {
ret = run_init_process(ramdisk_execute_command);
if (!ret)
return 0;
pr_err("Failed to execute %s (error %d)\n",
ramdisk_execute_command, ret);
}
/*
* We try each of these until one succeeds.
*
* The Bourne shell can be used instead of init if we are
* trying to recover a really broken machine.
*/
if (execute_command) {
ret = run_init_process(execute_command);
if (!ret)
return 0;
panic("Requested init %s failed (error %d).",
execute_command, ret);
}
if (!try_to_run_init_process("/sbin/init") ||
!try_to_run_init_process("/etc/init") ||
!try_to_run_init_process("/bin/init") ||
!try_to_run_init_process("/bin/sh"))
return 0;
panic("No working init found. Try passing init= option to kernel. "
"See Linux Documentation/init.txt for guidance.");
}
下面的笔记是真正执行 init 程序的代码,通过 system/core/init/Android.mk 下面对 LOCAL_MODULE_PATH 的定义,可以知道最终 init 可执行文件的安装路径在根文件系统。
LOCAL_MODULE_PATH := $(TARGET_ROOT_OUT)
int main(int argc, char** argv) {
// basename 是 C 库中的一个函数,得到特定的路径中的最后一个'/'后面的内容
if (!strcmp(basename(argv[0]), "ueventd")) {
return ueventd_main(argc, argv);
}
if (!strcmp(basename(argv[0]), "watchdogd")) {
return watchdogd_main(argc, argv);
}
if (REBOOT_BOOTLOADER_ON_PANIC) {
InstallRebootSignalHandlers();
}
add_environment("PATH", _PATH_DEFPATH);
bool is_first_stage = (getenv("INIT_SECOND_STAGE") == nullptr);
}
++程序代码说明++:
1. C++ 中主函数有两个参数,第一个参数 argc 代表参数个数,第二个参数是参数列表
2. 如果程序运行无其他参数,argc = 1,argv[0] = 执行进程的路径
3. init 进程有两个其他入口,ueventd(进入 ueventd_main)以及 watchdogd(进入 watchdogd_main)
rk3288:/sbin # ls -al
lrwxrwxrwx 1 root root 7 1969-12-31 19:00 ueventd -> ../init
lrwxrwxrwx 1 root root 7 1969-12-31 19:00 watchdogd -> ../init
可以看到 watchdog 和 ueventd 是一个软链接,直接链接到 init 程序
所以当执行 /sbin/ueventd 或 /sbin/watchdogd 时,将会进入相应的 ueventd_main 和 watchdogd_main 入口点
文件定义在 source/system/core/init/ueventd.cpp
Android 和 Linux 一样使用设备驱动来访问硬件设备,设备节点文件就是设备驱动的逻辑文件。但是 Android 根文件系统的映像中不存在 “/dev” 目录,该目录是 init 进程启动后动态创建的。
因此,创建 Android 设备节点文件的重任也在 init 进程身上,这就是 ueventd 的工作。
ueventd 通过两种方式创建设备节点文件
第一种方式对应 "冷插拔"(Cold Plug)
即以预先定义的设备信息为基础,当 ueventd 启动后,统一创建设备节点文件。
这一类设备节点文件也被称为静态节点文件。
第二种方式对应 "热插拔"(Hot Plug))
即在系统运行中,当有设备插入 USB 端口时,ueventd 就会接收到这一事件,为 插入的设备动态创建设备节点文件。
这一类设备节点文件也被称为动态节点文件
文件路径:source/system/core/init/ueventd.cpp
int ueventd_main(int argc, char** argv) {
// 创建新建文件的权限默认值
// 与 chmod 相反,这里相当于新建文件后权限为 666
umask(000);
// 初始化日志输出
InitKernelLogging(argv);
LOG(INFO) << "ueventd started!";
// 注册 selinux 相关的用于打印 log 的回调函数
selinux_callback cb;
cb.func_log = selinux_klog_callback;
selinux_set_callback(SELINUX_CB_LOG, cb);
DeviceHandler device_handler = CreateDeviceHandler();
// 创建 socket,用于监听 uevent 事件
UeventListener uevent_listener;
// 通过 access 判断文件 /dev/.coldboot_done 是否存在
// 若已经存在则表明已经进行过冷插拔了
if (access(COLDBOOT_DONE, F_OK) != 0) {
ColdBoot cold_boot(uevent_listener, device_handler);
cold_boot.Run();
}
// We use waitpid() in ColdBoot, so we can't ignore SIGCHLD until now.
signal(SIGCHLD, SIG_IGN);
// Reap and pending children that exited between the last call to waitpid() and setting SIG_IGN
// for SIGCHLD above.
while (waitpid(-1, nullptr, WNOHANG) > 0) {
}
// 监听事件,进行热插拔处理
uevent_listener.Poll([&device_handler](const Uevent& uevent) {
HandleFirmwareEvent(uevent);
device_handler.HandleDeviceEvent(uevent);
return ListenerAction::kContinue;
});
return 0;
}
“看门狗”本身是一个定时器电路,内部会不断的进行计时(或计数)操作,计算机系统和”看门狗”有两个引脚相连接,正常运行时每隔一段时间就会通过其中一个引脚向”看门狗”发送信号,”看门狗”接收到信号后会将计时器清零并重新开始计时。
一旦系统出现问题,进入死循环或任何阻塞状态,不能及时发送信号让”看门狗”的计时器清零,当计时结束时,”看门狗”就会通过另一个引脚向系统发送”复位信号”,让系统重启。
watchdogd_main 主要是定时器作用,而 DEV_NAME 就是那个引脚
主要操作就是”喂狗”,往 DEV_NAME 写入数据复位信号
文件路径:source/system/core/init/watchdogd.cpp
int fd = open(DEV_NAME, O_RDWR|O_CLOEXEC);
if (fd == -1) {
PLOG(ERROR) << "Failed to open " << DEV_NAME;
return 1;
}
...
while (true) {
write(fd, "", 1);
sleep(interval);
}
文件路径:source/system/core/init/init.cpp
if (REBOOT_BOOTLOADER_ON_PANIC) {
install_reboot_signal_handlers();
}
REBOOT_BOOTLOADER_ON_PANIC 在顶层 init 模块的 mk 文件中定义,userdebug 和 eng 版本的固件会打开该选项。
主要作用是:当 init 进程崩溃时,重启 bootloader,让用户更容易定位问题。
install_reboot_signal_handlers 函数将各种信号量,如 SIGABRT、SIGBUS 等的行为设置为 SA_RESTART,一旦监听到这些信号即执行重启系统。
在 init 的代码中根据环境变量 INIT_SECOND_STAGE 执行两条分路的代码,第一次执行完成之后,就将 INIT_SECOND_STAGE 的值设置为 true,然后重新执行一遍 init 程序,走第二条分路的代码,在下面的记录中将 init 第一阶段的执行称为内核态执行,第二阶段的执行称为用户态执行。
文件路径:文件路径:source/system/core/init/init.cpp
bool is_first_stage = (getenv("INIT_SECOND_STAGE") == nullptr);
if (is_first_stage) {
boot_clock::time_point start_time = boot_clock::now();
// Clear the umask.
umask(0); // 设置 umask 值为0,清空访问权限屏蔽码
// 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.
mount("tmpfs", "/dev", "tmpfs", MS_NOSUID, "mode=0755");
mkdir("/dev/pts", 0755);
mkdir("/dev/socket", 0755);
mount("devpts", "/dev/pts", "devpts", 0, NULL);
#define MAKE_STR(x) __STRING(x)
mount("proc", "/proc", "proc", 0, "hidepid=2,gid=" MAKE_STR(AID_READPROC));
// Don't expose the raw commandline to unprivileged processes.
chmod("/proc/cmdline", 0440);
gid_t groups[] = { AID_READPROC };
setgroups(arraysize(groups), groups);
mount("sysfs", "/sys", "sysfs", 0, NULL);
mount("selinuxfs", "/sys/fs/selinux", "selinuxfs", 0, NULL);
mknod("/dev/kmsg", S_IFCHR | 0600, makedev(1, 11));
mknod("/dev/random", S_IFCHR | 0666, makedev(1, 8));
mknod("/dev/urandom", S_IFCHR | 0666, makedev(1, 9));
...
}
...
init 的 main 函数会执行两次,由 is_first_stage 环境变量控制
android init 进程在内核态的执行过程中,需要挂载上基本的文件系统。
文件系统相关函数的说明的介绍可以查看 这里
其中,/dev/ 分区是临时文件系统 tmpfs,使用 RAM 将所有的文件储存在虚拟内存中,
主要用于创建和存放设备文件节点,该分区可根据需要动态调整。
/sys/ 分区使用 sysfs 文件系统,把连接在系统上的设备和总线组织成为一个分级的文件,使得它们可以在用户空间存取。
/proc/ 分区使用 proc 文件系统,proc 文件系统是一个非常重要的虚拟文件系统,它可以看作是内核内部数据结构的接口。通过它我们可以获得系统的信息,同时也能够在运行时修改特定的内核参数。
selinuxfs 是虚拟文件系统,通常挂载在 /sys/fs/selinux,用来存放 SELinux 安全策略文件。
文件路径:文件路径:source/system/core/init/init.cpp
// Now that tmpfs is mounted on /dev and we have /dev/kmsg, we can actually
// talk to the outside world...
InitKernelLogging(argv);
这句的作用就是将 KernelLogger 函数作为 log 日志的处理函数,KernelLogger 主要作用就是将要输出的日志格式化之后写入到 /dev/kmsg 设备中。
文件路径:文件路径:source/system/core/init/init_first_stage.cpp
bool DoFirstStageMount() {
// Skips first stage mount if we're in recovery mode.
if (IsRecoveryMode()) {
LOG(INFO) << "First stage mount skipped (recovery mode)";
return true;
}
// Firstly checks if device tree fstab entries are compatible.
if (!is_android_dt_value_expected("fstab/compatible", "android,fstab")) {
LOG(INFO) << "First stage mount skipped (missing/incompatible fstab in device tree)";
return true;
}
std::unique_ptr<FirstStageMount> handle = FirstStageMount::Create();
if (!handle) {
LOG(ERROR) << "Failed to create FirstStageMount";
return false;
}
return handle->DoFirstStageMount();
}
Android 8.1 系统将 system、vendor 分区的挂载功能移植到 kernel device-tree 中进行。
在 kernel 的 dts 文件中,需要包含如下的 firmware 分区挂载节点,在 DoFirstStageMount 函数执行过程中会检查、读取 device-tree 中记录的分区挂载信息。
firmware {
android {
compatible = "android,firmware";
fstab {
compatible = "android,fstab";
system {
compatible = "android,system";
dev = "/dev/block/by-name/system";
type = "ext4";
mnt_flags = "ro,barrier=1,inode_readahead_blks=8";
fsmgr_flags = "wait";
};
vendor {
compatible = "android,vendor";
dev = "/dev/block/by-name/vendor";
type = "ext4";
mnt_flags = "ro,barrier=1,inode_readahead_blks=8";
fsmgr_flags = "wait";
};
};
};
};
// Firstly checks if device tree fstab entries are compatible.
if (!is_android_dt_value_expected("fstab/compatible", "android,fstab")) {
LOG(INFO) << "First stage mount skipped (missing/incompatible fstab in device tree)";
return true;
}
android device-tree 目录默认在 /proc/device-tree/firmware/android 下,如果 kernel 的启动参数 /proc/cmdline 中包含 androidboot.android_dt_dir 值得设定,则直接使用。
首先确认 android device-tree 目录下 fstab/compatible 目录属性值是不是 “android,fstab”。
dts compatible 节点的组织形式为 ,。 android,fstab 代表执行的功能为 fstab 分区挂载。
文件路径:文件路径:source/system/core/init/init_first_stage.cpp
FirstStageMount::FirstStageMount()
: need_dm_verity_(false), device_tree_fstab_(fs_mgr_read_fstab_dt(), fs_mgr_free_fstab) {
if (!device_tree_fstab_) {
LOG(ERROR) << "Failed to read fstab from device tree";
return;
}
// Stores device_tree_fstab_->recs[] into mount_fstab_recs_ (vector)
// for easier manipulation later, e.g., range-base for loop.
for (int i = 0; i < device_tree_fstab_->num_entries; i++) {
mount_fstab_recs_.push_back(&device_tree_fstab_->recs[i]);
}
}
bool FirstStageMount::DoFirstStageMount() {
// Nothing to mount.
if (mount_fstab_recs_.empty()) return true;
if (!InitDevices()) return false;
if (!MountPartitions()) return false;
return true;
}
FirstStageMount 的构造函数中通过 fs_mgr_read_fstab_dt 函数读取 /proc/device-tree/firmware/android/fstab 目录下的分布挂载信息,最后统计成 fstab_rec 类型的 vector 数组,
struct fstab_rec {
char* blk_device;
char* mount_point;
char* fs_type;
unsigned long flags;
char* fs_options;
int fs_mgr_flags;
char* key_loc;
char* key_dir;
char* verity_loc;
long long length;
char* label;
int partnum;
int swap_prio;
int max_comp_streams;
unsigned int zram_size;
uint64_t reserved_size;
unsigned int file_contents_mode;
unsigned int file_names_mode;
unsigned int erase_blk_size;
unsigned int logical_blk_size;
};
MountPartitions() 函数遍历 fstab_rec 数组,找到 mount_source 和 mount_target,使用 mount 函数将 system、vendor或者 oem 分区挂载上。
成功挂载的 Log 打印如下:
[ 1.608773] init: [libfs_mgr]__mount(source=/dev/block/by-name/system,target=/system,type=ext4)=0: Success
[ 1.611679] init: [libfs_mgr]__mount(source=/dev/block/by-name/vendor,target=/vendor,type=ext4)=0: Success
SELinux是「Security-Enhanced Linux」的简称,是美国国家安全局「NSA=The National Security Agency」
和 SCC(Secure Computing Corporation)开发的 Linux 的一个扩张强制访问控制安全模块。
在这种访问控制体系的限制下,进程只能访问那些在他的任务中所需要文件。
SELinux 在 Andoird 中的具体应用可以点击 android 8.1 安全机制 — SEAndroid & SELinux
具体的源码分析查看 4.2 节介绍
这里主要就是设置一些变量如 INIT_SECOND_STAGE、INIT_STARTED_AT,为第二阶段做准备,然后再次调用 init 的 main 函数,启动用户态的 init 进程
if (is_first_stage) {
...
setenv("INIT_SECOND_STAGE", "true", 1);
static constexpr uint32_t kNanosecondsPerMillisecond = 1e6;
uint64_t start_ms = start_time.time_since_epoch().count() / kNanosecondsPerMillisecond;
setenv("INIT_STARTED_AT", StringPrintf("%" PRIu64, start_ms).c_str(), 1);//记录第二阶段开始时间戳
char* path = argv[0];
char* args[] = { path, nullptr };
execv(path, args); //重新执行main方法,进入第二阶段
// execv() only returns if an error happened, in which case we
// panic and never fall through this conditional.
PLOG(ERROR) << "execv(\"" << path << "\") failed";
security_failure();
}
init 进程第一阶段做的主要工作是
1. 挂载分区 dev、system、vendor
2. 创建设备节点及设备节点热插拔事件监听处理(ueventd)
3. 创建一些关键目录、初始化日志输出系统
4. 启用 SELinux 安全策略