3.2 init分析

init进程的入口函数是main,它的代码如下所示:


[—>init.c]

int main(int argc,char**argv)

{

int device_fd=-1;

int property_set_fd=-1;

int signal_recv_fd=-1;

int keychord_fd=-1;

int fd_count;

int s[2];

int fd;

struct sigaction act;

char tmp[PROP_VALUE_MAX];

struct pollfd ufds[4];

char*tmpdev;

char*debuggable;

//设置子进程退出的信号处理函数,该函数为sigchld_handler。

act.sa_handler=sigchld_handler;

act.sa_flags=SA_NOCLDSTOP;

act.sa_mask=0;

act.sa_restorer=NULL;

sigaction(SIGCHLD,&act,0);

……//创建一些文件夹,并挂载设备,这些是与Linux相关的,不做过多讨论。

mkdir("/dev/socket",0755);

mount("devpts","/dev/pts","devpts",0,NULL);

mount("proc","/proc","proc",0,NULL);

mount("sysfs","/sys","sysfs",0,NULL);

//重定向标准输入/输出/错误输出到/dev/null

open_devnull_stdio();

/*

设置init的日志输出设备为/dev/kmsg,不过该文件打开后,会立即被unlink了,这样,其他进程就无法打开这个文件读取日志信息了。

*/

log_init();

//上面涉及很多与Linux系统相关的知识,不熟悉的读者可自行研究,它们不影响我们的分析。

//解析init.rc配置文件

parse_config_file("/init.rc");

……

//下面这个函数通过读取/proc/cpuinfo得到机器的Hardware名,笔者的HTC G7手机为bravo。

get_hardware_name();

snprintf(tmp,sizeof(tmp),"/init.%s.rc",hardware);

//解析这个和机器相关的配置文件,笔者的G7手机对应文件为init.bravo.rc。

parse_config_file(tmp);

/*

解析完上述两个配置文件后,会得到一系列的Action(动作),下面两句代码将执行那些处于early-init阶段的Action。init将动作执行的时间划分为四个阶段:early-init、init、early-boot、boot。由于有些动作必须在其他动作完成后才能执行,所以就有了先后之分。哪些动作属于哪个阶段由配置文件决定,后面会介绍配置文件的相关知识。

*/

action_for_each_trigger("early-init",action_add_queue_tail);

drain_action_queue();

/*

创建利用Uevent与Linux内核交互的socket。关于Uevent的知识,第9章中对Vold进行分析时会做介绍。

*/

device_fd=device_init();

//初始化和属性相关的资源property_init();

//初始化/dev/keychord设备,这与调试有关,本书不讨论它的用法。读者可以自行研究,

//内容比较简单。

keychord_fd=open_keychord();

……

/*

INIT_IMAGE_FILE定义为"/initlogo.rle",下面这个函数将加载这个文件作为系统的开机画面,注意,它不是开机动画控制程序bootanimation加载的开机动画文件。

*/

if(load_565rle_image(INIT_IMAGE_FILE)){

/*

如果加载initlogo.rle文件失败(可能是没有这个文件),则会打开/dev/ty0设备,并输出"ANDROID"的字样作为开机画面。在模拟器上看到的开机画面就是它。

*/

……

}

}

if(qemu[0])

import_kernel_cmdline(1);

……

//调用property_set函数设置属性项,一个属性项包括属性名和属性值。

property_set("ro.bootloader",bootloader[0]?bootloader:"unknown");

……//执行位于init阶段的动作

action_for_each_trigger("init",action_add_queue_tail);

drain_action_queue();

//启动属性服务

property_set_fd=start_property_service();

/*

调用socketpair函数创建两个已经connect好的socket。socketpair是Linux的系统调用,不熟悉的读者可以利用man socketpair查询相关信息。后面就会知道它们的用处了。

*/

if(socketpair(AF_UNIX,SOCK_STREAM,0,s)==0){

signal_fd=s[0];

signal_recv_fd=s[1];

……

}

……

//执行配置文件中early-boot和boot阶段的动作。

action_for_each_trigger("early-boot",action_add_queue_tail);

action_for_each_trigger("boot",action_add_queue_tail);

drain_action_queue();

……

//init关注来自四个方面的事情。

ufds[0].fd=device_fd;//device_fd用于监听来自内核的Uevent事件。

ufds[0].events=POLLIN;

ufds[1].fd=property_set_fd;//property_set_fd用于监听来自属性服务器的事件。

ufds[1].events=POLLIN;

//signal_recv_fd由socketpair创建,它的事件来自另外一个socket。

ufds[2].fd=signal_recv_fd;

ufds[2].events=POLLIN;

fd_count=3;

if(keychord_fd>0){

//如果keychord设备初始化成功,则init也会关注来自这个设备的事件。

ufds[3].fd=keychord_fd;

ufds[3].events=POLLIN;

fd_count++;

}

……

if BOOTCHART

……//与Boot char相关,不做讨论了。

/*

Boot chart是一个小工具,它能对系统的性能进行分析,并生成系统启动过程的图表,以提供一些有价值的信息,而这些信息最大的用处就是帮助提升系统的启动速度。

*/

endif

for(;){

//从此init将进入一个无限循环。

int nr,i,timeout=-1;

for(i=0;i<fd_count;i++)

ufds[i].revents=0;

//在循环中执行动作

drain_action_queue();

restart_processes();//重启那些已经死去的进程

……

if BOOTCHART

……//Boot Chart相关

endif

//调用poll等待一些事情的发生nr=poll(ufds,fd_count,timeout);

……

//ufds[2]保存的是signal_recv_fd,用于接收来自socket的消息。

if(ufds[2].revents==POLLIN){

//有一个子进程去世,init要处理这个事情。

read(signal_recv_fd,tmp,sizeof(tmp));

while(!wait_for_one_process(0))

continue;

}

if(ufds[0].revents==POLLIN)

handle_device_fd(device_fd);//处理Uevent事件。

if(ufds[1].revents==POLLIN)

handle_property_set_fd(property_set_fd);//处理属性服务的事件。

if(ufds[3].revents==POLLIN)

handle_keychord(keychord_fd);//处理keychord事件。

}

return 0;

}


从上面的代码可知,init的工作任务还是很重的。上面的代码虽已省略了不少行,可结果还是很长,不过从本章要分析的两个知识点来看,可将init的工作流程精简为以下四点:

解析两个配置文件,我们将分析其中对init.rc文件的解析。

执行各个阶段的动作,创建zygote的工作就是在其中的某个阶段完成的。

调用property_init初始化属性相关的资源,并且通过property_start_service启动属性服务。

init进入一个无限循环,并且等待一些事情的发生。重点关注init如何处理来自socket和来自属性服务器的相关事情。

提示 精简工作流程,是本书后面分析代码时常用的方法。读者在分析代码的过程中,也可使用这种方法。

3.2.1 解析配置文件

根据上面的代码可知,在init中会解析两个配置文件,其中一个是系统配置文件init.rc,另外一个是与硬件平台相关的配置文件。以HTC G7手机为例,这个配置文件名为init.bravo.rc,其中bravo是硬件平台的名称。对这两个配置文件进行解析,调用的是同一个parse_config_file函数。下面就来看这个函数,在分析过程中以init.rc为主。


[—>parser.c]

int parse_config_file(const char*fn)

{

char*data;

data=read_file(fn,0);//读取配置文件的内容,这个文件是init.rc。

if(!data)return-1;

parse_config(fn,data);//调用parse_config做真正的解析。

return 0;

}


读取完文件的内容后,将调用parse_config进行解析,这个函数的代码如下所示:


[—>parser.c]

static void parse_config(const charfn,chars)

{

struct parse_state state;

char*args[SVC_MAXARGS];

int nargs;

nargs=0;

state.filename=fn;

state.line=1;

state.ptr=s;

state.nexttoken=0;

state.parse_line=parse_line_no_op;//设置解析函数,不同的内容用不同的解析函数。

for(;){

switch(next_token(&state)){

case T_EOF:

state.parse_line(&state,0,0);

return;

case T_NEWLINE:

if(nargs){

//得到关键字的类型。

int kw=lookup_keyword(args[0]);

if(kw_is(kw,SECTION)){//判断关键字类型是不是SECTION。

state.parse_line(&state,0,0);

parse_new_section(&state,kw,nargs,args);//解析这个SECTION。

}else{

state.parse_line(&state,nargs,args);

}

nargs=0;

}

break;

case T_TEXT:

……

break;

}

}

}


上面就是parse_config函数,代码虽短,实际却比较复杂。从整体来说,parse_config首先会找到配置文件的一个section,然后针对不同的section使用不同的解析函数来解析。那么,什么是section呢?这和init.rc文件的组织结构有关。先不必急着去看init.rc,还是先到代码中去寻找答案。

1.关键字定义

keywords.h这个文件定义了init中使用的关键字,它的用法很有意思,先来看这个文件,代码如下所示:


[—>keywords.h]

ifndef KEYWORD//如果没有定义KEYWORD宏,则走下面的分支。

……//声明一些函数,这些函数就是前面所说的Action的执行函数。

int do_class_start(int nargs,char**args);

int do_class_stop(int nargs,char**args);

……

int do_restart(int nargs,char**args);

……

defineMAKE_KEYWORD_ENUM//定义一个宏。

/*

定义KEYWORD宏,虽然有四个参数,但这里只用第一个,其中K_##symbol中的##表示连接的意思,即最后得到的值为K_symbol。symbol其实就是init.rc中的关键字。

*/

define KEYWORD(symbol,flags,nargs,func)K_##symbol,

enum{//定义一个枚举,这个枚举定义了各个关键字的枚举值。

K_UNKNOWN,

endif

……

//根据上面KEYWORD的定义,这里将得到一个枚举值K_class。

KEYWORD(class,OPTION,0,0)

KEYWORD(class_start,COMMAND,1,do_class_start)//K_class_start

KEYWORD(class_stop,COMMAND,1,do_class_stop)//K_class_stop

KEYWORD(on,SECTION,0,0)//K_on

KEYWORD(oneshot,OPTION,0,0)

KEYWORD(onrestart,OPTION,0,0)

KEYWORD(restart,COMMAND,1,do_restart)

KEYWORD(service,SECTION,0,0)

……

KEYWORD(socket,OPTION,0,0)

KEYWORD(start,COMMAND,1,do_start)

KEYWORD(stop,COMMAND,1,do_stop)

……

ifdefMAKE_KEYWORD_ENUM

KEYWORD_COUNT,

};

undefMAKE_KEYWORD_ENUM

undef KEYWORD//取消KEYWORD宏的定义。

endif


keywords.h好像没什么奇特之处,它不过是个简单的头文件,为什么说它的用法很有意思呢?接下来看在代码中是如何使用它的,如下所示:


[—>parser.c]

……//parser.c中将包含keywords.h头文件,而且还不止一次!

//第一次包含keywords.h,根据keywords.h的代码,我们首先会得到一个枚举定义。

include"keywords.h"

/*

重新定义KEYWORD宏,这回四个参数全用上了,看起来好像是一个结构体。其中#symbol表示一个字符串,其值为“symbol”。

*/

define KEYWORD(symbol,flags,nargs,func)\

[K_##symbol]={#symbol,func,nargs+1,flags,},

//定义一个结构体keyword_info数组,它用来描述关键字的一些属性,请注意里面的注释内容。

struct{

const char*name;//关键字的名称。

int(func)(int nargs,char*args);//对应关键字的处理函数。

unsigned char nargs;//参数个数,每个关键字的参数个数是固定的。

//关键字的属性有三种:COMMAND、OPTION和SECTION,其中COMMAND有对应的处理函数。

unsigned char flags;

}keyword_info[KEYWORD_COUNT]={

[K_UNKNOWN]={"unknown",0,0,0},

/*

第二次包含keywords.h,由于已经重新定了KEYWORD宏,所以以前那些作为枚举值的关键字现在变成keyword_info数组的索引了。

*/

include"keywords.h"

};

undef KEYWORD

//一些辅助宏,帮助我们快速操作keyword_info中的内容。

define kw_is(kw,type)(keyword_info[kw].flags&(type))

define kw_name(kw)(keyword_info[kw].name)

define kw_func(kw)(keyword_info[kw].func)

define kw_nargs(kw)(keyword_info[kw].nargs)


现在领略了keywords.h的神奇之处了吧?原来它干了两件事情:

第一次包含keywords.h时,它声明了一些诸如do_class_start的函数,另外还定义了一个枚举,枚举值为K_class,K_mkdir等关键字。

第二次包含keywords.h后,得到了一个keyword_info结构体数组,这个keyword_info结构体数组以前面定义的枚举值为索引,存储对应的关键字信息,这些信息包括关键字名称、处理函数、处理函数的参数个数,以及属性。

目前,关键字信息中最重要的就是symbol和flags了。什么样的关键字被认为是section呢?根据keywords.h的定义,当symbol为on或service的时候表示section:


KEYWORD(on,SECTION,0,0)

KEYWORD(service,SECTION,0,0)


有了上面的知识,再来看配置文件init.rc的内容就比较容易了。

2.解析init.rc

init.rc的内容如下所示(这里只截取了部分内容,注意,其中的注释符号是#):


[—>init.rc]

on init#根据上面的分析可知,on关键字标示一个section,对应的名字是"init"

……#下面所有的内容都属于这个section,直到下一个section开始时。

export PATH/sbin:/system/sbin:/system/bin:/system/xbin

export LD_LIBRARY_PATH/system/lib

export ANDROID_BOOTLOGO 1#根据keywords.h的定义可知,export表示一个COMMAND。

export ANDROID_ROOT/system

export ANDROID_ASSETS/system/app

……#省略部分内容

on boot#这是一个新的section,名为"boot"。

ifup lo#这是一个COMMAND。

hostname localhost

domainname localdomain

……

class_start也是一个COMMAND,对应函数为do_class_start,很重要,切记。

class_start default

……

下面这个section的意思是:待属性persist.service.adb.enable的值变为1后,#需要执行对应的COMMAND,这个COMMAND是start adbd。

on property:persist.service.adb.enable=1

start adbd//start是一个COMMAND

on property:persist.service.adb.enable=0

stop adbd

……

service也是section的标示,对应section的名为"zygote"。

service zygote/system/bin/app_process-Xzygote/system/bin-zygote\

—start-system-server

socket zygote stream 666#socket关键字表示OPTION。

onrestart write/sys/android_power/request_state wake#onrestart也是OPTION。

onrestart write/sys/power/state on

onrestart restart media

一个service(同时也是一个section),名为"media"

service media/system/bin/mediaserver

user media

group system audio camera graphics inet net_bt net_bt_admin net_raw

ioprio rt 4


从上面对init.rc的分析可知:

一个section的内容从这个标识section的关键字开始,到下一个标识section的地方结束。

init.rc中出现了名为boot和init的section,这里的boot和init就是前面介绍的4个动作执行阶段中的boot和init。也就是说,在boot阶段执行的动作都是由boot这个section定义的。

另外还可发现,zygote被放在了一个service section中。下面以zygote这个section为例,介绍service是如何解析的。