这里开始分析init进程中配置文件的解析,在配置文件中的命令的执行和服务的启动。
首先init是一个可执行文件,它的对应的Makfile是init/Android.mk。 Android.mk定义了init
程序在编译的时候,使用了哪些源码,以及生成方式。当init程序生成之后,最终会放到/init,
即根目录的init文件。通常所说的init进程就是执行这个init程序。
执行这个init程序的代码是在KERNEL/init/main.c文件中的kernel_init()函数里,当kernel
把一些基本的工作,比如CPU初始化,内存初始化,输入输出初始化等等完成之后,就会找到
根目录下的init程序,然后执行这个程序。
在Android系统中,init程序对应的代码在ANDROID/system/core/init/下,用Android.mk,文件
管理编译的。这个程序的入口函数是init.c的main函数。在Android下init进程的主要功能和其它
发行版的Linux的系统差不多,都是通过解析配置文件,完成Linux系统的基本的操作,比如
用户级别,权限的设定,一些安全策略的启动,如selinux的启动,基本的文件(/dev, /sys等)创建,
然后就是其它基本的进程。 init的进程的id永远为1,在系统中最基本的进程之一。
今天我们主要目的目标如下:
1) init进程是如何解析配置文件的
2) init进程是如何启动其它基本的进程的
1 init进程是如何解析配置文件
1.1 了解init.rc文件的语法(见init.rc语法)
1.2 init.rc对应的数据结构
parse_state这个结构体是用来跟踪整个init.rc文件解析过程的。这个在阅读代码时,把这个结构体理解为一个篮子,
这个篮子中放的都是各种各样的rc文件内容即可。
1 struct parse_state 2 struct command{ 3 struct listnode clist;//命令数列 4 5 int (*func)(int nargs, char **args);//当前命令对应的函数,比如write命令,对应do_write(xxx) 6 int nargs; //参数个数 7 char *args[1]; //参数数组 8 };
action就是on xxx对应的部分,action主要是由command组成。Android系统在开机执行的顺序,
是按照这个action顺序执行的。也就是说系统触发不同的trigger,就顺序执行这个trigger对应的action
下的所有命令,这个执行过程是顺序执行的,没有并行进行。而且系统在启动过程中,都是按照action去
执行的。记住,是在开机过程中才是这么执行的。
struct action { struct listnode alist;//系统中所有的action的队列 struct listnode qlist;//等待触发trigger的队列 struct listnode tlist;//这个队列的意义不明,好在也没人用到这个队列 unsigned hash; const char *name;//这个名字就是triggeer的名字 struct listnode commands;//在当前这个action下所有的comman的队列 struct command *current;//当前要执行的命令 };
service也是系统的一个SECTION,这样的SECTION仅有service, import, on xxxx这三种。对这三种
不同的SECTION解析的函数是不同的。记住on xxxx就是一个action。这个数据结构就是对应一个service,在
系统中service是不能直接执行的,它仅仅是一个service的属性信息的集合,为service的启动提供全面的信息,
但是它不是一个命令,所以不能直接执行。前面这些command可能仅仅就是在init进程中执行,但是service不同。
service启动是在一个新的进程中进行的。也就是说init进程会先fork一个进程,然后通过exec家族函数去启动
这个service。所以service是运行在一个新的进程中的。
struct service
1.3 init.rc文件的解析
对于init.rc文件的解析主要是通过以下代码实现的[init.c文件中]:
1 init_parse_config_file("/init.rc");
这句代码是解析所有init.rc文件的入口,其他的*.rc文件都是通过init.rc中通过import一级一级地导入的;
所以从这句代码入手是最合适的地方。 init_parse_config_file函数读取init.rc文件,并调用parse_config函数,
这个函数才是真正解析init.rc文件的地方。先看看这个函数的主要部分,如下[init_parce.c文件中]:
1 static void parse_config(const char *fn, char *s) 2 { 3 //第一部分 4 struct parse_state state; 5 struct listnode import_list; 6 struct listnode *node; 7 char *args[INIT_PARSER_MAXARGS]; 8 int nargs; 9 10 nargs = 0; 11 state.filename = fn; 12 state.line = 0; 13 state.ptr = s; 14 state.nexttoken = 0; 15 state.parse_line = parse_line_no_op; 16 17 list_init(&import_list); 18 state.priv = &import_list; 19 //第二部分 20 for (;;) { 21 switch (next_token(&state)) { 22 case T_EOF: 23 state.parse_line(&state, 0, 0); 24 goto parser_done; 25 case T_NEWLINE: 26 state.line++; 27 if (nargs) { 28 int kw = lookup_keyword(args[0]); 29 if (kw_is(kw, SECTION)) { 30 state.parse_line(&state, 0, 0); 31 parse_new_section(&state, kw, nargs, args); 32 } else { 33 state.parse_line(&state, nargs, args); 34 } 35 nargs = 0; 36 } 37 break; 38 case T_TEXT: 39 if (nargs < INIT_PARSER_MAXARGS) { 40 args[nargs++] = state.text; 41 } 42 break; 43 } 44 } 45 //第三部分 46 parser_done: 47 list_for_each(node, &import_list) { 48 struct import *import = node_to_item(node, struct import, list); 49 int ret; 50 51 INFO("importing ‘%s‘", import->filename); 52 ret = init_parse_config_file(import->filename); 53 if (ret) 54 ERROR("could not import file ‘%s‘ from ‘%s‘\n", 55 import->filename, fn); 56 } 57 }
把这个函数分成三部分理解会更方便,首先第一部分只是在解析过程中一些变量的初始化,主要是就是parse_state结构体
的初始化。真正解析rc文件的是在第二部分;在当前rc文件解析完成后,会处理当前文件import的其它rc文件,这些是在
第三部分做的。如果有import其它的rc文件,在第三部分中会调用init_parse_config_file函数,接着解析导入的rc文件;
从整体上看,像是一个递归函数。我们把重点放在第二部分上。
在第二部分中next_token函数相当于一个简单的词法分析器,这个函数对应的状态机如下图:
这个图画的不是很标准,我再大概注释下吧:对于rc文件内容逐个字母输入这个状态机,如果是字符的话就继续输入下一个字符,直到输入的是空格或\r或者\t,
那么就进入TEXT状态,并把这个token返回给parse_config函数去处理;同样,要是遇到换行,或者文件结束,都要返回给parse_config去处理。
从第二部分可以看出,rc文件的解析是以行为单位进行的,在每一行中检查到一个TEXT,就会把这个单词放入args数组中;
这样,当一行结束时,这个args数组和nargs变量就初始化完成;然后开始换行;在换行时,需要在如下代码(第二部分代码中)中进行:
1 case T_NEWLINE: 2 state.line++; 3 //nargs非0,说明这行中有命令需要解析 4 if (nargs) { 5 //先看看在本行中第一个词是不是关键词, 6 //关键词列表在keywords.h文件中 7 int kw = lookup_keyword(args[0]); 8 //如果第一参数是关键词,那么就会返回关键词的index 9 //然后判断这个关键词是不是SECTION,只有import,on,service 10 //才是SECTION 11 if (kw_is(kw, SECTION)) { 12 state.parse_line(&state, 0, 0); 13 //在parse_new_section中,会解析这个section, 14 //在解析过程中,最重要的是给parse_line赋值; 15 //因为parse_line是个函数指针 16 parse_new_section(&state, kw, nargs, args); 17 } else { 18 //在一个SECTION中,parse_line会保持不变的, 19 //直到遇到下个SECTION之前,这个parse_line函数 20 //会保持不变的 21 state.parse_line(&state, nargs, args); 22 } 23 nargs = 0; 24 } 25 break;
给parse_line赋值是在parse_new_section中进行的,能够赋值给parse_line的函数有parse_line_service,parse_line_action,
对这个两个函数的理解,有助于对init.rc文件解析的理解。对于这两个函数没必要逐行分析,这里不作介绍;
在lookup_keyword函数中,有个地方说明下;可能是我的基础知识不是很牢固,在看这个函数的时候,有些地方卡了一下;
在init_parse.c文件中有如下宏定义
1 #define KEYWORD(symbol, flags, nargs, func, uev_func) 2 [ K_##symbol ] = { #symbol, func, uev_func, nargs + 1, flags, },
而在keywords.h文件中,还有个宏定义如下:
#define KEYWORD(symbol, flags, nargs, func, uev_func) K_##symbol,
这两个宏是一样的。 C语言中宏的作用范围只是在单文件中。宏是在编译过程中进行的替换,这个过程发生在链接之前,所以
宏的作用范围只能是在当前文件中。这样的话,在init_parse.c文件中的宏定义没人用到,因此哪个宏也是无意义的。
2 init进程是如何启动其它基本的进程的
2.1 init进程中命令和服务启动顺序
在正式开始介绍init进程是如何启动其它服务之前,还有一些内容要介绍,就是系统如何确定开机执行顺序的。在init.c文件中有如下代码:
1 action_for_each_trigger("early-init", action_add_queue_tail); 2 queue_builtin_action(wait_for_coldboot_done_action, "wait_for_coldboot_done"); 3 queue_builtin_action(mix_hwrng_into_linux_rng_action, "mix_hwrng_into_linux_rng"); 4 queue_builtin_action(keychord_init_action, "keychord_init"); 5 queue_builtin_action(console_init_action, "console_init"); 6 action_for_each_trigger("init", action_add_queue_tail); 7 if (!is_special) { 8 action_for_each_trigger("early-fs", action_add_queue_tail); 9 action_for_each_trigger("fs", action_add_queue_tail); 10 action_for_each_trigger("post-fs", action_add_queue_tail); 11 action_for_each_trigger("post-fs-data", action_add_queue_tail); 12 } 13 /* Repeat mix_hwrng_into_linux_rng in case /dev/hw_random or /dev/random 14 * wasn‘t ready immediately after wait_for_coldboot_done 15 */ 16 queue_builtin_action(mix_hwrng_into_linux_rng_action, "mix_hwrng_into_linux_rng"); 17 queue_builtin_action(property_service_init_action, "property_service_init"); 18 queue_builtin_action(signal_init_action, "signal_init"); 19 queue_builtin_action(check_startup_action, "check_startup"); 20 if (is_special) { 21 action_for_each_trigger(bootmode, action_add_queue_tail); 22 } else { 23 action_for_each_trigger("early-boot", action_add_queue_tail); 24 action_for_each_trigger("boot", action_add_queue_tail); 25 } 26 queue_builtin_action(queue_property_triggers_action, "queue_property_triggers");
这段代码中主要的函数就两个,分别是action_for_each_trigger,queue_builtin_action,这两个函数操作同意队列,
这个队列就是action_queue。init进程启动顺序就是action在action_queue这个队列中的顺序。action_for_each_trigger
函数就是把*.rc文件中, 对应trigger的action放入action_queue队列。 而queue_builtin_action函数是临时需要在action_queue中
新增加一个action,只不过这个action仅仅有一个command,这个command就是queue_builtin_action函数第一个参数对应的函数。
因此从这段代码中,我们可以看出,init执行顺序是:
early-init, wait_for_coldboot_done, mix_hwrng_into_linux_rng, keychord_init,console_init, init,
early-fs, fs, post-fs, post-fs-data, mix_hwrng_into_linux_rng, property_service_init,signal_init,
check_startup, early-boot, boot, queue_property_triggers.
上面这些代码仅仅是把action_queue队列配置完成,安排好每个action的顺序。 但是现在并没有开始按照这个队列去执行各个
命令或者启动各个服务。
2.2 在init进程中执行命令和服务
这里就开始介绍init进程是如何执行命令和启动服务的。这个过程是在如下代码开始执行的:
1 for(;;) { 2 int nr, i, timeout = -1; 3 //第一部分 4 //execute_one_command是开始执行命令的地方 5 execute_one_command(); 6 //如果有些进程需要重启的话在这里进行 7 restart_processes(); 8 //第二部分 9 //接下来是四个socket,使用Linux的poll机制去监听 10 //这四个文件的状态,与其它进程通信;比如: 11 //当我们通过命令调用 setprop时候,就会和get_property_set_fd 12 //指向的socket通信 13 if (!property_set_fd_init && get_property_set_fd() > 0) { 14 ufds[fd_count].fd = get_property_set_fd(); 15 ufds[fd_count].events = POLLIN; 16 ufds[fd_count].revents = 0; 17 fd_count++; 18 property_set_fd_init = 1; 19 } 20 if (!signal_fd_init && get_signal_fd() > 0) { 21 ufds[fd_count].fd = get_signal_fd(); 22 ufds[fd_count].events = POLLIN; 23 ufds[fd_count].revents = 0; 24 fd_count++; 25 signal_fd_init = 1; 26 } 27 if (!keychord_fd_init && get_keychord_fd() > 0) { 28 ufds[fd_count].fd = get_keychord_fd(); 29 ufds[fd_count].events = POLLIN; 30 ufds[fd_count].revents = 0; 31 fd_count++; 32 keychord_fd_init = 1; 33 } 34 35 if (process_needs_restart) { 36 timeout = (process_needs_restart - gettime()) * 1000; 37 if (timeout < 0) 38 timeout = 0; 39 } 40 41 if (!action_queue_empty() || cur_action) 42 timeout = 0; 43 //这里就是poll机制的利用, 44 //接收到socket通信后,对于这些事件的分配 45 nr = poll(ufds, fd_count, timeout); 46 if (nr <= 0) 47 continue; 48 49 for (i = 0; i < fd_count; i++) { 50 if (ufds[i].revents == POLLIN) { 51 if (ufds[i].fd == get_property_set_fd()) 52 handle_property_set_fd(); 53 else if (ufds[i].fd == get_keychord_fd()) 54 handle_keychord(); 55 else if (ufds[i].fd == get_signal_fd()) 56 handle_signal(); 57 } 58 } 59 }
便于理解,把上述代码分为两个部分。这两个部分都是重点。
不过第一部分更切合我们这一小节的主题:命令的执行和服务的启动。
execute_one_command函数是命令执行和服务启动主要函数,这个函数的代码如下:
1 void execute_one_command(void) 2 { 3 int ret; 4 5 if (!cur_action || !cur_command || is_last_command(cur_action, cur_command)) { 6 //从action_queue中取出第一个action 7 cur_action = action_remove_queue_head(); 8 cur_command = NULL; 9 if (!cur_action) 10 return; 11 INFO("processing action %p (%s)\n", cur_action, cur_action->name); 12 //从当前action中取出第一个command 13 cur_command = get_first_command(cur_action); 14 } else { 15 cur_command = get_next_command(cur_action, cur_command); 16 } 17 18 if (!cur_command) 19 return; 20 //执行这个command 21 ret = cur_command->func(cur_command->nargs, cur_command->args); 22 INFO("command ‘%s‘ r=%d\n", cur_command->args[0], ret); 23 }
上面这个过程就是命令执行的过程.这些命令对应的函数,可以从kerwords.h中看到。如果前面的rc文件解析中,仔细分析
了整个流程的话,就很容易回忆起前面的这些内容。对于这个func也不会陌生,这些函数的实现都是在builtins.c文件中实现的。
在rc文件中出现的命令,绝大部分都是很常见的,不多做介绍。不过,下面还是会以其中一个命令说明下整个过程,这个命令就是
class_start--这个命令是专门用来启动service的,在其他Linux系统中也是没有的。
对于这个命令,首先到keywords.h中找到其对应的函数,如下:
1 KEYWORD(class_start, COMMAND, 1, do_class_start, 0)
所以class_start命令对应的函数实际上就是do_class_start。前文已经说过,这些函数都是在builtins.c文件中实现的,那么这个
函数实现如下:
1 int do_class_start(int nargs, char **args){ 2 /* Starting a class does not start services 3 * which are explicitly disabled. They must 4 * be started individually. 5 * */ 6 service_for_each_class(args[1], service_start_if_not_disabled); 7 return 0; 8 } 9 service_for_each_class函数的实现实在init_parser.c文件中,如下 10 void service_for_each_class(const char *classname, void (*func)(struct service *svc)){ 11 struct listnode *node; 12 struct service *svc; 13 list_for_each(node, &service_list) { 14 svc = node_to_item(node, struct service, slist); 15 if (!strcmp(svc->classname, classname)) { 16 func(svc); 17 } 18 } 19 }
到这里,基本已经可以看出这个命令和service之间的关系来。通过这两个函数,实际上就是通过service_start_if_not_disabled
函数启动所有className与给出的classname相同的service。 这些classname指的就是在定义每一个service的时候,定义在class
之后的部分,比如下面这个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
servicemanager服务的classname就是 core. 那么调用class_start命令的地方在哪里呢?
调用class_start命令的地方在init.rc中,当执行boot action的时候,顺序执行就能执行到这个命令,首先启动的core一级别的服务,
然后启动才是main级别的服务
1 on boot 2 ... 3 class_start core 4 class_start main
既然已经找到了命令开始的地方了,那么我们可以继续看看service到底是如何执行的。这就要看service_start_if_not_disabled函数了,
这个函数的代码如下,在builtins.c文件中:
1 static void service_start_if_not_disabled(struct service *svc){ 2 if (!(svc->flags & SVC_DISABLED)) { 3 service_start(svc, NULL); 4 } 5 }
这里会检查flags中没有disabled选项的启动。在service_start函数是真正启动一个服务的地方。service_start函数在init.c文件中实现的。
当你在这个函数中看到fork()函数时候,你就明白这个服务启动了。而逐个启动每个服务,这个循环过程实在service_for_each_class中的
list_for_each中的,可以回头再看看。
到这里就是介绍完成来通过rc文件启动一个服务的过程。能读到这里,说明你真的很有耐心啊,给自己一个表扬吧。不过这时候,你也许会有
一个疑问,如果一个service中flags中有disabled项时,这样的服务是怎样启动的呢?
下面就以下面这个含有disabled项服务的启动为例,介绍下这类服务的启动过程,这个服务如下:
1 service bootanim /system/bin/bootanimation 2 class main 3 user graphics 4 group graphics 5 disabled 6 oneshot
这个服务是开机动画,如果使用的是模拟器的话,就是开机闪烁android的那个动画。这个服务中含有disabled, 通过上面的解析,我们知道这个服务
肯定不是通过class_start命令启动的。但是在开机过程中,我们的确看到来开机动画,那么这个服务是在何时由谁启动的呢?
启动开机动画是在SurfaceFlinger初始化完成时,由SurfaceFlinger间接启动这个服务的。在SurfceFlinger初始化完成时,调用来函数startBootAnim(),
这个函数通过设置一个系统属性,然后开机动画就开始来。设置这个属性的代码如下:
1 void SurfaceFlinger::startBootAnim() { 2 // 首先是先退出开机动画。如果开机动画在进行中,那么就退出这个开机动画; 3 // 如果开机动画没有运行,那么这个属性设置上也是没有影响的 4 property_set("service.bootanim.exit", "0"); 5 //然后,开始播放动画。init进程会检查这个属性,然后开始动画播放 6 property_set("ctl.start", "bootanim"); 7 }
property_set的函数,在实现的时候,实际上是通过写socket和init进程通信。在前面也说到过,init进程在启动后,会监视四个文件的状态,这四个
文件都是socket文件,其中一直就是属性socket文件的描述符get_property_set_fd(). 通过Linux的poll机制,当有有系统属性设置进来的时候,就会
有触发调用下面的函数:
handle_property_set_fd()
这函数是在init.c文件中被调用,它的实现实在property_service.c文件中。这个函数从socket中读取出传递过来的信息。传递过来的信息都是按照键值对
封装好的。如果键值对中的name中前4个字母是"crtl.",那么就会调用handle_control_message()函数。关于函数handle_property_set_fd()的代码在property_service.c
文件中,这里就不再拿出来单独看了.
在调用到handle_control_message函数,这个函数如下:
1 void handle_control_message(const char *msg, const char *arg) 2 { 3 if (!strcmp(msg,"start")) { 4 msg_start(arg); 5 } else if (!strcmp(msg,"stop")) { 6 msg_stop(arg); 7 } else if (!strcmp(msg,"restart")) { 8 msg_restart(arg); 9 } else { 10 ERROR("unknown control msg ‘%s‘\n", msg); 11 } 12 } 13 static void msg_start(const char *name) 14 { 15 ... 16 svc = service_find_by_name(name); 17 ... 18 service_start(svc, args); 19 ... 20 }
如果命令中的start的话,这里把msg_start函数彻底简写了,仅仅保留这两个最核心的代码。根据service的名字找到这个service,
然后启动service。当你看到service_start()函数的时候,想必你已经明白来整个过程来。service_start()函数在前面有过简略
地介绍,这里也不多说来。
就是类似与这样,init进程在设备启动后,还在忙碌地参与这系统的运行。