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进程的主要内容,如果未能解决你的问题,请参考以下文章

iTOP4412开发板Android4.4源码编译分享

Android4.4的zygote进程(上)

Android4.4的zygote进程(上)

Android4.4的zygote进程(下)

Android4.4的zygote进程(下)

Systemd管理