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的形式如下:

  1. on  <trigger>
  2. <command1>
  3. <command2>
  4. ......

Service表示一个服务程序,会在初始化时启动。因为init.rc脚本中描述的服务往往都是核心服务,所以(基本上所有的)服务会在退出时自动重启。Service的形式如下:

  1. service <name> <pathname> [<arguments>]*
  2. <option>
  3. <option>
  4. ......

Init.rc中的Service截选如下:

  1. service servicemanager /system/bin/servicemanager
  2. class core
  3. user system
  4. group system
  5. critical
  6. onrestart restart healthd
  7. onrestart restart zygote
  8. onrestart restart media
  9. onrestart restart surfaceflinger
  10. onrestart restart drm
  11. service vold /system/bin/vold
  12. class core
  13. socket vold stream 0660 root mount
  14. ioprio be 2
  15. service netd /system/bin/netd
  16. class main
  17. socket netd stream 0660 root system
  18. socket dnsproxyd stream 0660 root inet
  19. 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句子截选如下:

  1. import /init.environ.rc
  2. import /init.usb.rc
  3. import /init.${ro.hardware}.rc
  4. 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】

  1. int init_parse_config_file(const char *fn)
  2. {
  3. char *data;
  4. data = read_file(fn, 0);
  5. if (!data) return -1;
  6. parse_config(fn, data);
  7. DUMP();
  8. return 0;
  9. }

先用read_file()把脚本内容读入一块内存,而后调用parse_config()解析这块内存。

parse_config()的代码截选如下:

  1. static void parse_config(const char *fn, char *s)
  2. {
  3. . . . . . .
  4. for (;;) {
  5. switch (next_token(&state)) {
  6. . . . . . .
  7. case T_NEWLINE:   // 遇到折行
  8. <span style="white-space:pre">        </span>    state.line++;
  9. if (nargs) {
  10. <span style="white-space:pre">            </span>    int kw = lookup_keyword(args[0]);
  11. if (kw_is(kw, SECTION)) {
  12. state.parse_line(&state, 0, 0);  // 不同section的parse_line也不同噢
  13. parse_new_section(&state, kw, nargs, args);
  14. } else {
  15. state.parse_line(&state, nargs, args);
  16. }
  17. nargs = 0;
  18. }
  19. break;
  20. . . . . . .
  21. . . . . . .
  22. }
 

它在逐行分析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】

  1. int lookup_keyword(const char *s)
  2. {
  3. switch (*s++) {
  4. case 'c':
  5. if (!strcmp(s, "opy")) return K_copy;
  6. if (!strcmp(s, "apability")) return K_capability;
  7. if (!strcmp(s, "hdir")) return K_chdir;
  8. if (!strcmp(s, "hroot")) return K_chroot;
  9. if (!strcmp(s, "lass")) return K_class;
  10. if (!strcmp(s, "lass_start")) return K_class_start;
  11. if (!strcmp(s, "lass_stop")) return K_class_stop;
  12. if (!strcmp(s, "lass_reset")) return K_class_reset;
  13. if (!strcmp(s, "onsole")) return K_console;
  14. if (!strcmp(s, "hown")) return K_chown;
  15. if (!strcmp(s, "hmod")) return K_chmod;
  16. if (!strcmp(s, "ritical")) return K_critical;
  17. break;
  18. case 'd':
  19. if (!strcmp(s, "isabled")) return K_disabled;
  20. if (!strcmp(s, "omainname")) return K_domainname;
  21. break;
  22. . . . . . .
  23. . . . . . .

kw_is()宏的定义如下:

  1. #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】

  1. #include "keywords.h"
  2. #define KEYWORD(symbol, flags, nargs, func) \
  3. [ K_##symbol ] = { #symbol, func, nargs + 1, flags, },
  4. struct {
  5. const char *name;
  6. int (*func)(int nargs, char **args);
  7. unsigned char nargs;
  8. unsigned char flags;
  9. } keyword_info[KEYWORD_COUNT] = {
  10. [ K_UNKNOWN ] = { "unknown", 0, 0, 0 },
  11. #include "keywords.h"
  12. };
  13. #undef KEYWORD

这里用到了一点儿小技巧,两次include了keywords.h头文件,其实keywords.h中会先定义一次KEYWORD宏,其主要目的是为了形成一个顺序排列的enum,而后就#undef KEYWORD了。接着上面代码中再次定义了KEYWORD宏,这次的主要目的是为了形成一个struct数组,即keyword_info数组。

keywords.h的部分截选如下:
【system/core/init/Keywords.h】

  1. #ifndef KEYWORD
  2. int do_chroot(int nargs, char **args);
  3. int do_chdir(int nargs, char **args);
  4. int do_class_start(int nargs, char **args);
  5. . . . . . .
  6. . . . . . .
  7. #define __MAKE_KEYWORD_ENUM__
  8. #define KEYWORD(symbol, flags, nargs, func) K_##symbol,
  9. enum {
  10. K_UNKNOWN,
  11. #endif
  12. KEYWORD(capability,  OPTION,  0, 0)
  13. KEYWORD(chdir,       COMMAND, 1, do_chdir)
  14. KEYWORD(chroot,      COMMAND, 1, do_chroot)
  15. KEYWORD(class,       OPTION,  0, 0)
  16. . . . . . .
  17. . . . . . .
  18. #ifdef __MAKE_KEYWORD_ENUM__
  19. KEYWORD_COUNT,
  20. };
  21. #undef __MAKE_KEYWORD_ENUM__
  22. #undef KEYWORD
  23. #endif

其中的#define KEYWORD是第一次定义KEYWORD,我们比对一下这两次定义:

  1. // 第一次
  2. #define KEYWORD(symbol, flags, nargs, func) K_##symbol,
  3. // 第二次
  4. #define KEYWORD(symbol, flags, nargs, func) \
  5. [ K_##symbol ] = { #symbol, func, nargs + 1, flags, },

总之,最后形成了如下数组:

表中只有3个表项的flag是SECTION,表示这是个小节,我用黄色框表示。

3.2.2解析section小节

一旦分析出某句脚本是以on或者service或者import开始,就说明一个新的小节要开始了。此时,会调用到parse_new_section(),该函数的代码如下:

  1. void parse_new_section(struct parse_state *state, int kw, int nargs, char **args)
  2. {
  3. printf("[ %s %s ]\n", args[0],
  4. nargs > 1 ? args[1] : "");
  5. switch(kw) {
  6. case K_service:
  7. state->context = parse_service(state, nargs, args);
  8. if (state->context) {
  9. state->parse_line = parse_line_service;
  10. return;
  11. }
  12. break;
  13. case K_on:
  14. state->context = parse_action(state, nargs, args);
  15. if (state->context) {
  16. state->parse_line = parse_line_action;
  17. return;
  18. }
  19. break;
  20. case K_import:
  21. parse_import(state, nargs, args);
  22. break;
  23. }
  24. state->parse_line = parse_line_no_op;
  25. }

很明显,解析的小节就是那三类: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】

  1. static void *parse_service(struct parse_state *state, int nargs, char **args)
  2. {
  3. struct service *svc;
  4. . . . . . .
  5. svc = service_find_by_name(args[1]);
  6. if (svc) {
  7. parse_error(state, "ignored duplicate definition of service '%s'\n", args[1]);
  8. return 0;
  9. }
  10. nargs -= 2;
  11. svc = calloc(1, sizeof(*svc) + sizeof(char*) * nargs);
  12. if (!svc) {
  13. parse_error(state, "out of memory\n");
  14. return 0;
  15. }
  16. svc->name = args[1];
  17. svc->classname = "default";
  18. memcpy(svc->args, args + 2, sizeof(char*) * nargs);
  19. svc->args[nargs] = 0;
  20. svc->nargs = nargs;
  21. svc->onrestart.name = "onrestart";
  22. list_init(&svc->onrestart.commands);
  23. list_add_tail(&service_list, &svc->slist);
  24. return svc;
  25. }

解析service段时,会用calloc()申请一个service节点,填入service名等信息,并连入service_list总表中。注意,此时该service节点的onrestart.commands部分还是个空链表,因为我们还没有分析该service的后续脚本行呢。

parse_new_section()中为service明确指定了解析后续行的函数parse_line_service()。该函数的代码截选如下:

  1. static void parse_line_service(struct parse_state *state, int nargs, char **args)
  2. {
  3. struct service *svc = state->context;
  4. struct command *cmd;
  5. . . . . . .
  6. kw = lookup_keyword(args[0]);   // 解析具体的service option也是要查关键字表的
  7. switch (kw) {
  8. case K_capability:
  9. break;
  10. case K_class:
  11. if (nargs != 2) {
  12. parse_error(state, "class option requires a classname\n");
  13. } else {
  14. svc->classname = args[1];
  15. }
  16. break;
  17. case K_console:
  18. svc->flags |= SVC_CONSOLE;
  19. break;
  20. case K_disabled:
  21. . . . . . .
  22. . . . . . .
 

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部分也是空的。

  1. static void *parse_action(struct parse_state *state, int nargs, char **args)
  2. {
  3. struct action *act;
  4. . . . . . .
  5. act = calloc(1, sizeof(*act));
  6. act->name = args[1];
  7. list_init(&act->commands);
  8. list_init(&act->qlist);
  9. list_add_tail(&action_list, &act->alist);
  10. return act;
  11. }

对于action小节而言,我们指定了不同的解析后续行的函数,也就是parse_line_action()。该函数的代码截选如下:

  1. static void parse_line_action(struct parse_state* state, int nargs, char **args)
  2. {
  3. struct command *cmd;
  4. struct action *act = state->context;
  5. . . . . . .
  6. kw = lookup_keyword(args[0]);   // 解析具体的action command也是要查关键字表的
  7. if (!kw_is(kw, COMMAND)) {
  8. parse_error(state, "invalid command '%s'\n", args[0]);
  9. return;
  10. }
  11. n = kw_nargs(kw);
  12. if (nargs < n) {
  13. parse_error(state, "%s requires %d %s\n", args[0], n - 1,
  14. n > 2 ? "arguments" : "argument");
  15. return;
  16. }
  17. cmd = malloc(sizeof(*cmd) + sizeof(char*) * nargs);
  18. cmd->func = kw_func(kw);
  19. cmd->nargs = nargs;
  20. memcpy(cmd->args, args, sizeof(char*) * nargs);
  21. list_add_tail(&act->commands, &cmd->clist);
  22. }

既然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()函数中,我们可以看到如下句子:

  1. int main(int argc, char **argv)
  2. {
  3. . . . . . .
  4. . . . . . .
  5. init_parse_config_file("/init.rc");  // 内部将脚本内容转换成action链表了
  6. action_for_each_trigger("early-init", action_add_queue_tail);
  7. queue_builtin_action(wait_for_coldboot_done_action,
  8. "wait_for_coldboot_done");
  9. queue_builtin_action(mix_hwrng_into_linux_rng_action,
  10. "mix_hwrng_into_linux_rng");
  11. queue_builtin_action(keychord_init_action, "keychord_init");
  12. queue_builtin_action(console_init_action, "console_init");
  13. /* execute all the boot actions to get us started */
  14. action_for_each_trigger("init", action_add_queue_tail);
  15. . . . . . .
  16. . . . . . .
  17. }

首先,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()的代码如下:

  1. void action_for_each_trigger(const char *trigger,
  2. void (*func)(struct action *act))
  3. {
  4. struct listnode *node;
  5. struct action *act;
  6. list_for_each(node, &action_list) {
  7. act = node_to_item(node, struct action, alist);
  8. if (!strcmp(act->name, trigger)) {
  9. func(act);  // 只要匹配,就回调func
  10. }
  11. }
  12. }

可以看到是在遍历action_list链表,找寻所有“action名”和“参数trigger”匹配的节点,并回调“参数func所指的回调函数”。在前面的代码中,回调函数就是action_add_queue_tail()。

  1. void action_add_queue_tail(struct action *act)
  2. {
  3. if (list_empty(&act->qlist)) {
  4. list_add_tail(&action_queue, &act->qlist);
  5. }
  6. }

嗯,这里又出现了个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()的代码如下:

  1. void queue_builtin_action(int (*func)(int nargs, char **args), char *name)
  2. {
  3. struct action *act;
  4. struct command *cmd;
  5. act = calloc(1, sizeof(*act));
  6. act->name = name;
  7. list_init(&act->commands);
  8. list_init(&act->qlist);
  9. cmd = calloc(1, sizeof(*cmd));
  10. cmd->func = func;
  11. cmd->args[0] = name;
  12. list_add_tail(&act->commands, &cmd->clist);
  13. list_add_tail(&action_list, &act->alist);
  14. action_add_queue_tail(act);
  15. }

?

init进程里主要分割的“子阶段”如下图所示:

桔色方框表示的子阶段,是比较重要的阶段。

3.3.1.1early-init子阶段

我们先看early-init子阶段,这部分在init.rc里是这样表达的:

  1. on early-init
  2. # Set init and its forked children's oom_adj.
  3. write /proc/1/oom_adj -16
  4. # Set the security context for the init process.
  5. # This should occur before anything else (e.g. ueventd) is started.
  6. setcon u:r:init:s0
  7. start ueventd
  8. # create mountpoints
  9. mkdir /mnt 0775 root system

这个action包含4条command,分别是write、setcon、start和mkdir。不同command对应的func回调函数也是不同的,具体对应什么,可以查看Keywords.h。
【system/core/init/Keywords.h】

  1. KEYWORD(service,     SECTION, 0, 0)
  2. KEYWORD(setcon,      COMMAND, 1, do_setcon)
  3. KEYWORD(setenforce,  COMMAND, 1, do_setenforce)
  4. KEYWORD(setenv,      OPTION,  2, 0)
  5. KEYWORD(setkey,      COMMAND, 0, do_setkey)
  6. KEYWORD(setprop,     COMMAND, 2, do_setprop)
  7. KEYWORD(setrlimit,   COMMAND, 3, do_setrlimit)
  8. KEYWORD(setsebool,   COMMAND, 2, do_setsebool)
  9. KEYWORD(socket,      OPTION,  0, 0)
  10. KEYWORD(start,       COMMAND, 1, do_start)
  11. KEYWORD(stop,        COMMAND, 1, do_stop)
  12. KEYWORD(swapon_all,  COMMAND, 1, do_swapon_all)
  13. KEYWORD(trigger,     COMMAND, 1, do_trigger)
  14. KEYWORD(symlink,     COMMAND, 1, do_symlink)
  15. KEYWORD(sysclktz,    COMMAND, 1, do_sysclktz)
  16. KEYWORD(user,        OPTION,  0, 0)
  17. KEYWORD(wait,        COMMAND, 1, do_wait)
  18. KEYWORD(write,       COMMAND, 2, do_write)
  19. KEYWORD(copy,        COMMAND, 2, do_copy)
  20. KEYWORD(chown,       COMMAND, 2, do_chown)
  21. KEYWORD(chmod,       COMMAND, 2, do_chmod)

比如说start命令对应的回调函数就是do_start():

  1. int do_start(int nargs, char **args)
  2. {
  3. struct service *svc;
  4. svc = service_find_by_name(args[1]);
  5. if (svc) {
  6. service_start(svc, NULL);
  7. }
  8. return 0;
  9. }

启动所指定的service。

3.3.1.2boot子阶段

boot部分在init.rc里是这样表达的:

  1. on boot
  2. ifup lo
  3. hostname localhost
  4. domainname localdomain
  5. setrlimit 13 40 40
  6. . . . . . .
  7. write /proc/sys/vm/overcommit_memory 1
  8. write /proc/sys/vm/min_free_order_shift 4
  9. chown root system /sys/module/lowmemorykiller/parameters/adj
  10. chmod 0664 /sys/module/lowmemorykiller/parameters/adj
  11. . . . . . .
  12. . . . . . .
  13. setprop net.tcp.buffersize.default 4096,87380,110208,4096,16384,110208
  14. setprop net.tcp.buffersize.wifi
  15. 524288,1048576,2097152,262144,524288,1048576
  16. . . . . . .
  17. setprop net.tcp.default_init_rwnd 60
  18. class_start core
  19. class_start main
请注意最后的两句,表示boot动作的最后,会自动先启动所有类型为“core”的服务,而后再启动所有类型为“main”的服务。我们在前文阐述init.rc脚本中的service写法时,特别让大家留意service的class选项,比如class core和class main,现在要用到这个概念了。

class_start命令对应的回调函数是do_class_start(),该函数的代码如下:
【system/core/init/Builtins.c】

  1. int do_class_start(int nargs, char **args)
  2. {
  3. service_for_each_class(args[1], service_start_if_not_disabled);
  4. return 0;
  5. }
  1. void service_for_each_class(const char *classname,
  2. void (*func)(struct service *svc))
  3. {
  4. struct listnode *node;
  5. struct service *svc;
  6. list_for_each(node, &service_list) {
  7. svc = node_to_item(node, struct service, slist);
  8. if (!strcmp(svc->classname, classname)) {
  9. func(svc);    // 回调service_start_if_not_disabled()
  10. }
  11. }
  12. }
其回调的func,就是service_start_if_not_disabled(),代码如下:
  1. static void service_start_if_not_disabled(struct service *svc)
  2. {
  3. if (!(svc->flags & SVC_DISABLED)) {
  4. service_start(svc, NULL);
  5. }
  6. }
代码很简单,service_for_each_class()会遍历service_list链表,找到所有和classname匹配的service节点,如果这个节点没有被disabled的话,那么就启动其对应的服务。 
 

boot子阶段先启动的“core”类型的服务有:

core类型的服务 对应的可执行文件 说明
ueventd /sbin/ueventd  
healthd /sbin/healthd  
console /system/bin/sh  
adbd /sbin/adbd  
servicemanager /system/bin/servicemanager 大名鼎鼎的service manager service服务,Android的核心之一。
vold /system/bin/vold  

而后,boot子阶段启动的“main”类型的服务有:

main类型的服务 对应的可执行文件 说明
netd /system/bin/netd  
debuggerd /system/bin/debuggerd  
ril-daemon /system/bin/rild  
surfaceflinger /system/bin/surfaceflinger  
zygote /system/bin/app_process Android创建内部创建新进程的核心服务。
drm /system/bin/drmserver  
media /system/bin/mediaserver  
bootanim /system/bin/bootanimation  
installd /system/bin/installd  
flash_recovery /system/etc/install-recovery.sh  
racoon /system/bin/racoon  
mtpd /system/bin/mtpd  
keystore /system/bin/keystore  
dumpstate /system/bin/dumpstate  
sshd /system/bin/start-ssh  
mdnsd /system/bin/mdnsd  

3.3.2for循环中执行action_queue队列

现在我们继续看,动作在编排进action_queue队列之后,又是如何执行的呢?我们知道,init进程最终会进入一个for(;;)循环,在这个循环中,每次都会尝试执行一个command:

  1. int main(int argc, char **argv)
  2. {
  3. . . . . . .
  4. . . . . . .
  5. // 这个for循环非常重要哦!
  6. for(;;) {
  7. int nr, i, timeout = -1;
  8. execute_one_command();
  9. restart_processes();
  10. . . . . . .
  11. }
其中调用的execute_one_command()的代码如下:
  1. void execute_one_command(void)
  2. {
  3. int ret;
  4. if (!cur_action || !cur_command || is_last_command(cur_action, cur_command))
  5. {
  6. cur_action = action_remove_queue_head();
  7. cur_command = NULL;
  8. if (!cur_action)
  9. return;
  10. INFO("processing action %p (%s)\n", cur_action, cur_action->name);
  11. cur_command = get_first_command(cur_action);
  12. } else {
  13. cur_command = get_next_command(cur_action, cur_command);
  14. }
  15. if (!cur_command)
  16. return;
  17. ret = cur_command->func(cur_command->nargs, cur_command->args);
  18. INFO("command '%s' r=%d\n", cur_command->args[0], ret);
  19. }

它的意思是说,执行“当前action”(cur_action)的“当前command”(cur_command)。如果执行时没有“当前action”,就尝试从action_queue队列的头部摘取一个节点。如果执行时没有“当前command”,就从“当前action”中获取下一个该执行的command。而一旦得到了该执行的command,就回调其func函数指针。

在那几个core类型的service中,有一个非常重要的service,叫做zygote,它是android内部创建新进程的核心服务,但本文就不对它细说了。

4补充说明几个运作机理知识

下面我们补充说明几个init进程里的运作机理。

4.1service是如何重启的?

关于service的重启方法,其实用到了linux的一点儿信号机制。在init进程的main()函数中,除了“early-init”、“init”等子阶段外,还有个子阶段叫作“signal_init”:

  1. queue_builtin_action(signal_init_action, "signal_init");
当init进程执行到这个子阶段时,会执行signal_init_action()回调函数:

【system/core/init/Init.c】

  1. static int signal_init_action(int nargs, char **args)
  2. {
  3. signal_init();
  4. return 0;
  5. }
  1. void signal_init(void)
  2. {
  3. int s[2];
  4. struct sigaction act;
  5. memset(&act, 0, sizeof(act));
  6. act.sa_handler = sigchld_handler;
  7. act.sa_flags = SA_NOCLDSTOP;
  8. sigaction(SIGCHLD, &act, 0);   // 向系统注册一个系统回调
  9. /* create a signalling mechanism for the sigchld handler */
  10. if (socketpair(AF_UNIX, SOCK_STREAM, 0, s) == 0) {
  11. signal_fd = s[0];       // 以后回调函数会向这个fd写数据
  12. signal_recv_fd = s[1];
  13. fcntl(s[0], F_SETFD, FD_CLOEXEC);
  14. fcntl(s[0], F_SETFL, O_NONBLOCK);
  15. fcntl(s[1], F_SETFD, FD_CLOEXEC);
  16. fcntl(s[1], F_SETFL, O_NONBLOCK);
  17. }
  18. handle_signal();
  19. }

请注意,signal_init()中调用了sigaction(SIGCHLD,...)一句。在linux系统中,当一个进程终止或者停止时,系统会向其父进程发送SIGCHLD信号。sigaction()动作可以被理解为向系统注册一个系统回调函数。在本例中,每当有子进程终止时,系统就会回调sigchld_handler()回调函数,该函数的代码如下:

【system/core/init/Signal_handler.c】

  1. static void sigchld_handler(int s)
  2. {
  3. write(signal_fd, &s, 1);
  4. }

看到了吗?无非是向signal_init()中创建的“socket对”里的signal_fd写数据,于是“socket对”的另一个句柄signal_recv_fd就可以得到所写的数据。

在init进程的main()函数中,最终进入那个无限for循环,监听系统的风吹草动,其中就包括监听这个signal_recv_fd:

  1. int main(int argc, char **argv)
  2. {
  3. . . . . . .
  4. . . . . . .
  5. for(;;) {
  6. . . . . . .
  7. if (!signal_fd_init && get_signal_fd() > 0) {
  8. ufds[fd_count].fd = get_signal_fd();  // 就是signal_recv_fd !
  9. ufds[fd_count].events = POLLIN;
  10. ufds[fd_count].revents = 0;
  11. fd_count++;
  12. signal_fd_init = 1;
  13. }
  14. . . . . . .
  15. . . . . . .
  16. nr = poll(ufds, fd_count, timeout);
  17. . . . . . .
  18. for (i = 0; i < fd_count; i++) {
  19. if (ufds[i].revents == POLLIN) {
  20. if (ufds[i].fd == get_property_set_fd())
  21. handle_property_set_fd();  // 处理设置属性的命令
  22. else if (ufds[i].fd == get_keychord_fd())
  23. handle_keychord();     // 处理类似混合按键的命令,类似同时按
  24. // 钢琴上的若干键
  25. else if (ufds[i].fd == get_signal_fd())
  26. handle_signal();       // 处理因子进程挂掉而发来的信号
  27. }
  28. }
  29. }
  30. . . . . . .
  31. }

当监听到signal_recv_fd有动静时,会调用handle_signal()来处理:

  1. void handle_signal(void)
  2. {
  3. char tmp[32];
  4. /* we got a SIGCHLD - reap and restart as needed */
  5. read(signal_recv_fd, tmp, sizeof(tmp));
  6. while (!wait_for_one_process(0))
  7. ;
  8. }

wait_for_one_process()的代码截选如下:

  1. static int wait_for_one_process(int block)
  2. {
  3. . . . . . .
  4. while ( (pid = waitpid(-1, &status, block ? 0 : WNOHANG)) == -1
  5. && errno == EINTR );
  6. . . . . . .
  7. svc = service_find_by_pid(pid);   // 查询出是哪个service进程挂掉了
  8. . . . . . .
  9. svc->pid = 0;
  10. svc->flags &= (~SVC_RUNNING);
  11. if ((svc->flags & SVC_ONESHOT) && !(svc->flags & SVC_RESTART)) {
  12. svc->flags |= SVC_DISABLED;
  13. }
  14. if (svc->flags & (SVC_DISABLED | SVC_RESET) )  {
  15. notify_service_state(svc->name, "stopped");
  16. return 0;
  17. }
  18. . . . . . .
  19. svc->flags &= (~SVC_RESTART);
  20. svc->flags |= SVC_RESTARTING;
  21. /* Execute all onrestart commands for this service. */
  22. list_for_each(node, &svc->onrestart.commands) {
  23. cmd = node_to_item(node, struct command, clist);
  24. cmd->func(cmd->nargs, cmd->args);
  25. }
  26. notify_service_state(svc->name, "restarting");
  27. return 0;
  28. }

该函数的代码比较清晰,当init进程被通知某个子进程终止时,它会尝试找到这个子进程对应的service节点,并辗转给该节点的flags域添加SVC_RESTARTING标记,然后又会马上执行这个service节点中所有onrestart选项对应的动作。

代码中处理SVC_ONESHOT的地方多判断了SVC_RESTART标志,这是为什么呢?我想理由是这样的:SVC_ONESHOT表达的意思是“只打一枪”,也就是说以它装饰的service进程,就算挂掉了,也不会重新启动。然而必须兼顾到其他进程restart的情况。假如有另一个进程会连锁restart该service,此时就算该service有SVC_ONESHOT标志,它还是应该再次启动的。

svc节点的onrestart域本身就是个action类型的域:

  1. struct action onrestart;

现在开始遍历onrestart域里的commands列表:

  1. list_for_each(node, &svc->onrestart.commands) {
  2. cmd = node_to_item(node, struct command, clist);
  3. cmd->func(cmd->nargs, cmd->args);
  4. }

看来,service的那些onrestart子句是一次性完成的。我们以前文说的zygote服务为例,当它重启时,会执行两次do_write()以及两次do_start(),分别启动media服务和netd服务。

最后,wait_for_one_process()还会调用一下notify_service_state()。毕竟这是因为某个service挂掉了,才会再走到这里的,现在我们马上就要重新启动那个刚死的service啦,所以最好还是做一些必要的“通知动作”。请注意,这种关于重启service的“通知”并不是简单发个事件什么的,而是设置某个相应的系统属性。具体的动作请看notify_service_state()的代码:

  1. void notify_service_state(const char *name, const char *state)
  2. {
  3. char pname[PROP_NAME_MAX];
  4. int len = strlen(name);
  5. if ((len + 10) > PROP_NAME_MAX)
  6. return;
  7. snprintf(pname, sizeof(pname), "init.svc.%s", name);
  8. property_set(pname, state);
  9. }

看到了吗?会设置一个以“init.svc.”打头的系统属性。比如重启zygote服务,此时就会把“init.svc.zygote”属性值设为“SVC_RESTARTING”。

大家有没有注意到,wait_for_one_process()里根本没有fork动作。这也就是说,wait_for_one_process()中并不会立即重启新的service进程。大家都知道现在我们正处于init进程的无限for循环中,所以程序从wait_for_one_process()返回后,总会再次走到for循环中的restart_processes():

  1. int main(int argc, char **argv)
  2. {
  3. . . . . .
  4. for(;;) {
  5. int nr, i, timeout = -1;
  6. execute_one_command();
  7. restart_processes();

此时才会重启新的进程:

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

遍历service_list列表,找出那些flags中携带有SVC_RESTARTING标志的service节点,并执行restart_service_if_needed()。

  1. static void restart_service_if_needed(struct service *svc)
  2. {
  3. time_t next_start_time = svc->time_started + 5;
  4. if (next_start_time <= gettime()) {
  5. svc->flags &= (~SVC_RESTARTING);
  6. service_start(svc, NULL);
  7. return;
  8. }
  9. if ((next_start_time < process_needs_restart) ||
  10. (process_needs_restart == 0)) {
  11. process_needs_restart = next_start_time;
  12. }
  13. }

注意,为了防止出现service连续紧密重启的情况,next_start_time会赋值为svc->time_started + 5,也就是说,至少得喘息个5毫秒,然后才能进行下一次重启。这就是Android中重启service的具体流程。

4.2混合按键是如何启动service的?

现在我们顺便说一下用混合按键重启service的技术,这部分内容现在已经很少用到了。至少在我们常见的项目的init.rc脚本里是搜不到“keycodes”关键字的。这个关键字是个option,如果某个service里含有keycodes选项的话,就说明设计者希望在用户按下某种组合键时,init进程能重启这个service。

这种能点击出的组合键,很像同时按下几个钢琴键而发出和旋,因此被称为keychord。在init进程的启动子过程中,“keychord(初始化)子阶段”甚至还要早于“init子阶段”呢。

  1. queue_builtin_action(keychord_init_action, "keychord_init");
其中keychord_init_action()的代码如下:

【system/core/init/Init.c】

  1. static int keychord_init_action(int nargs, char **args)
  2. {
  3. keychord_init();
  4. return 0;
  5. }
【system/core/init/Keychords.c】
  1. void keychord_init()
  2. {
  3. int fd, ret;
  4. service_for_each(add_service_keycodes);
  5. if (!keychords)
  6. return;
  7. fd = open("/dev/keychord", O_RDWR);
  8. if (fd < 0) {
  9. ERROR("could not open /dev/keychord\n");
  10. return;
  11. }
  12. fcntl(fd, F_SETFD, FD_CLOEXEC);
  13. ret = write(fd, keychords, keychords_length);
  14. if (ret != keychords_length) {
  15. ERROR("could not configure /dev/keychord %d (%d)\n", ret, errno);
  16. close(fd);
  17. fd = -1;
  18. }
  19. free(keychords);
  20. keychords = 0;
  21. keychord_fd = fd;
  22. }
初始化时,利用service_for_each(),遍历service_list列表,对每个列表节点调用add_service_keycodes(),该函数代码如下:

【system/core/init/Keychords.c】

  1. void add_service_keycodes(struct service *svc)
  2. {
  3. struct input_keychord *keychord;
  4. int i, size;
  5. if (svc->keycodes) {
  6. /* add a new keychord to the list */
  7. size = sizeof(*keychord) +
  8. svc->nkeycodes * sizeof(keychord->keycodes[0]);
  9. keychords = realloc(keychords, keychords_length + size);
  10. if (!keychords) {
  11. ERROR("could not allocate keychords\n");
  12. keychords_length = 0;
  13. keychords_count = 0;
  14. return;
  15. }
  16. keychord = (struct input_keychord *)
  17. ((char *)keychords + keychords_length);
  18. keychord->version = KEYCHORD_VERSION;
  19. keychord->id = keychords_count + 1;
  20. keychord->count = svc->nkeycodes;
  21. svc->keychord_id = keychord->id;
  22. for (i = 0; i < svc->nkeycodes; i++) {
  23. keychord->keycodes[i] = svc->keycodes[i];
  24. }
  25. keychords_count++;
  26. keychords_length += size;
  27. }
  28. }

其中用到的keychords是个静态变量:

  1. static struct input_keychord *keychords = 0;

它实质上指向了一块buffer,该buffer最终会存下所有keychord信息。当我们遍历service_list列表时,一旦发现某个service节点携带有keycodes,就会从这个buffer中划分出一块,并在其中写入从service节点读取到的keycodes信息。因为不同service携带的keycode部分可能不一样,所以每次分出的那块内存的大小也不太一样。不过大体上每一小块记录的都是input_keychord结构,该结构的定义如下:
【kernel/include/linux/Keychord.h】

  1. struct input_keychord {
  2. __u16 version;
  3. __u16 id;
  4. __u16 count;
  5. __u16 keycodes[];
  6. };

另外,请注意上面代码中的这几句:

  1. keychord->id = keychords_count + 1;
  2. keychord->count = svc->nkeycodes;
  3. svc->keychord_id = keychord->id;

keychord信息里有个唯一的id号,而且这个id号还会回写到service节点的keychord_id域。

经过这次遍历,我们大体上可以画出下面这样的示意图:

在整理好keychords这块buffer后,keychord_init()会把它写入“/dev/keychord”设备文件。

  1. fd = open("/dev/keychord", O_RDWR);
  2. . . . . . .
  3. ret = write(fd, keychords, keychords_length);

这应该是向驱动层通知重要信息了。而且请注意,这个fd文件描述符会被记录下来:

  1. keychord_fd = fd;

记录下fd有什么用呢?很简单,init进程在最后那个for循环里,会监听这个fd,从而感知到从驱动层发来的混合按键,代码如下:

  1. if (!keychord_fd_init && get_keychord_fd() > 0) {
  2. ufds[fd_count].fd = get_keychord_fd(); // 得到的就是那个keychord文件描述符
  3. ufds[fd_count].events = POLLIN;
  4. ufds[fd_count].revents = 0;
  5. fd_count++;
  6. keychord_fd_init = 1;
  7. }

一旦监听到有混合按键发生了,就会走到下面的handle_keychord():

  1. for (i = 0; i < fd_count; i++) {
  2. if (ufds[i].revents == POLLIN) {
  3. if (ufds[i].fd == get_property_set_fd())
  4. handle_property_set_fd();
  5. else if (ufds[i].fd == get_keychord_fd())
  6. handle_keychord();   // 处理混合按键
  7. else if (ufds[i].fd == get_signal_fd())
  8. handle_signal();
  9. }
  10. }

【system/core/init/Keychords.c】

  1. void handle_keychord()
  2. {
  3. struct service *svc;
  4. char adb_enabled[PROP_VALUE_MAX];
  5. int ret;
  6. __u16 id;
  7. // Only handle keychords if adb is enabled.
  8. property_get("init.svc.adbd", adb_enabled);
  9. ret = read(keychord_fd, &id, sizeof(id));
  10. if (ret != sizeof(id)) {
  11. ERROR("could not read keychord id\n");
  12. return;
  13. }
  14. if (!strcmp(adb_enabled, "running")) {
  15. svc = service_find_by_keychord(id);
  16. if (svc) {
  17. INFO("starting service %s from keychord\n", svc->name);
  18. service_start(svc, NULL);
  19. } else {
  20. ERROR("service for keychord %d not found\n", id);
  21. }
  22. }
  23. }

此时会从/dev/keychord设备文件里读取一个id号,还记得前文说到的“id号会回写到service节点的keychord_id域”吗,现在会再次遍历service_list列表,找到那个keychord_id和读到的id匹配的service节点,然后调用service_start(svc, NULL)启动这个service。

5小结

关于init进程,我们就先说这么多吧。限于篇幅,我们不得不把很多不那么重要的细节省去,有兴趣的同学可以自行深入研究。

转自http://blog.csdn.net/codefly/article/details/48392745

Android4.4的init进程的更多相关文章

  1. Android4.4的zygote进程(下)

    3.2.4启动Android系统服务——startSystemServer() 接下来就是启动Android的重头戏了,此时ZygoteInit的main()函数会调用startSystemServe ...

  2. Android4.4的zygote进程(上)

    1背景 前些天为了在科室做培训,我基于Android 4.4重新整理了一份关于zygote的文档.从技术的角度看,这几年zygote并没有出现什么大的变化,所以如果有人以前研究过zygote,应该不会 ...

  3. Android系统init进程启动及init.rc全解析

    转:https://blog.csdn.net/zhonglunshun/article/details/78615980 服务启动机制system/core/init/init.c文件main函数中 ...

  4. Linux---从start_kernel到init进程启动

    “平安的祝福 + 原创作品转载请注明出处 + <Linux内核分析>MOOC课程http://mooc.study.163.com/course/USTC-1000029000 ” ini ...

  5. Linux init进程详解

    init模块 一般来说,Linux程序只能用另一个Linux程序启动.例如,登录Linux终端程序Mingetty. 但终端程序又由谁启动呢?在计算机上启动Linux时,内核装入并启动init程序. ...

  6. imx6 启动 init进程

    之前不知道imx6内核是怎么启动文件系统的init进程,查了下资料,记录于此,以后再来补充. kernel/init/main.c static noinline int init_post(void ...

  7. Android init进程概述

    init进程,其程序位于根文件系统中,在kernle自行启动后,其中的 start_kernel 函数把根文件系统挂载到/目录后,在 rest_init 函数中通过 kernel_thread(ker ...

  8. init进程解析rc文件的相关函数分析

    init进程的源码文件位于system/core/init,其中解析rc文件语法的代码放在五个函数中, init_parse_config_file (init_parser.c), read_fil ...

  9. init进程学习

    linux的init进程 一个在线编辑markdown文档的编辑器,是内核启动的第一个进程,init进程有很多重要的任务,它的pit 为1,在linux shell中使用pstree命令可以看到它为其 ...

随机推荐

  1. Unity3d通用工具类之定时触发器

    时隔多日,好不容易挤出点时间来写写博文.不容易,请送我几朵红花,点个赞也行. 今天呢,我们主要来扩展下通用工具类==>定时触发器. 顾名思义,所谓的定时触发器,就是告诉程序在过多长时间后,我要执 ...

  2. Android内存优化1 了解java内存分配 1

    开篇废话 今天我们一起来学习JVM的内存分配,主要目的是为我们Android内存优化打下基础. 一直在想以什么样的方式来呈现这个知识点才能让我们易于理解,最终决定使用方法为:图解+源代码分析. 欢迎访 ...

  3. python编码规范、js编码规范及IDE的检查插件pylint/eslint等

    一.python规范 参考:https://zh-google-styleguide.readthedocs.io/en/latest/google-python-styleguide/的风格规范和语 ...

  4. HDU 4886 TIANKENG’s restaurant(Ⅱ) hash+dfs

    题意: 1.找一个字符串s使得 s不是给定母串的子串 2.且s要最短 3.s在最短情况下字典序最小 hash.,,结果t掉了...加了个姿势怪异的hash值剪枝才过.. #include <cs ...

  5. Python爬虫之一 PySpider 抓取淘宝MM的个人信息和图片

    ySpider 是一个非常方便并且功能强大的爬虫框架,支持多线程爬取.JS动态解析,提供了可操作界面.出错重试.定时爬取等等的功能,使用非常人性化. 本篇通过做一个PySpider 项目,来理解 Py ...

  6. (六)SSO之CAS框架扩展 改动CAS源代码实现与ESS动态password验证对接

    题记: 偶尔的偶尔我们会听到这个站点的数据泄露了,那个站点的用户数据泄露了.让用户又一次改动登录password,所以,对于用户数据安全性越发的引起我们的重视了,尤其是一些保密性要求高的站点.更须要添 ...

  7. [Algorithms] Build a Binary Tree in JavaScript and Several Traversal Algorithms

    A binary tree is a tree where each node may only have up to two children. These children are stored ...

  8. Joiner的用法

    Google Guava提供了Joiner类专门用来连接String. 譬如说有个String数组,里面有"a","b","c",我们可以通 ...

  9. vue 的 生命周期

    图示: 解析: 那么下面我们来进行测试一下 <section id="app-8"> {{data}} </section> var myVue=new V ...

  10. windows下更换jdk运行当前jar包处理命令一则

    可在文本文档中新建以下内容 set JAVA_HOME=C:\jdk1.7.0_67set CLASSPATH=.;%JAVA_HOME%\lib\dt.jar;%JAVA_HOMe%\lib\too ...