Android4.4的init进程
Posted 悠然红茶
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android4.4的init进程相关的知识,希望对你有一定的参考价值。
android4.4的init进程
侯 亮
1背景
前些日子需要在科室内做关于Android系统启动流程的培训。为此,我在几年前的技术手记的基础上,重新改了一份培训文档。在重新整理文档期间,我也重读了一下Android 4.4的相关代码,发现还有一些东西是我以前一直没重视过的,所以打算写下来总结一二。
我以前之所以没有把关于Android系统启动方面的手记整理成博文,主要是因为网上已经有许多类似的文章了,再说一遍好像也没什么意思。但这次的培训既然已迫使我重整了一份文档,那么倒也不妨贴出来供大家参考。文中的某些细节是我最近新补充的内容,这样或许能和网上其他文章有所区别吧。
2概述init进程
我们先概述一下Android的init进程。init是Linux系统中,用户空间的第一个进程。它负责创建系统中最关键的几个子进程,尤其是zygote。另外,init还提供了property service(属性服务),类似于windows系统的注册表服务。有关属性服务的细节,大家可参考我写的《Android Property机制》一文,本文就不多说了。
在Android系统中,会有个init.rc脚本。Init进程一启动就会读取并解析这个脚本文件,把其中的元素整理成自己的数据结构(链表)。具体情况可参考system\\core\\init\\init.c文件,它的main()函数会先调用init_parse_config_file(“/init.rc”)来解析init.rc脚本,分析出应该执行的语义,并且把脚本中描述的action和service信息分别组织成双向链表,然后执行之。示意图如下:
3解析init.rc脚本
3.1介绍init.rc脚本
Init.rc脚本使用的是一种初始化语言,其中包含了4类声明:
1)Action
2)Command
3)Service
4)Option
该语言规定,Action和Service是以一种“小节”(Section)的形式出现的,其中每个Action小节可以含有若干Command,而每个Service小节可以含有若干Option。小节只有起始标记,却没有明确的结束标记,也就是说,是用“后一个小节”的起始来结束“前一个小节”的。
脚本中的Action大体上表示一个“行动”,它用一系列Command共同完成该“行动”。Action需要有一个触发器(trigger)来触发它,一旦满足了触发条件,这个Action就会被加到执行队列的末尾。Action的形式如下:
on <trigger>
<command1>
<command2>
......
?
Service表示一个服务程序,会在初始化时启动。因为init.rc脚本中描述的服务往往都是核心服务,所以(基本上所有的)服务会在退出时自动重启。Service的形式如下:
service <name> <pathname> [<arguments>]*
<option>
<option>
......
?
Init.rc中的Service截选如下:
service servicemanager /system/bin/servicemanager
class core
user system
group system
critical
onrestart restart healthd
onrestart restart zygote
onrestart restart media
onrestart restart surfaceflinger
onrestart restart drm
service vold /system/bin/vold
class core
socket vold stream 0660 root mount
ioprio be 2
service netd /system/bin/netd
class main
socket netd stream 0660 root system
socket dnsproxyd stream 0660 root inet
socket mdns stream 0660 root system
请大家留心service里的class选项,比如上面的class core和class main。它表示该service是属于哪种类型的服务。在后文的阐述boot子阶段时,会用到这个概念。
其实,除了Action和Service,Init.rc中还有一种小节,就是Import小节。该小节表达的意思有点儿像java中的import,也就是说,Init.rc中还可以导入其他.rc脚本文件的内容。在早期的Android中,好像并不支持import语句,不过至少从Android4.0开始,添加了import语句。至于import最早出现在哪个版本,我没有考证过。import句子截选如下:
import /init.environ.rc
import /init.usb.rc
import /init.$ro.hardware.rc
import /init.trace.rc
?
3.2解析
在init进程的main()函数里,会调用init_parse_config_file("/init.rc")一句来解析init.rc脚本。init_parse_config_file()的代码如下:
【system/core/init/Init_parser.c】
int init_parse_config_file(const char *fn)
char *data;
data = read_file(fn, 0);
if (!data) return -1;
parse_config(fn, data);
DUMP();
return 0;
?
先用read_file()把脚本内容读入一块内存,而后调用parse_config()解析这块内存。
parse_config()的代码截选如下:
static void parse_config(const char *fn, char *s)
. . . . . .
for (;;)
switch (next_token(&state))
. . . . . .
case T_NEWLINE: // 遇到折行
<span style="white-space:pre"> </span> state.line++;
if (nargs)
<span style="white-space:pre"> </span> int kw = lookup_keyword(args[0]);
if (kw_is(kw, SECTION))
state.parse_line(&state, 0, 0); // 不同section的parse_line也不同噢
parse_new_section(&state, kw, nargs, args);
else
state.parse_line(&state, nargs, args);
nargs = 0;
break;
. . . . . .
. . . . . .
它在逐行分析init.rc脚本,判断每一行的第一个参数是什么类型的,如果是action或service类型的,就表示要创建一个新的section节点了,此时它会设置一下解析后续行的解析函数,也就是给state->parse_line赋值啦。针对service类型,解析后续行的函数是parse_line_service(),而针对action类型,解析后续行的函数则是parse_line_action()。
这么看来,parse_config()里有3个地方值得我们注意:
- lookup_keyword()和kw_is()
- parse_new_section()
- state.parse_line()
3.2.1查询脚本关键字
我们先介绍关于关键字查找方面的知识,在这里主要看lookup_keyword()和kw_is()。 lookup_keyword()的定义截选如下:
【system/core/init/Init_parser.c】
int lookup_keyword(const char *s)
switch (*s++)
case 'c':
if (!strcmp(s, "opy")) return K_copy;
if (!strcmp(s, "apability")) return K_capability;
if (!strcmp(s, "hdir")) return K_chdir;
if (!strcmp(s, "hroot")) return K_chroot;
if (!strcmp(s, "lass")) return K_class;
if (!strcmp(s, "lass_start")) return K_class_start;
if (!strcmp(s, "lass_stop")) return K_class_stop;
if (!strcmp(s, "lass_reset")) return K_class_reset;
if (!strcmp(s, "onsole")) return K_console;
if (!strcmp(s, "hown")) return K_chown;
if (!strcmp(s, "hmod")) return K_chmod;
if (!strcmp(s, "ritical")) return K_critical;
break;
case 'd':
if (!strcmp(s, "isabled")) return K_disabled;
if (!strcmp(s, "omainname")) return K_domainname;
break;
. . . . . .
. . . . . .
?
kw_is()宏的定义如下:
#define kw_is(kw, type) (keyword_info[kw].flags & (type))
基本上是查表的过程,而lookup_keyword()返回的那些K_copy、K_capability值,其实就是表项的索引号。这张关键字表的技术细节如下。
在init_parser.c文件中有下面这样的代码:
【system/core/init/Init_parser.c】
#include "keywords.h"
#define KEYWORD(symbol, flags, nargs, func) \\
[ K_##symbol ] = #symbol, func, nargs + 1, flags, ,
struct
const char *name;
int (*func)(int nargs, char **args);
unsigned char nargs;
unsigned char flags;
keyword_info[KEYWORD_COUNT] =
[ K_UNKNOWN ] = "unknown", 0, 0, 0 ,
#include "keywords.h"
;
#undef KEYWORD
这里用到了一点儿小技巧,两次include了keywords.h头文件,其实keywords.h中会先定义一次KEYWORD宏,其主要目的是为了形成一个顺序排列的enum,而后就#undef KEYWORD了。接着上面代码中再次定义了KEYWORD宏,这次的主要目的是为了形成一个struct数组,即keyword_info数组。
keywords.h的部分截选如下:
【system/core/init/Keywords.h】
#ifndef KEYWORD
int do_chroot(int nargs, char **args);
int do_chdir(int nargs, char **args);
int do_class_start(int nargs, char **args);
. . . . . .
. . . . . .
#define __MAKE_KEYWORD_ENUM__
#define KEYWORD(symbol, flags, nargs, func) K_##symbol,
enum
K_UNKNOWN,
#endif
KEYWORD(capability, OPTION, 0, 0)
KEYWORD(chdir, COMMAND, 1, do_chdir)
KEYWORD(chroot, COMMAND, 1, do_chroot)
KEYWORD(class, OPTION, 0, 0)
. . . . . .
. . . . . .
#ifdef __MAKE_KEYWORD_ENUM__
KEYWORD_COUNT,
;
#undef __MAKE_KEYWORD_ENUM__
#undef KEYWORD
#endif
其中的#define KEYWORD是第一次定义KEYWORD,我们比对一下这两次定义:
// 第一次
#define KEYWORD(symbol, flags, nargs, func) K_##symbol,
// 第二次
#define KEYWORD(symbol, flags, nargs, func) \\
[ K_##symbol ] = #symbol, func, nargs + 1, flags, ,
总之,最后形成了如下数组:
表中只有3个表项的flag是SECTION,表示这是个小节,我用黄色框表示。
3.2.2解析section小节
一旦分析出某句脚本是以on或者service或者import开始,就说明一个新的小节要开始了。此时,会调用到parse_new_section(),该函数的代码如下:
void parse_new_section(struct parse_state *state, int kw, int nargs, char **args)
printf("[ %s %s ]\\n", args[0],
nargs > 1 ? args[1] : "");
switch(kw)
case K_service:
state->context = parse_service(state, nargs, args);
if (state->context)
state->parse_line = parse_line_service;
return;
break;
case K_on:
state->context = parse_action(state, nargs, args);
if (state->context)
state->parse_line = parse_line_action;
return;
break;
case K_import:
parse_import(state, nargs, args);
break;
state->parse_line = parse_line_no_op;
?
很明显,解析的小节就是那三类:action小节(以on开头的),service小节和import小节。最核心的部分当然是service小节和action小节,具体解析的地方在上面代码中的parse_service()和parse_action()函数里。至于import小节,parse_import()函数只是把脚本中的所有import语句先汇总成一个链表,记入state结构中,待回到parse_config()后再做处理。
3.2.2.1解析service小节
parse_service()的代码如下:【system/core/init/Init_parser.c】
static void *parse_service(struct parse_state *state, int nargs, char **args)
struct service *svc;
. . . . . .
svc = service_find_by_name(args[1]);
if (svc)
parse_error(state, "ignored duplicate definition of service '%s'\\n", args[1]);
return 0;
nargs -= 2;
svc = calloc(1, sizeof(*svc) + sizeof(char*) * nargs);
if (!svc)
parse_error(state, "out of memory\\n");
return 0;
svc->name = args[1];
svc->classname = "default";
memcpy(svc->args, args + 2, sizeof(char*) * nargs);
svc->args[nargs] = 0;
svc->nargs = nargs;
svc->onrestart.name = "onrestart";
list_init(&svc->onrestart.commands);
list_add_tail(&service_list, &svc->slist);
return svc;
?
解析service段时,会用calloc()申请一个service节点,填入service名等信息,并连入service_list总表中。注意,此时该service节点的onrestart.commands部分还是个空链表,因为我们还没有分析该service的后续脚本行呢。
parse_new_section()中为service明确指定了解析后续行的函数parse_line_service()。该函数的代码截选如下:
static void parse_line_service(struct parse_state *state, int nargs, char **args)
struct service *svc = state->context;
struct command *cmd;
. . . . . .
kw = lookup_keyword(args[0]); // 解析具体的service option也是要查关键字表的
switch (kw)
case K_capability:
break;
case K_class:
if (nargs != 2)
parse_error(state, "class option requires a classname\\n");
else
svc->classname = args[1];
break;
case K_console:
svc->flags |= SVC_CONSOLE;
break;
case K_disabled:
. . . . . .
. . . . . .
service的各个option会影响service节点的不同域,比如flags域、classname域、onrestart域等等。比较麻烦的是onrestart域,因为它本身又是个action节点,可携带若干个子command。
下面是service中常见的option:
1)K_capability
2)K_class
3)K_console
4)K_disabled
5)K_ioprio
6)K_group
7)K_user
8)K_keycodes
9)K_oneshot
10)K_onrestart
11)K_critical
12)K_setenv
13)K_socket
14)K_seclabel
在service小节解析完毕后,我们应该能得到类似下图这样的service节点:
3.2.2.2解析action小节
另一方面,解析action小节时的动作也很简单,会用calloc()申请一个action节点,填入action名等信息,然后连入action_list总表中。当然,此时action的commands部分也是空的。
static void *parse_action(struct parse_state *state, int nargs, char **args)
struct action *act;
. . . . . .
act = calloc(1, sizeof(*act));
act->name = args[1];
list_init(&act->commands);
list_init(&act->qlist);
list_add_tail(&action_list, &act->alist);
return act;
?
对于action小节而言,我们指定了不同的解析后续行的函数,也就是parse_line_action()。该函数的代码截选如下:
static void parse_line_action(struct parse_state* state, int nargs, char **args)
struct command *cmd;
struct action *act = state->context;
. . . . . .
kw = lookup_keyword(args[0]); // 解析具体的action command也是要查关键字表的
if (!kw_is(kw, COMMAND))
parse_error(state, "invalid command '%s'\\n", args[0]);
return;
n = kw_nargs(kw);
if (nargs < n)
parse_error(state, "%s requires %d %s\\n", args[0], n - 1,
n > 2 ? "arguments" : "argument");
return;
cmd = malloc(sizeof(*cmd) + sizeof(char*) * nargs);
cmd->func = kw_func(kw);
cmd->nargs = nargs;
memcpy(cmd->args, args, sizeof(char*) * nargs);
list_add_tail(&act->commands, &cmd->clist);
?
既然action的后续行可以包含多条command,那么parse_line_action()就必须先确定出当前分析的是什么command,这一点和parse_line_service()是一致的,都是通过调用lookup_keyword()来查询关键字的。另外,command子行的所有参数其实已被记入传进来的args参数,现在这些参数会记入command节点的args域中,而且这个command节点会链入action节点的commands链表尾部。
在action小节解析完毕后,我们应该能得到类似下图这样的action节点:
3.2.3主要形成两个双向链表
我们画了一张关于parse_config()的调用关系图,如下:
init_parse_config_file()函数会将Init.rc脚本解析成两个双向链表,对应的表头分别是service_list和action_list。双向链表示意图如下:
3.3具体执行那些action
经过解析一步,init.rc脚本中的actions被整理成双向链表了,但是这些action并没有被实际执行。现在我们就来看下一步具体执行action的流程。在init进程的main()函数中,我们可以看到如下句子:
int main(int argc, char **argv)
. . . . . .
. . . . . .
init_parse_config_file("/init.rc"); // 内部将脚本内容转换成action链表了
action_for_each_trigger("early-init", action_add_queue_tail);
queue_builtin_action(wait_for_coldboot_done_action,
"wait_for_coldboot_done");
queue_builtin_action(mix_hwrng_into_linux_rng_action,
"mix_hwrng_into_linux_rng");
queue_builtin_action(keychord_init_action, "keychord_init");
queue_builtin_action(console_init_action, "console_init");
/* execute all the boot actions to get us started */
action_for_each_trigger("init", action_add_queue_tail);
. . . . . .
. . . . . .
?
首先,init_parse_config_file()已经把init.rc脚本里的内容转换成action链表了,接着代码运行到action_for_each_trigger(“early-init”...)一句,这一句会把action_list列表中匹配的action节点,连入action_queue队列。
3.3.1整理action_queue队列
init进程希望把系统初始化过程分割成若干“子阶段”,action_for_each_trigger()的意思就是“触发某个子阶段里的所有action”。在早期的Android中,大概就只有4、5个子阶段,现在随着Android的不断升级,子阶段也变得越来越多了。
action_for_each_trigger()的代码如下:
void action_for_each_trigger(const char *trigger,
void (*func)(struct action *act))
struct listnode *node;
struct action *act;
list_for_each(node, &action_list)
act = node_to_item(node, struct action, alist);
if (!strcmp(act->name, trigger))
func(act); // 只要匹配,就回调func
?
可以看到是在遍历action_list链表,找寻所有“action名”和“参数trigger”匹配的节点,并回调“参数func所指的回调函数”。在前面的代码中,回调函数就是action_add_queue_tail()。
void action_add_queue_tail(struct action *act)
if (list_empty(&act->qlist))
list_add_tail(&action_queue, &act->qlist);
?
嗯,这里又出现了个action_queue队列!它和action_list列表有什么关系?
其实很简单,action_list可以被理解成一个来自init.rc的“草稿列表”,列表中的节点顺序基本上和init.rc脚本里编写section时的顺序一致,而这个顺序不一定就是合适的“运行顺序”,所以我们需要另一个按我们的要求依次串接的队列,那就是action_queue队列。另外,有些新的action并没有体现在init.rc脚本里,而是写在具体代码里的,这些action可以被称为“内建action”,我们可以通过调用queue_builtin_action()将“内建action”添加进action_list列表和action_queue队列中。
queue_builtin_action()的代码如下:
void queue_builtin_action(int (*func)(int nargs, char **args), char *name)
struct action *act;
struct command *cmd;
act = calloc(1, sizeof(*act));
act->name = name;
list_init(&act->commands);
list_init(&act->qlist);
cmd = calloc(1, sizeof(*cmd));
cmd->func = func;
cmd->args[0] = name;
list_add_tail(&act->commands, &cmd->clist);
list_add_tail(&action_list, &act->alist);
action_add_queue_tail(act);
?
init进程里主要分割的“子阶段”如下图所示:
桔色方框表示的子阶段,是比较重要的阶段。
3.3.1.1early-init子阶段
我们先看early-init子阶段,这部分在init.rc里是这样表达的:
on early-init
# Set init and its forked children's oom_adj.
write /proc/1/oom_adj -16
# Set the security context for the init process.
# This should occur before anything else (e.g. ueventd) is started.
setcon u:r:init:s0
start ueventd
# create mountpoints
mkdir /mnt 0775 root system
这个action包含4条command,分别是write、setcon、start和mkdir。不同command对应的func回调函数也是不同的,具体对应什么,可以查看Keywords.h。
【system/core/init/Keywords.h】
KEYWORD(service, SECTION, 0, 0)
KEYWORD(setcon, COMMAND, 1, do_setcon)
KEYWORD(setenforce, COMMAND, 1, do_setenforce)
KEYWORD(setenv, OPTION, 2, 0)
KEYWORD(setkey, COMMAND, 0, do_setkey)
KEYWORD(setprop, COMMAND, 2, do_setprop)
KEYWORD(setrlimit, COMMAND, 3, do_setrlimit)
KEYWORD(setsebool, COMMAND, 2, do_setsebool)
KEYWORD(socket, OPTION, 0, 0)
KEYWORD(start, COMMAND, 1, do_start)
KEYWORD(stop, COMMAND, 1, do_stop)
KEYWORD(swapon_all, COMMAND, 1, do_swapon_all)
KEYWORD(trigger, COMMAND, 1, do_trigger)
KEYWORD(symlink, COMMAND, 1, do_symlink)
KEYWORD(sysclktz, COMMAND, 1, do_sysclktz)
KEYWORD(user, OPTION, 0, 0)
KEYWORD(wait, COMMAND, 1, do_wait)
KEYWORD(write, COMMAND, 2, do_write)
KEYWORD(copy, COMMAND, 2, do_copy)
KEYWORD(chown, COMMAND, 2, do_chown)
KEYWORD(chmod, COMMAND, 2, do_chmod)
比如说start命令对应的回调函数就是do_start():
int do_start(int nargs, char **args)
struct service *svc;
svc = service_find_by_name(args[1]);
if (svc)
service_start(svc, NULL);
return 0;
启动所指定的service。
3.3.1.2boot子阶段
以上是关于Android4.4的init进程的主要内容,如果未能解决你的问题,请参考以下文章