Framework篇--开机启动init进程

基于 Android 6.0.0 源码进行分析

init 进程涉及的源码:

  • /system/core/init/init.cpp

概述

init 进程是 Linux 系统中用户空间的第一个进程,其进程号为 1。在 Kernel 启动后,在用户空间启动 init 进程,并调用 init 中的 main() 方法执行 init 进程的职责。对于 init 进程的功能,通过分析其 main() 方法的逻辑,主要可以划分为以下 5 部分:

  • 挂载、创建所系统运行所需要的目录,如 tmpfsptsproc
  • 加载属性服务;
  • 解析并运行所有的 init.rc 相关的文件;
  • 化身为守护进程,处理signal、property和keychord等服务;

接下来,让我们一起进入 Android Framework 源码的世界吧!

main

[->init.cpp]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
int main(int argc, char** argv) {
if (!strcmp(basename(argv[0]), "ueventd")) {
//
return ueventd_main(argc, argv);
}

if (!strcmp(basename(argv[0]), "watchdogd")) {
//watchdogd 的作用 {崩溃 & 死机} =》重启
return watchdogd_main(argc, argv);
}

// Clear the umask.
// 设置文件属性
umask(0);

add_environment("PATH", _PATH_DEFPATH);

bool is_first_stage = (argc == 1) || (strcmp(argv[1], "--second-stage") != 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.
if (is_first_stage) {
mount("tmpfs", "/dev", "tmpfs", MS_NOSUID, "mode=0755");
mkdir("/dev/pts", 0755);
mkdir("/dev/socket", 0755);
mount("devpts", "/dev/pts", "devpts", 0, NULL);
mount("proc", "/proc", "proc", 0, NULL);
mount("sysfs", "/sys", "sysfs", 0, NULL);
}

// We must have some place other than / to create the device nodes for
// kmsg and null, otherwise we won't be able to remount / read-only
// later on. Now that tmpfs is mounted on /dev, we can actually talk
// to the outside world.
open_devnull_stdio();
// 初始化内核log
klog_init();
// 给内核 log 设置日志 5,表示当 log 的日志小于 5 时则会把日志输出到 kernel log 上,
// 默认的日志等级为 3;KLOG_DEFAULT_LEVEL ==》3
klog_set_level(KLOG_NOTICE_LEVEL);

NOTICE("init%s started!\n", is_first_stage ? "" : " second stage");

if (!is_first_stage) {
// Indicate that booting is in progress to background fw loaders, etc.
close(open("/dev/.booting", O_WRONLY | O_CREAT | O_CLOEXEC, 0000));
// 创建一块共享的内存空间,用于属性服务
property_init();

// If arguments are passed both on the command line and in DT,
// properties set in DT always have priority over the command-line ones.
process_kernel_dt();
process_kernel_cmdline();

// Propogate the kernel variables to internal variables
// used by init as well as the current required properties.
export_kernel_boot_props();
}

// Set up SELinux, including loading the SELinux policy if we're in the kernel domain.
selinux_initialize(is_first_stage);

// If we're in the kernel domain, re-exec init to transition to the init domain now
// that the SELinux policy has been loaded.
if (is_first_stage) {
if (restorecon("/init") == -1) {
ERROR("restorecon failed: %s\n", strerror(errno));
security_failure();
}
char* path = argv[0];
char* args[] = { path, const_cast<char*>("--second-stage"), nullptr };
if (execv(path, args) == -1) {
ERROR("execv(\"%s\") failed: %s\n", path, strerror(errno));
security_failure();
}
}

// These directories were necessarily created before initial policy load
// and therefore need their security context restored to the proper value.
// This must happen before /dev is populated by ueventd.
INFO("Running restorecon...\n");
restorecon("/dev");
restorecon("/dev/socket");
restorecon("/dev/__properties__");
restorecon_recursive("/sys");
// 初始化epoll功能
epoll_fd = epoll_create1(EPOLL_CLOEXEC);
if (epoll_fd == -1) {
ERROR("epoll_create1 failed: %s\n", strerror(errno));
exit(1);
}

// 处理子进程挂掉的情况,主要的核心有二:
// 1.通过挂掉的 pid 找到对应的 svc,将僵尸进程 kill 掉,达到释放资源的目的;
// 2.根据标志位确认是否重启,如果重启则设置属性"init.svc.{$service_name}" "restarting",供别处获取;
signal_handler_init();
// 加载default.prop文件
property_load_boot_defaults();
// 启动属性服务器,此处会调用epoll_ctl设置property fd可读的回调函数
start_property_service();
// 解析init.rc文件
init_parse_config_file("/init.rc");
// 执行rc文件中触发器为on early-init的语句
action_for_each_trigger("early-init", action_add_queue_tail);

// Queue an action that waits for coldboot done so we know ueventd has set up all of /dev...
queue_builtin_action(wait_for_coldboot_done_action, "wait_for_coldboot_done");
// ... so that we can start queuing up actions that require stuff from /dev.
queue_builtin_action(mix_hwrng_into_linux_rng_action, "mix_hwrng_into_linux_rng");
queue_builtin_action(keychord_init_action, "keychord_init");
// 设备组合键的初始化操作,此处会调用epoll_ctl设置keychord fd可读的回调函数
// console_init_action ==》这便是开机显示的底部带ANDROID字样的画面的回调函数
queue_builtin_action(console_init_action, "console_init");


// Trigger all the boot actions to get us started.
// 执行rc文件中触发器为on init的语句
action_for_each_trigger("init", action_add_queue_tail);

// Repeat mix_hwrng_into_linux_rng in case /dev/hw_random or /dev/random
// wasn't ready immediately after wait_for_coldboot_done
queue_builtin_action(mix_hwrng_into_linux_rng_action, "mix_hwrng_into_linux_rng");

// Don't mount filesystems or start core system services in charger mode.
char bootmode[PROP_VALUE_MAX];
// 当处于充电模式,则charger加入执行队列;否则late-init加入队列。
if (property_get("ro.bootmode", bootmode) > 0 && strcmp(bootmode, "charger") == 0) {
action_for_each_trigger("charger", action_add_queue_tail);
} else {
action_for_each_trigger("late-init", action_add_queue_tail);
}

// Run all property triggers based on current state of the properties.
// 触发器为属性是否设置
queue_builtin_action(queue_property_triggers_action, "queue_property_triggers");

// init化身守护进程
while (true) {
if (!waiting_for_exec) {
// 执行命令列表中的命令
execute_one_command();
// 检查service_list中的所有服务,对于带有SVC_RESTARTING标志的服务,则都会调用其相应的
// restart_service_if_needed
restart_processes();
}

int timeout = -1;
if (process_needs_restart) {
timeout = (process_needs_restart - gettime()) * 1000;
if (timeout < 0)
timeout = 0;
}

if (!action_queue_empty() || cur_action) {
timeout = 0;
}
//
bootchart_sample(&timeout);

epoll_event ev;
// 循环等待事件发生,到这里,init 进程进入 epoll_wait 的状态
int nr = TEMP_FAILURE_RETRY(epoll_wait(epoll_fd, &ev, 1, timeout));
if (nr == -1) {
// ==》对应 klog_init();
ERROR("epoll_wait failed: %s\n", strerror(errno));
} else if (nr == 1) {
// 调用epoll_event事件存储的函数指针处理事件
((void (*)()) ev.data.ptr)();
}
}
return 0;
}

挂载文件系统

init 进程初始化的过程会分别挂载 tmpfsdevptsprocsysfs 文件系统。

tmpfs文件系统

tmpfs是一种虚拟内存文件系统,因此它会将所有的文件存储在虚拟内存中。并且tmpfs下的所有内容均为临时性的内容。tmpfs是一个独立的文件系统,不是块设备,只要挂接就可以使用。同时,mpfs是驻留在RAM的,因此它的内容是不持久的,断电后,tmpfs的内容就消失了,这也是被称作tmpfs的原因。

devpts文件系统

devpts文件系统为伪终端提供了一个标准接口,它的标准挂接点是/dev/pts。只要pty的主复合设备/dev/ptmx被打开,就会在/dev/pts下动态的创建一个新的pty设备文件。

proc文件系统

proc文件系统是一个非常重要的虚拟文件系统,它可以看着是内核数据结构的接口,通过它我们可以获取到系统的信息,同时也可以在运行时修改特定的内核参数。

sysfs文件系统

与proc文件系统类似,sysfs文件系统也是一个不占有任何磁盘空间的虚拟文件系统。它通常被挂接在/sys目录下。sysfs文件系统是Linux2.6内核引入的,它把连接在系统上的设备和总线组织成为一个分级的文件,使得它们可以在用户空间存取。

准备klog

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void klog_init(void) {
if (klog_fd >= 0) return; /* Already initialized */

// 内核 log 文件,可以通过 cat /dev/kmsg 来查看系统 log 日志
// O_CLOEXEC ==》原子性操作
klog_fd = open("/dev/kmsg", O_WRONLY | O_CLOEXEC);
if (klog_fd >= 0) {
return;
}

// mknod [选项] 设备名 设备类型 主设备号 次设备号
// 创建 __kmsg__ 设备
static const char* name = "/dev/__kmsg__";
if (mknod(name, S_IFCHR | 0600, (1 << 8) | 11) == 0) {
klog_fd = open(name, O_WRONLY | O_CLOEXEC);
unlink(name);
}
}

这里通过 open 函数打开 /dev/kmsg 设备节点。例如可以通过 cat /dev/kmsg 来打印内核的 log 日志。

然后通过 klog_set_level(5) 设备日志输出的条件。当日志小于 5 的时候,会输出日志到文件 /dev/kmsg

属性服务

属性服务在 init 守护进程中运行。每一个客户端想要设置属性时,必须连接属性服务,再向其发送信息。属性服务会在共享内存中修改、创建属性。

工作原理图:

[->\system\core\init\property_service.cpp]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void property_init() {
// 保证只初始化 /dev/__properties__ 一次
if (property_area_initialized) {
return;
}
property_area_initialized = true;
// 通过 mmap 初始化 /dev/__properties__
if (__system_property_area_init()) {
return;
}
pa_workspace.size = 0;
pa_workspace.fd = open(PROP_FILENAME, O_RDONLY | O_NOFOLLOW | O_CLOEXEC);
if (pa_workspace.fd == -1) {
ERROR("Failed to open %s: %s\n", PROP_FILENAME, strerror(errno));
return;
}
}

其中,主要的核心的主要操作是在方法 __system_property_area_init 中:

  • 通过 open 打开 /dev/__properties__ 文件;
  • 通过 mmap 内存映射到进程中,并以共享内存的方式分配 #define PA_SIZE (128 * 1024) 大小的内存区域,这样其他进程就可以跨进程访问属性区域了;
  • 然后把内存的首地址保存在全局变量 __system_property_area__ 中,后续的增加或者修改属性都是基于该变量来计算位置。
#1.0 start_property_service
1
2
3
4
5
6
7
8
9
10
11
void start_property_service() {
// 创建名字为 property_service 的 socket
property_set_fd = create_socket(PROP_SERVICE_NAME, SOCK_STREAM | SOCK_CLOEXEC | SOCK_NONBLOCK,0666, 0, 0, NULL);
if (property_set_fd == -1) {
ERROR("start_property_service socket creation failed: %s\n", strerror(errno));
exit(1);
}
listen(property_set_fd, 8);
// 通过 epoll_ctl 设置 property 文件描述符触发可读时的回调函数为 handle_property_set_fd
register_epoll_handler(property_set_fd, handle_property_set_fd);
}

handle_property_set_fd 回调函数主要是针对 socket 接收的事件进行处理,同时在设置属性的时候,也会对属性名称进行检查是否合法。

解析 init.rc 文件

init 进程中,配置文件是指 init.rc 文件,其路径是:

\system\core\rootdir\init.rc

文件 init.rc 包含四种状态类被,分别是 Action、Commands、Services 和 Options。当 声明一个 Services 或 Action 的时候,它将隐式声明一个 section,它之后跟随的 Command 或者 Option 都属于这个 section。

Action

Action 就是在某种条件下触发的一系列命令,通常有一个 trigger,通过触发器 trigger ,即以 on 开头的语句来决定执行相应的 Service 的时机,具体的时机如下:

  • on early-init:在初始化早期阶段触发;
  • on init:在初始化阶段触发;
  • on late-init:在初始化阶段触发;
  • on boot/charger:当系统启动/充电时触发;
  • on property:=:当属性值满足条件时触发;

通常的形式如下:

1
2
3
on <trigger>
<command>
<command>

如下例子:

1
2
3
4
5
6
on early-init
# Set init and its forked children's oom_adj.
write /proc/1/oom_score_adj -1000
# Set the security context of /adb_keys if present.
restorecon /adb_keys
start ueventd

下面列举常用的 command 命令:

  • class_start : 启动属于同一个class的所有服务;
  • start : 启动指定的服务,若已启动则跳过;
  • stop : 停止正在运行的服务;
  • setprop :设置属性值;
  • mkdir :创建指定目录;
  • symlink : 创建连接到符号链接;
  • write : 向文件path中写入字符串;
  • exec: fork并执行,会阻塞init进程直到程序完毕;
  • exprot :设定环境变量;
  • loglevel :设置log级别;

Service

服务Service,以 service开头,由init进程启动,一般运行在init的一个子进程,所以启动service前需要判断对应的可执行文件是否存在。init生成的子进程,定义在rc文件,其中每一个service在启动时会通过fork方式生成子进程。

Service 的结构如下:

1
2
3
service <name> <pathname> [<argument>]*
<option>
<option>

如下例子:

1
2
3
4
5
service surfaceflinger /system/bin/surfaceflinger
class core
user system
group graphics drmrpc
onrestart restart zygote

在上述的例子中,surfaceflinger 表示为服务名,而 /system/bin/surfaceflinger 表示的是服务的执行路径。

其中,**option **是 Service 的修饰词,也可以理解为 Service 的可选项,和 Service 配合使用。主要包括以下的选项:

  • critical:表示如果服务在 4 分钟重启的次数多于 4 次,则系统重启到 recovery mode;
  • disabled:表示服务不会自动重启,需要手动调用名字重启。
  • setEnv : 设置启动环境变量;
  • socket [ []]: 开启一个 unix 域的 socket,名字为 /dev/socket/, 只能是 dgram 或者stream, 默认为 0;
  • user: 表示将用户切换为 , 用户名已经定义好了,只能是 /system/root;
  • group:表示将组切换为
  • oneshot:表示这个 Service 只启动一次;
  • class: 指定一个要启动的类,这个类中如果有多个 Service,将会被同时启动。默认的class 将会 “default”;
  • onrrestart: 在重启时执行一条命令;
分析 init.rc 的过程

init 是在 main 方法中通过 init_parse_config_file 来解析 init.rc 文件的。整个解析的流程可以总结为:

init_parse_config_file >> read_file >> parse_config

其中 init_parse_config_file 、以及 parse_config 主要是通过 for 循环对文件 init.rc 的内容进行解析,以一行一行的形式进行读取。

最后会来到 parse_config 中对读取到的每一行进行解析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
static void parse_config(const char *fn, const std::string& data)
{
struct listnode import_list;
struct listnode *node;
char *args[INIT_PARSER_MAXARGS];

int nargs = 0;

parse_state state;
state.filename = fn;
state.line = 0;
state.ptr = strdup(data.c_str()); // TODO: fix this code!
state.nexttoken = 0;
state.parse_line = parse_line_no_op;

list_init(&import_list);
state.priv = &import_list;

for (;;) {
switch (next_token(&state)) {
case T_EOF: // 文件尾
state.parse_line(&state, 0, 0);
goto parser_done;
case T_NEWLINE: // 新的一行
state.line++;
if (nargs) {
int kw = lookup_keyword(args[0]);
if (kw_is(kw, SECTION)) {
state.parse_line(&state, 0, 0);
parse_new_section(&state, kw, nargs, args);
} else {
state.parse_line(&state, nargs, args);
}
nargs = 0;
}
break;
case T_TEXT: // 内容文本
if (nargs < INIT_PARSER_MAXARGS) {
args[nargs++] = state.text;
}
break;
}
}
... //省略部分代码
}

其中上面关键的代码是 lookup_keyword 主要是对每一行的第一个字符进行进行 case 判断。如常用的 service 和 on 经过 lookup_keyword 解析会返回 K_serviceK_on ,随后通过 kw_is(kw, type) 判断是否属于 SECTION 类型,而在 init.rc 文件中只有 service 和 on 满足该类型。

启动服务
1
2
3
4
5
6
7
8
9
10
11
on early-init
...
on init
# Mount filesystems and start core system services.
on late-init
...
on post-fs // 挂载文件系统
...
on post-fs-data //挂载data
...
on boot //启动核心服务

init.rc 文件可以看出该文件的解析是按照一定的顺序进行解析的。即以 early-init >> init >> late-init 的顺序进行解析。

Zygote

zygote 服务被定义在 init.zygote.rc 文件中,与 init.rc 在相同的目录下,即 system/core/rootdir 目录下。

1
2
3
4
5
6
7
service zygote /system/bin/app_process64 -Xzygote /system/bin --zygote --start-system-server --socket-name=zygote
class main
socket zygote stream 660 root system
onrestart write /sys/android_power/request_state wake
onrestart write /sys/power/state on
onrestart restart media
onrestart restart netd

其中,会执行到 zygote 的源代码, 其源代码位于 /framework/base/cmds/app_process/APP_main.cpp 文件的 main 下,但在这里先不对其详细的逻辑进行展开。

信号处理

# 1.0 restart_processes

[->init.cpp]

1
2
3
4
5
6
static void restart_processes()
{
process_needs_restart = 0;
service_for_each_flags(SVC_RESTARTING,
restart_service_if_needed);
}

主要的作用是检查 service_list 中的所有服务,对带有 SVC_RESTARTING 标志的服务,则会去调用方法 restart_service_if_needed

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static void restart_service_if_needed(struct service *svc)
{
time_t next_start_time = svc->time_started + 5;

if (next_start_time <= gettime()) {
svc->flags &= (~SVC_RESTARTING);
service_start(svc, NULL);
return;
}

if ((next_start_time < process_needs_restart) ||
(process_needs_restart == 0)) {
process_needs_restart = next_start_time;
}
}

然后会去调用 service_start 方法来启动服务。

# 1.2 signal_handler_init

init 进程的 main 方法中,通过调用 signal_handler_init 方法来初始化对 signal 信号的处理。其主要的处理逻辑有以下:

  • sigaction 动作注册需要监听的信号类型;
  • 通过 waitpid 循环监听子进程的结束情况,存在子进程退出,便给退出的子进程标记上 SVC_RESTARTING;
  • 通过 epoll_ctl 把需要监听的 socket fd 注册在 epoll_fd 上,当 socket fd 发生变化,便调用 handle_signal 处理子进程退出的情况;
# 1.2.1 signal_handler_init
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
void signal_handler_init() {
// 机制 ==》 mechanism
// Create a signalling mechanism for SIGCHLD.
int s[2];
// 创建socket pair
// socketpair()函数用于创建一对无名的、相互连接的套接字;
// 如果函数成功,则返回0,创建好的套接字分别是sv[0]和sv[1];否则返回-1,错误码保存于errno中;
// socketpair的用法和理解 ===》》 https://blog.csdn.net/weixin_40039738/article/details/81095013
if (socketpair(AF_UNIX, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0, s) == -1) {
ERROR("socketpair failed: %s\n", strerror(errno));
exit(1);
}
signal_write_fd = s[0];
signal_read_fd = s[1];
// 当捕获信号SIGCHLD,则写入signal_write_fd
// Write to signal_write_fd if we catch SIGCHLD.
struct sigaction act;
memset(&act, 0, sizeof(act));
// SA_NOCLDSTOP使init进程只有在其子进程终止时才会收到SIGCHLD信号
// 设置信号处理函数句柄,当有信号产生时,会向上面创建的socket写入数据,
// epoll监控到该socket对中的fd可读时,就会调用注册的函数去处理该事件;(写入数据)
act.sa_handler = SIGCHLD_handler;
// 设置标志,表示只有当子进程终止时才接受SIGCHID信号
act.sa_flags = SA_NOCLDSTOP; //sa_nocldstop
// 注册监听 SIGCHLD 信号
sigaction(SIGCHLD, &act, 0);

// 进入waitpid来处理子进程是否退出的情况
reap_any_outstanding_children();
// (读取数据)==> 当 signal_read_fd 有数据来的时候,会调用 handle_signal 函数进行
register_epoll_handler(signal_read_fd, handle_signal);
}

这里初始化两个 signal_write_fd、以及 signal_read_fd 读写端,作用于当系统发出指定的 SIGCHLD 信号时,通知执行 handle_signal

1
2
3
4
5
6
7
static void handle_signal() {
// Clear outstanding requests.
char buf[32];
// 读取 signal_read_fd 中的数据,并放入 buf 中;
read(signal_read_fd, buf, sizeof(buf));
reap_any_outstanding_children();
}

最后在 handle_signal 中,会去调用到 reap_any_outstanding_children

1
2
3
4
static void reap_any_outstanding_children() {
while (wait_for_one_process()) {
}
}

reap_any_outstanding_children 中,如果 wait_for_one_process 在返回 false 的情况下,会一直循环,通过 wait_for_one_process 里面的 waitpid 来等待可能的子进程退出的情况。当监听到子进程存在退出的情况,那么最后会给该子进程打上 SVC_RESTARTING 的标记。

总结

init 进程(pid=1)是 Linux 系统中用户空间的第一个进程,主要的工作如下:

  • 初始化属性服务;
  • 解析各个 rc 文件,并启动相应的服务进程;
  • 初始化 epoll,依次执行 signal 等回调函数;
  • 进入无限循环状态,主要做如下工作:
    • 检查 service_list 中的所有服务,对于带有 SVC_RESTARTING 标志的服务,则都会调用其相应的restart_service_if_needed;(检查是否有需要重启的进程)
    • 进入 epoll_wait 状态,直到系统属性发生变化,或者有 keychord 键盘输入事件,则会等待状态,执行相应的回调函数;

本文标题:Framework篇--开机启动init进程

文章作者:

发布时间:2021年07月05日 - 22:07

最后更新:2021年07月05日 - 23:07

原始链接:https://hndroid.github.io/2021/07/05/Framework%E7%AF%87-%E5%BC%80%E6%9C%BA%E5%90%AF%E5%8A%A8init%E8%BF%9B%E7%A8%8B/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。