概述
- 在介绍 Init 之前,先了解一下Android系统的启动过程。从系统角度看,android 系统启动大概分为三个大阶段:bootloader引导、装载,和启动Linux内核,启动Android系统三个大的阶段。其中Android系统的启动还可以分为 启动 Init进程、启动 Zygote 进程、启动 SystemService 、启动 SystemServer 、启动 Home 等多个阶段。
Bootloader 引导
- 当我们按下电源键时,最先运行的就是 Bootloader 。Bootloader作用是初始化基本的硬件设备(如:CPU,内存等),并且通过建立内存空间映射,为加载 Linux 内核准备好合适的运行环境,一旦 Linux 内核装载完毕,Bootloader会从内存中清理掉。
- 如果在 Bootloader 引导期间,按下预定的组合键,可进入系统的特定模式。
装载和启动 Linux 内核
- Android 的 Boot.img 存放的就是 Linux 内核和一个根文件系统。BootLoader会把 Boot.img 映像装载进内存。然后 Linux 内核会执行整个系统的初始化,完成后装载根文件系统,最后启动 Init 进程。
启动 Init 进程(基于 Android9 源码)
- Linux 内核加载完毕后,会首先启动 Init 进程,Init 进程是系统的第一个进程,在 Init启动过程中,会解析 Linux 配置脚本 init.rc 文件的内容,Init 进程会创建文件系统,创建系统目录,初始化属性系统,启动Android系统重要的守护进程,这些进程包括 USB 守护进程,adb 守护进程,vold守护进程,rild守护进程等。最后 Init 进程也作为守护进程来执行修改属性请求,重启崩溃的进程程序等。
Init 进程的初始化过程
- Init 进程源码位于 system/core/init 下。程序的入口main() 函数在init.c中。
main 函数执行流程
- 首先执行的是下面两句:init 进程中包含了另外两个守护进程的代码,启动时如果是另外两个的信号,则启动那两个守护进程。(生成信号链接是在 Android.mk 文件中)
int main(int argc, char** argv) {
if (!strcmp(basename(argv[0]), "ueventd")) {
return ueventd_main(argc, argv);
}
if (!strcmp(basename(argv[0]), "watchdogd")) {
return watchdogd_main(argc, argv);
}
}
if (is_first_stage) {
boot_clock::time_point start_time = boot_clock::now();
umask(0);
umask(0); 函数可以设置属性的掩码,参数为0 意味着创建文件的属性为 0777 。
- 接下来就是创建一些基本目录,包括 /dev ,/porc , /sys 等,同时把一些文件系统 mount到相应目录,部分代码如下,包括一些基于虚拟的文件系统等。部分代码如下:
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));
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);
mount("tmpfs", "/mnt", "tmpfs", MS_NOEXEC | MS_NOSUID | MS_NODEV,
"mode=0755,uid=0,gid=1000");
mkdir("/mnt/vendor", 0755);
LOG(INFO) << "init first stage started!";
SetInitAvbVersionInRecovery();
global_seccomp();
- 接下来 main 函数第二个启动阶段,下面是部分代码
InitKernelLogging(argv);
close(open("/dev/.booting", O_WRONLY | O_CREAT | O_CLOEXEC, 0000));
property_init();
process_kernel_dt();
process_kernel_cmdline();
property_load_boot_defaults();
LoadBootScripts(am, sm);
LoadBootScripts,其中主要探讨的是 parser.ParseConfig("/init.rc"); 解析完成后将 init 文件中的 service 和 action 分别加入到内部的列表中
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);
}
}
- main 函数最后会进入一个无限 for 循环
会执行一些检测,如果一些服务进程意外停止,则执行重启操作。这也是为什么我们在命令行停止一些进程时立刻就会发现有一个新的进程启动出来。
解析启动 init.rc
- Init 进程启动最重要的就是解析 init.rc 。
init 文件格式介绍
-
init.rc 是以块(section)为单位组织的。一个块可以包含多行。块可以分为两大类,一类称为行为(action)一类称为服务(service)。“行为”以 on 开头表示一堆命令的集合,“服务”以 service 开头,表示启动某个进程的方式和参数。“块” 以 on 或者 service 开头,直到下一个 on 或者 service 结束。中间所有行都属于这个“块”。注释以 # 开头 -
无论行为还是服务,都不是以文件的顺序执行的,他们只是存在这里的定义,至于执行与否和何时执行要看init进程在运行时决定的。 -
init 中启动了很多守护进程(官方建议把不必要立即启动的进程放入 Zygote 进程去启动,我发现9.0的 service 启动项好像比 5.0的要少)
service ueventd /sbin/ueventd
class core
critical
seclabel u:r:ueventd:s0
shutdown critical
service console /system/bin/sh
class core
console
disabled
user shell
group shell log readproc
seclabel u:r:shell:s0
setenv HOSTNAME console
init 进程对信号的处理
-
当一个进程调用 exit() 函数退出的时候,会像它的父进程发送 SIGCHID 信号,父进程收到信号后,将释放已经分配和子进程的资源。同时父进程必须执行系统调用 wait() 或 waitpid() 来等待子进程结束,如果父进程不这么做,而且父进程初始化也没调用 sign(SIGCHID)来显式忽略对SIGCHID信号的处理,那么子进程将会一直保持当前退出的状态,这样的进程被称为僵尸(Zombie)进程。 Android 查看僵尸进程是 adb命令下 通过 ps 进程状态为 Z 就是僵尸进程。 -
僵尸进程不能被调度,仅仅在进程列表中占用一个位置,记载该进程退出状态等信息。除此之外僵尸进程不再占用任何内存空间,虽然还留有一些小尾巴,它的危害是什么呢?Linux 系统对每个用户运行进程数量是有限的,如果超过最大限制创建进程将会失败,这就是僵尸进程最大的危害。当然如果只有少量的进程,也不会有什么危害。 -
init 进程是如何处理 SIGCHID 信号的:在main函数中调用下面代码
sigchld_handler_init();
- 信号的初始化是通过 sigaction(SIGCHLD, &act, 0); 函数来完成的
void sigchld_handler_init() {
int s[2];
if (socketpair(AF_UNIX, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0, s) == -1) {
PLOG(FATAL) << "socketpair failed in sigchld_handler_init";
}
signal_write_fd = s[0];
signal_read_fd = s[1];
struct sigaction act;
memset(&act, 0, sizeof(act));
act.sa_handler = SIGCHLD_handler;
act.sa_flags = SA_NOCLDSTOP;
sigaction(SIGCHLD, &act, 0);
ReapAnyOutstandingChildren();
register_epoll_handler(signal_read_fd, handle_signal);
}
- 在 Linux 系统中,信号又称为软中断,信号的到来会中断进程正在处理的工作,因此在信号处理的函数中,不要去调用一些不可重入的函数。而且 Linux 不会对信号进程排列,不管在信号处理期间来了多少个信号,当前的信号处理完成以后,内核只会在发送一个信号给进程,因此为了不丢失信号,我们信号处理函数应该越快越好。
- 但是对于 SIGCHLD 信号,父进程执行等待操作,这个时间比较长,解决这个问题的方式是:创建了一个对本地 socket 进程线程通信,这样当信号来时,只需要向 socket 写入数据就可以返回了,不会耽搁下一个信号处理。
|