[深入理解Android卷一全文-第三章]深入理解init

原创 2015年08月02日 09:49:39

由于《深入理解Android 卷一》和《深入理解Android卷二》不再出版,而知识的传播不应该因为纸质媒介的问题而中断,所以我将在CSDN博客中全文转发这两本书的全部内容。



第3章  深入理解init

本章主要内容

·  深入分析init。

本章涉及的源代码文件名及位置

下面是本章分析的源码文件名及其位置。

·  init.c

system/core/init/init.c

·  parser.c

system/core/init/parser.c

·  builtins.c

system/core/init/builtins.c

·  keywords.h

system/core/init/keywords/h

·  init.rc

system/core/rootdir/init.rc

·  properties_service.c

system/core/init/properties_service.c

·  libc_init_dynamic.c

bionic/libc/bionic/libc_init_common.c

·  libc_init_common.c

bionic/libc/bionic/libc_init_common.c

·  properties.c

system/core/libcutils/properties.c

3.1  概述

 init是一个进程,确切地说,它是Linux系统中用户空间的第一个进程。由于Android是基于Linux内核的,所以init也是Android系统中用户空间的第一个进程,它的进程号是1。作为天字第一号的进程,init被赋予了很多极其重要的工作职责,本章将关注其中两个比较重要的职责:

·  init进程负责创建系统中的几个关键进程,尤其是下一章要介绍的Zygote,它更是Java世界的开创者。那么,init进程是如何创建Zygote的呢?

·  Android系统有很多属性,于是init就提供了一个property service(属性服务)来管理它们。那么这个属性服务是怎么工作的呢?

如上所述,本章将通过下面两方面内容来分析init:

·  init如何创建zygote。

·  init的属性服务是如何工作的。

 

3.2  init分析

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

[-->init.c]

int main(int argc, char **argv)

{

    intdevice_fd = -1;

    intproperty_set_fd = -1;

    intsignal_recv_fd = -1;

    intkeychord_fd = -1;

    int fd_count;

    ints[2];

    intfd;

    structsigaction act;

    chartmp[PROP_VALUE_MAX];

    structpollfd 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名,我的HTCG7手机为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事件。

    }

 

    return0;

}

从上面的代码中可知,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 char *fn, char*s)

{

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;

      caseT_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);

......

#define __MAKE_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)

    ......

#ifdef __MAKE_KEYWORD_ENUM__

   KEYWORD_COUNT,

};

#undef __MAKE_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 {

    constchar *name;  //关键字的名。

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

unsignedchar 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的神奇之处了吧?原来它干了两件事情:

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

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

目前,关键字信息中最重要的就是symbol和flags了。什么样的关键字被认为是section呢?根据keywords.h的定义,symbol为下面两个的关键字表示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开始时。

 exportPATH /sbin:/system/sbin:/system/bin:/system/xbin

 exportLD_LIBRARY_PATH /system/lib

 exportANDROID_BOOTLOGO 1 #根据keywords.h的定义,export表示一个COMMAND

export ANDROID_ROOT /system

 exportANDROID_ASSETS /system/app

...... #省略部分内容

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

   ifup lo#这是一个COMMAND

   hostname localhost

   domainname localdomain

    ......

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

    class_startdefault 

    ......

#下面这个section的意思是:待属性persist.service.adb.enable的值变为1后,

#需要执行对应的COMMAND,这个COMMAND是start adbd

     onproperty:persist.service.adb.enable=1

         start adbd //start是一个COMMAND

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

         stopadbd

    ......

#service也是section的标示,对应section的名为“zygote“

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

 --start-system-server

    socketzygote stream 666  #socket关键字表示OPTION

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

   onrestart write /sys/power/state on

   onrestart restart media

#一个section,名为”media”

service media /system/bin/mediaserver

    usermedia

    groupsystem audio camera graphics inet net_bt net_bt_admin net_raw

iopriort 4

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

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

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

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

3.2.2  解析service

zygote对应的service section内容是:

[-->init.rc::zygote]

service zygote /system/bin/app_process -Xzygote/system/bin –zygote \ --start-system-server

socketzygote stream 666  #socket是OPTION

#下面的onrestart是OPTION,而write和restart是COMMAND

    onrestartwrite /sys/android_power/request_state wake

   onrestart write /sys/power/state on

onrestartrestart media

解析section的入口函数是parse_new_section,它的代码如下所示:

[-->parser.c]

void parse_new_section(struct parse_state*state, int kw,

                       int nargs, char **args)

{

   switch(kw) {

    caseK_service:  //解析service,用parse_service和parse_line_service

       state->context = parse_service(state, nargs, args);

        if(state->context) {

           state->parse_line = parse_line_service;

            return;

        }

       break;

    caseK_on: //解析on section

        ......//读者可以自己研究

       break;

    }

   state->parse_line = parse_line_no_op;

}

其中,service解析时,用到了parse_service和parse_line_service两个函数,在分别介绍它们之前,先看init是如何组织这个service的。

1. service结构体

init中使用了一个叫service的结构体来保存和service section相关的信息,不妨来看这个结构体,代码如下所示:

[-->init.h::service结构体定义]

struct service {

 //listnode是一个特殊的结构体,在内核代码中用得非常多,主要用来将结构体链接成一个

  //双向链表。init中有一个全局的service_list,专门用来保存解析配置文件后得到的service。

   struct listnode slist; 

    constchar *name; //service的名字,对应我们这个例子就是”zygote”。

    constchar *classname; //service所属class的名字,默认是”defult”

   unsigned flags;//service的属性

    pid_tpid;    //进程号

    time_ttime_started;   //上一次启动的时间

    time_ttime_crashed;  //上一次死亡的时间

    intnr_crashed;        //死亡次数

     uid_tuid;     //uid,gid相关

    gid_tgid;

    gid_tsupp_gids[NR_SVC_SUPP_GIDS];

    size_tnr_supp_gids;

   /*

有些service需要使用socket,下面这个socketinfo用来描述socket的相关信息。

我们的zygote也使用了socket,配置文件中的内容是socket zygote stream 666。

它表示将创建一个AF_STREAM类型的socket(其实就是TCP socket),该socket的名为“zygote”,

读写权限是666。

   */

structsocketinfo *sockets; 

//service一般运行在单独的一个进程中,envvars用来描述创建这个进程时所需的环境变量信息。

    structsvcenvinfo *envvars; 

   /*

  虽然关键字onrestart标示一个OPTION,可是这个OPTION后面一般跟着COMMAND,

 下面这个action结构体可用来存储command信息,马上就会分析到它。

*/

    structaction onrestart;

   

    //和keychord相关的内容

    int*keycodes;

    intnkeycodes;

    intkeychord_id;

    //io优先级设置

    intioprio_class;

    intioprio_pri;

    //参数个数

    intnargs;

    //用于存储参数

    char*args[1];

}; 

我们现在已了解的service的结构体,相对来说还算是清晰易懂的。而zygote中的那三个onrestart该怎么表示呢?请看service中使用的这个action结构体:

[-->init.h::action结构体定义]

struct action {

/*

一个action结构体可存放在三个双向链表中,其中alist用于存储所有action,

qlist用于链接那些等待执行的action,tlist用于链接那些待某些条件满足后

就需要执行的action。

*/

    structlistnode alist;

   structlistnode qlist;

    structlistnode tlist;

 

   unsigned hash;

    constchar *name;

   

   //这个OPTION对应的COMMAND链表,以zygote为例,它有三个onrestart option,所以

  //它对应会创建三个command结构体。

    structlistnode commands;

    structcommand *current;

};

了解了上面的知识后,你是否能猜到parse_service和parse_line_service的作用了呢?马上就来看它们。

2. parse_service

parse_service的代码如下所示:

[-->parser.c]

static void *parse_service(struct parse_state*state, int nargs, char **args)

{

    structservice *svc; //声明一个service结构体

    ......

    //init维护了一个全局的service链表,先判断是否已经有同名的service了。

    svc =service_find_by_name(args[1]);

    if(svc) {

       ......  //如果有同名的service,则不能继续后面的操作。

       return 0;

    }

   

nargs-= 2;

    svc =calloc(1, sizeof(*svc) + sizeof(char*) * nargs);

    ......

   svc->name = args[1];

svc->classname= "default";//设置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);

    //把zygote这个service加到全局链表service_list中。

   list_add_tail(&service_list, &svc->slist);

    returnsvc;

}

parse_service函数只是搭建了一个service的架子,具体的内容尚需由后面的解析函数来填充。来看service的另外一个解析函数parse_line_service。

3. parse_line_service

parse_line_service的代码如下所示:

[-->parser.c]

static void parse_line_service(structparse_state *state, int nargs,

char **args)

{

    structservice *svc = state->context;

    structcommand *cmd;

    int i,kw, kw_nargs;

    ......

   svc->ioprio_class = IoSchedClass_NONE;

    //其实还是根据关键字来做各种处理。

    kw =lookup_keyword(args[0]);

    switch(kw) {

    caseK_capability:

       break;

    caseK_class:

        if(nargs != 2) {

           ......

        }else {

           svc->classname = args[1];

        }

       break;

    ......

caseK_oneshot:

   /*

这是service的属性,它一共有五个属性,分别为:

SVC_DISABLED:不随class自动启动。下面将会看到class的作用。

SVC_ONESHOT:退出后不需要重启,也就是这个service只启动一次就可以了。

SVC_RUNNING:正在运行,这是service的状态。

SVC_RESTARTING:等待重启,这也是service的状态。

SVC_CONSOLE:该service需要使用控制台 。

SVC_CRITICAL:如果在规定时间内该service不断重启,则系统会重启并进入恢复模式。

zygote没有使用任何属性,这表明它:会随着class的处理自动启动;

退出后会由init重启;不使用控制台;即使不断重启也不会导致系统进入恢复模式。

       */

       svc->flags |= SVC_ONESHOT;

       break;

    caseK_onrestart: //根据onrestart的内容,填充action结构体的内容

       nargs--;

       args++;

        kw= lookup_keyword(args[0]);

        ......

        //创建command结构体

       cmd = malloc(sizeof(*cmd) + sizeof(char*) * nargs);

       cmd->func = kw_func(kw);

       cmd->nargs = nargs;

       memcpy(cmd->args, args, sizeof(char*) * nargs);

        //把新建的command加入到双向链表中。

       list_add_tail(&svc->onrestart.commands, &cmd->clist);

       break;

    ......

    caseK_socket: { //创建socket相关信息

       struct socketinfo *si;

        ......

        si= calloc(1, sizeof(*si));

        if(!si) {

           parse_error(state, "out of memory\n");

           break;

        }

       si->name = args[1]; //socket的名字

       si->type = args[2]; //socket的类型

       si->perm = strtoul(args[3], 0, 8); //socket的读写权限

        if(nargs > 4)

           si->uid = decode_uid(args[4]);

        if(nargs > 5)

           si->gid = decode_uid(args[5]);

       si->next = svc->sockets;

       svc->sockets = si;

       break;

    }

    ......

   default:

       parse_error(state, "invalid option '%s'\n", args[0]);

    }

}

parse_line_service将根据配置文件的内容填充service结构体,那么,zygote解析完后会得到什么呢?图3-1表示了zygote解析后的结果:


图3-1  zygote解析结果示意图

从上图中可知:

·  service_list链表将解析后的service全部链接到了一起,并且是一个双向链表,前向节点用prev表示,后向节点用next表示。

·  socketinfo也是一个双向链表,因为zygote只有一个socket,所以画了一个虚框socket做为链表的示范。

·  onrestart通过commands指向一个commands链表,zygote有三个commands。

zygote这个service解析完了,现在就是“万事俱备,只欠东风”了。接下来要了解的是,init是如何控制service的。

3.2.3  init控制service

先看service是如何启动的。

1.启动zygote

init.rc中有这样一句话:

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

 class_startdefault

class_start标示一个COMMAND,对应的处理函数为do_class_start,它位于boot section的范围内。为什么说它很重要呢?

还记得init进程中的四个执行阶段吗?当init进程执行到下面几句话时,do_class_start就会被执行了。

//将bootsection节的command加入到执行队列

action_for_each_trigger("boot",action_add_queue_tail);

//执行队列里的命令,class可是一个COMMAND,所以它对应的do_class_start会被执行。

drain_action_queue();

下面来看do_class_start函数:

[-->builtins.c]

int do_class_start(int nargs, char **args)

{

/*

args为do_class_start的参数,init.rc中只有一个参数,就是default。

下面这个函数将从service_list中寻找classname为”default”的service,然后

调用service_start_if_not_disabled函数。现在读者明白了service结构体中

classname的作用了吗?

*/

service_for_each_class(args[1],service_start_if_not_disabled);

return 0;

}

我们已经知道,zygote这个service的classname的值就是“default”,所以会针对这个service调用service_start_if_not_disabled,这个函数的代码是:

[-->parser.c]

static void service_start_if_not_disabled(structservice *svc)

{

if (!(svc->flags & SVC_DISABLED)) {

     service_start(svc,NULL); //zygote可没有设置SVC_DISABLED

 }

}

service_start函数的代码如下所示:

[-->init.c]

void service_start(struct service *svc, constchar *dynamic_args)

{

    structstat s;

    pid_tpid;

    intneeds_console;

    int n;

 

   svc->flags &= (~(SVC_DISABLED|SVC_RESTARTING));

   svc->time_started = 0;

   

    if(svc->flags & SVC_RUNNING) {

       return;//如果这个service已在运行,则不用处理

    }

  /*

service一般运行于另外一个进程中,这个进程也是init的子进程,所以启动service前需要判断

对应的可执行文件是否存在,zygote对应的可执行文件是/system/bin/app_process

*/

    if(stat(svc->args[0], &s) != 0) {

      svc->flags |= SVC_DISABLED;

       return;

    }

    ......

   pid =fork(); //调用fork创建子进程

if(pid == 0) {

    //pid为零,我们在子进程中

       struct socketinfo *si;

       struct svcenvinfo *ei;

       char tmp[32];

       int fd, sz;

       

//得到属性存储空间的信息并加到环境变量中,后面在属性服务一节中会碰到使用它的地方。

       get_property_workspace(&fd, &sz);

       add_environment("ANDROID_PROPERTY_WORKSPACE", tmp);

        //添加环境变量信息

       for (ei = svc->envvars; ei; ei = ei->next)

           add_environment(ei->name, ei->value);

        //根据socketinfo创建socket

       for (si = svc->sockets; si; si = si->next) {

           int s = create_socket(si->name,

                                  !strcmp(si->type,"dgram") ?

                                  SOCK_DGRAM :SOCK_STREAM,

                                  si->perm,si->uid, si->gid);

           if (s >= 0) {

               //在环境变量中添加socket信息。

                publish_socket(si->name, s);

           }

        }

       ......//设置uid,gid等

     setpgid(0, getpid());

       if(!dynamic_args) {

        /*

执行/system/bin/app_process,这样就进入到app_process的main函数中了。

fork、execve这两个函数都是Linux系统上常用的系统调用。

        */

            if (execve(svc->args[0], (char**)svc->args, (char**) ENV) < 0) {

              ......

           }

        }else {

          ......

    }

 

   ......//父进程init的处理,设置service的信息,如启动时间、进程号,以及状态等。

   svc->time_started = gettime();

   svc->pid = pid;

   svc->flags |= SVC_RUNNING;

//每一个service都有一个属性,zygote的属性为init.svc.zygote,现在设置它的值为running

   notify_service_state(svc->name, "running");

}

原来,zygote是通过fork和execv共同创建的!但service结构中的那个onrestart好像没有派上用场,原因何在?

2. 重启zygote

根据名字,就可猜到onrestart应该是在zygote重启时用的。下面先看在zygote死后,它的父进程init会有什么动作:

[-->init.c]

static void sigchld_handler(int s)

{  //当子进程退出时,init的这个信号处理函数会被调用

   write(signal_fd, &s, 1); //往signal_fd write数据

}

signal_fd,就是在init中通过socketpair创建的两个socket中的一个,既然会往这个signal_fd中发送数据,那么另外一个socket就一定能接收到,这样就会导致init从poll函数中返回:

[-->init.rc::main函数代码片断]

 nr =poll(ufds, fd_count, timeout);

 ......

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

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

       while (!wait_for_one_process(0))//调用wait_for_one_process函数处理

         ;

       continue;

 }

 ......

//直接看这个wait_for_one_process函数:

static int wait_for_one_process(int block)

{

    pid_tpid;

    intstatus;

    structservice *svc;

    structsocketinfo *si;

    time_tnow;

    structlistnode *node;

    structcommand *cmd;

 

while( (pid = waitpid(-1, &status, block ? 0 : WNOHANG)) == -1 &&

errno == EINTR );

    if(pid <= 0) return -1;

    //找到死掉的那个service,现在应该找到了代表zygote的那个service。

svc = service_find_by_pid(pid);

   ......

 

if(!(svc->flags & SVC_ONESHOT)) {

    //杀掉zygote创建的所有子进程,这就是zygote死后,Java世界崩溃的原因。

       kill(-pid, SIGKILL);

   }

 

    //清理socket信息,不清楚的读者可以通过命令man 7 AF_UNIX查询一下相关知识。

    for(si = svc->sockets; si; si = si->next) {

       char tmp[128];

       snprintf(tmp, sizeof(tmp), ANDROID_SOCKET_DIR"/%s",si->name);

        unlink(tmp);

    }

 

   svc->pid = 0;

   svc->flags &= (~SVC_RUNNING);

 

   if(svc->flags & SVC_ONESHOT) {

       svc->flags |= SVC_DISABLED;

    }

   ......

now= gettime();

/*

如果设置了SVC_CRITICAL标示,则4分钟内该服务重启次数不能超过4次,否则

机器会重启进入recovery模式。根据init.rc的配置,只有servicemanager进程

享有此种待遇。

*/

    if(svc->flags & SVC_CRITICAL) {

        if(svc->time_crashed + CRITICAL_CRASH_WINDOW >= now) {

           if (++svc->nr_crashed > CRITICAL_CRASH_THRESHOLD) {

              ......

               sync();

               __reboot(LINUX_REBOOT_MAGIC1,LINUX_REBOOT_MAGIC2,

                        LINUX_REBOOT_CMD_RESTART2, "recovery");

               return 0;

           }

        }else {

           svc->time_crashed = now;

           svc->nr_crashed = 1;

        }

    }

 

   svc->flags |= SVC_RESTARTING;

//设置标示为SVC_RESTARTING,然后执行该service onrestart中的COMMAND,这些内容就

//非常简单了,读者可以自行学习。

   list_for_each(node, &svc->onrestart.commands) {

       cmd = node_to_item(node, struct command, clist);

       cmd->func(cmd->nargs, cmd->args);

}

//设置init.svc.zygote的值为restarting。

   notify_service_state(svc->name, "restarting");

    return0;

}

通过上面的代码,可知道onrestart的作用了,但zygote本身又在哪里重启的呢?答案就在下面的代码中:

[-->init.c::main函数代码片断]

for(;;) {

       int nr, i, timeout = -1;

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

           ufds[i].revents = 0;

       drain_action_queue(); //poll函数返回后,会进入下一轮的循环

       restart_processes(); //这里会重启所有flag标志为SVC_RESTARTING的service。

       ......

}

这样,zygote又回来了!

3.2.4  属性服务

我们知道,Windows平台上有一个叫注册表的东西。注册表可以存储一些类似key/value的键值对。一般而言,系统或某些应用程序会把自己的一些属性存储在注册表中,即使下次系统重启或应用程序重启,它还能够根据之前在注册表中设置的属性,进行相应的初始化工作。Android平台也提供了一个类型机制,可称之为属性服务(property service)。应用程序可通过这个属性机制,查询或设置属性。读者可以用adb shell登录到真机或模拟器上,然后用getprop命令查看当前系统中有哪些属性。即如我的HTC G7测试结果,如图3-2所示:(图中只显示了部分属性)


图3-2  HTC G7属性示意图

这个属性服务是怎么实现的呢?下面来看代码,其中与init.c和属性服务有关的代码有下面两行:

property_init();

property_set_fd = start_property_service();

分别来看看它们。

1. 属性服务初始化

(1)创建存储空间

先看property_init函数,代码如下所示:

[-->property_service.c]

void property_init(void)

{

init_property_area();//初始化属性存储区域

//加载default.prop文件

   load_properties_from_file(PROP_PATH_RAMDISK_DEFAULT);

}

在properyty_init函数中,先调用init_property_area函数,创建一块用于存储属性的存储区域,然后加载default.prop文件中的内容。再看init_property_area是如何工作的,它的代码如下所示:

[-->property_service.c]

static int init_property_area(void)

{

   prop_area *pa;

 

   if(pa_info_array)

       return -1;

/*

初始化存储空间,PA_SIZE是这块存储空间的总大小,为32768字节,pa_workspace

为workspace类型的结构体,下面是它的定义:

typedef struct {

    void *data;   //存储空间的起始地址

    size_tsize;  //存储空间的大小

    int fd;   //共享内存的文件描述符

} workspace;

init_workspace函数调用Android系统提供的ashmem_create_region函数创建一块

共享内存。关于共享内存的知识我们在第7章会接触,这里,只需把它当做一块普通的内存就

可以了。

    */

   if(init_workspace(&pa_workspace, PA_SIZE))

       return -1;

 

   fcntl(pa_workspace.fd, F_SETFD, FD_CLOEXEC);

  

//在32768个字节的存储空间中,有PA_INFO_START(1024)个字节用来存储头部信息

   pa_info_array = (void*) (((char*) pa_workspace.data) + PA_INFO_START);

 

    pa =pa_workspace.data;

   memset(pa, 0, PA_SIZE);

   pa->magic = PROP_AREA_MAGIC;

   pa->version = PROP_AREA_VERSION;

//__system_property_area__这个变量由bionic libc库输出,有什么用呢?

       __system_property_area__ = pa;

 

    return0;

}

上面的内容比较简单,不过最后的赋值语句可是大有来头。__system_property_area__是bionic libc库中输出的一个变量,为什么这里要给它赋值呢?

原来,虽然属性区域是由init进程创建,但Android系统希望其他进程也能读取这块内存里的东西。为做到这一点,它便做了以下两项工作:

·  把属性区域创建在共享内存上,而共享内存是可以跨进程的。这一点,已经在上面的代码中见到了,init_workspace函数内部将创建这个共享内存。

·  如何让其他进程知道这个共享内存呢?Android利用了gcc的constructor属性,这个属性指明了一个__libc_prenit函数,当bionic libc库被加载时,将自动调用这个__libc_prenit,这个函数内部就将完成共享内存到本地进程的映射工作。

(2)客户端进程获取存储空间

关于上面的内容,来看相关代码:

[-->libc_init_dynamic.c]

//constructor属性指示加载器加载该库后,首先调用__libc_prenit函数。这一点和Windows上

//动态库的DllMain函数类似

void __attribute__((constructor))__libc_prenit(void);

void __libc_prenit(void)

{

    ......

     __libc_init_common(elfdata); //调用这个函数

    ......

}

__libc_init_common函数为:

[-->libc_init_common.c]

void __libc_init_common(uintptr_t *elfdata)

{

   ......

   __system_properties_init();//初始化客户端的属性存储区域

}

[-->system_properties.c]

int __system_properties_init(void)

{

   prop_area *pa;

    int s,fd;

   unsigned sz;

    char*env;

 

.....

//还记得在启动zygote一节中提到的添加环境变量的地方吗?属性存储区域的相关信息

//就是在那儿添加的,这里需要取出来使用了。

    env =getenv("ANDROID_PROPERTY_WORKSPACE");

    //取出属性存储区域的文件描述符。关于共享内存的知识,第7章中将会进行介绍。

    fd =atoi(env);

    env =strchr(env, ',');

    if(!env) {

       return -1;

    }

    sz =atoi(env + 1);

//映射init创建的那块内存到本地进程空间,这样本地进程就可以使用这块共享内存了。

//注意,映射的时候指定了PROT_READ属性,所以客户端进程只能读属性,而不能设置属性。

    pa =mmap(0, sz, PROT_READ, MAP_SHARED, fd, 0);

   

    if(pa== MAP_FAILED) {

       return -1;

    }

 

   if((pa->magic != PROP_AREA_MAGIC) || (pa->version !=PROP_AREA_VERSION)) {

       munmap(pa, sz);

       return -1;

    }

 

   __system_property_area__ = pa;

    return0;

}

上面代码中很多地方和共享内存有关,在第7章中会对与共享内存有关问题进行介绍,读者也可先行学习有关共享内存的知识。

总之,通过这种方式,客户端进程可以直接读取属性空间,但没有权限设置属性。客户端进程又是如何设置属性呢?

2. 启动属性服务器

(1)启动属性服务器

init进程会启动一个属性服务器,而客户端只能通过和属性服务器交互才能设置属性。先来看属性服务器的内容,它由start_property_service函数启动,代码如下所示:

[-->Property_servie.c]

int start_property_service(void)

{

    intfd;

  

   /*

       加载属性文件,其实就是解析这些文件中的属性,然后把它设置到属性空间中去。Android系统

      一共提供了四个存储属性的文件,它们分别是:

     #definePROP_PATH_RAMDISK_DEFAULT "/default.prop"

#define PROP_PATH_SYSTEM_BUILD     "/system/build.prop"

#define PROP_PATH_SYSTEM_DEFAULT   "/system/default.prop"

#define PROP_PATH_LOCAL_OVERRIDE   "/data/local.prop"

*/

  

   load_properties_from_file(PROP_PATH_SYSTEM_BUILD);

   load_properties_from_file(PROP_PATH_SYSTEM_DEFAULT);

load_properties_from_file(PROP_PATH_LOCAL_OVERRIDE);

//有一些属性是需要保存到永久介质上的,这些属性文件则由下面这个函数加载,这些文件

//存储在/data/property目录下,并且这些文件的文件名必须以persist.开头。这个函数

//很简单,读者可自行研究。

    load_persistent_properties();

   //创建一个socket,用于IPC通信。

    fd =create_socket(PROP_SERVICE_NAME, SOCK_STREAM, 0666, 0, 0);

    if(fd< 0) return -1;

   fcntl(fd, F_SETFD, FD_CLOEXEC);

   fcntl(fd, F_SETFL, O_NONBLOCK);

   listen(fd, 8);

    returnfd;

}

属性服务创建了一个用来接收请求的socket,可这个请求在哪里被处理呢?事实上,在init中的for循环那里已经进行相关处理了。

(2)处理设置属性请求

接收请求的地方是在init进程中,代码如下所示:

[-->init.c::main函数片断]

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

           handle_property_set_fd(property_set_fd);

当属性服务器收到客户端请求时,init会调用handle_property_set_fd进行处理。这个函数的代码如下所示:

[-->property_service.c]

void handle_property_set_fd(int fd)

{

   prop_msg msg;

    int s;

    int r;

    intres;

    structucred cr;

    structsockaddr_un addr;

   socklen_t addr_size = sizeof(addr);

   socklen_t cr_size = sizeof(cr);

    //先接收TCP连接

    if ((s= accept(fd, (struct sockaddr *) &addr, &addr_size)) < 0) {

       return;

    }

 

    //取出客户端进程的权限等属性。

    if(getsockopt(s, SOL_SOCKET, SO_PEERCRED, &cr, &cr_size) < 0) {

        ......

       return;

    }

   //接收请求数据

    r = recv(s,&msg, sizeof(msg), 0);

   close(s);

    ......

 

   switch(msg.cmd) {

    casePROP_MSG_SETPROP:

       msg.name[PROP_NAME_MAX-1] = 0;

       msg.value[PROP_VALUE_MAX-1] = 0;

        /*

如果是ctl开头的消息,则认为是控制消息,控制消息用来执行一些命令,例如用

adb shell登录后,输入setprop ctl.start bootanim就可以查看开机动画了,

关闭的话就输入setpropctl.stop bootanim,是不是很有意思呢?

        */

       if(memcmp(msg.name,"ctl.",4) == 0) {

           if (check_control_perms(msg.value, cr.uid, cr.gid)) {

               handle_control_message((char*) msg.name + 4, (char*) msg.value);

           }

           ......

        }else {

           //检查客户端进程是否有足够的权限

           if (check_perms(msg.name, cr.uid, cr.gid)) {

               //然后调用property_set设置。

               property_set((char*) msg.name, (char*) msg.value);

           }

           ......

        }

       break;

 

   default:

       break;

    }

}

当客户端的权限满足要求时,init就调用property_set进行相关处理,这个函数比较简单,代码如下所示:

[-->property_service.c]

int property_set(const char *name, const char*value)

{

   prop_area *pa;

   prop_info *pi;

 

    intnamelen = strlen(name);

    intvaluelen = strlen(value);

    ......

    //从属性存储空间中寻找是否已经存在该属性

    pi =(prop_info*) __system_property_find(name);

 

if(pi!= 0) {

    //如果属性名以ro.开头,则表示是只读的,不能设置,所以直接返回。

       if(!strncmp(name, "ro.", 3)) return -1;

 

        pa= __system_property_area__;

        //更新该属性的值

       update_prop_info(pi, value, valuelen);

       pa->serial++;

       __futex_wake(&pa->serial, INT32_MAX);

}else {

   //如果没有找到对应的属性,则认为是增加属性,所以需要新创建一项。注意,Android支持

   //最多247项属性,如果目前属性的存储空间中已经有247项,则直接返回。

        pa= __system_property_area__;

       if(pa->count == PA_COUNT_MAX) return -1;

 

        pi= pa_info_array + pa->count;

       pi->serial = (valuelen << 24);

       memcpy(pi->name, name, namelen + 1);

        memcpy(pi->value, value, valuelen +1);

 

       pa->toc[pa->count] =

           (namelen << 24) | (((unsigned) pi) - ((unsigned) pa));

 

       pa->count++;

       pa->serial++;

       __futex_wake(&pa->serial, INT32_MAX);

    }

    //有一些特殊的属性需要特殊处理,这里,主要是以net.change开头的属性。

    if(strncmp("net.", name, strlen("net.")) == 0)  {

        if(strcmp("net.change", name) == 0) {

           return 0;

        }

       property_set("net.change", name);

    } elseif (persistent_properties_loaded &&

       strncmp("persist.", name,strlen("persist.")) == 0) {

       //如果属性名以persist.开头,则需要把这些值写到对应文件中去。

      write_persistent_property(name, value);

}

/*

还记得init.rc中的下面这句话吗?

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

         startadbd

当persist.service.adb.enable属性置为1后,就会执行start adbd这个command,

这是通过property_changed函数来完成的,它非常简单,读者可以自己阅读。

*/

   property_changed(name, value);

    return0;

}

好,属性服务端的工作已经了解了,下面看客户端是如何设置属性的。

(3)客户端发送请求

客户端通过property_set发送请求,property_set由libcutils库提供,代码如下所示:

[-->properties.c]

int property_set(const char *key, const char*value)

{

   prop_msg msg;

   unsigned resp;

 

   ......

   msg.cmd = PROP_MSG_SETPROP;//设置消息码为PROP_MSG_SETPROP。

   strcpy((char*) msg.name, key);

   strcpy((char*) msg.value, value);

    //发送请求

    returnsend_prop_msg(&msg);

}

 

static int send_prop_msg(prop_msg *msg)

{

    int s;

    int r;

    //建立和属性服务器的socket连接

    s =socket_local_client(PROP_SERVICE_NAME,

                           ANDROID_SOCKET_NAMESPACE_RESERVED,

                            SOCK_STREAM);

    if(s< 0) return -1;

    //通过socket发送出去

   while((r = send(s, msg, sizeof(prop_msg), 0)) < 0) {

       if((errno == EINTR) || (errno == EAGAIN)) continue;

       break;

    }

 

    if(r== sizeof(prop_msg)) {

        r= 0;

    } else{

        r= -1;

    }

 

   close(s);

    returnr;

}

至此,属性服务器就介绍完了。总体来说,还算比较简单。

3.3  本章小结

本章讲解了init进程如何解析zygote,以及属性服务器的工作原理,旨在帮助读者认识这个天字号第一进程。从整体来说,init.rc的解析难度相对最大。相信读者通过以上实例分析,已经理解了init.rc的解析原理。另外,inti涉及很多和Linux系统相关的知识,有兴趣的读者可以自行研究。

[深入理解Android卷一全文-第四章]深入理解zygote

由于《深入理解Android 卷一》和《深入理解Android卷二》不再出版,而知识的传播不应该因为纸质媒介的问题而中断,所以我将在CSDN博客中全文转发这两本书的全部内容。第4章  深入理解Zygo...
  • Innost
  • Innost
  • 2015-08-02 15:39:28
  • 8387

[深入理解Android卷一全文-第十章]深入理解MediaScanner

由于《深入理解Android 卷一》和《深入理解Android卷二》不再出版,而知识的传播不应该因为纸质媒介的问题而中断,所以我将在CSDN博客中全文转发这两本书的全部内容。第10章 深入理解Medi...
  • Innost
  • Innost
  • 2015-08-02 16:53:59
  • 5160

[深入理解Android卷一全文-第五章]深入理解常见类

由于《深入理解Android 卷一》和《深入理解Android卷二》不再出版,而知识的传播不应该因为纸质媒介的问题而中断,所以我将在CSDN博客中全文转发这两本书的全部内容。第5章 深入理解常见类本章...
  • Innost
  • Innost
  • 2015-08-02 15:52:04
  • 6057

[深入理解Android卷一 全文-第二章]深入理解JNI

由于《深入理解Android 卷一》和《深入理解Android卷二》不再出版,而知识的传播不应该因为纸质媒介的问题而中断,所以我将在CSDN博客中全文转发这两本书的全部内容。(出版社排版好的PDF版正...
  • Innost
  • Innost
  • 2015-08-02 09:39:48
  • 10710

[深入理解Android卷一全文-第一章]阅读前的准备工作

由于《深入理解Android 卷一》和《深入理解Android卷二》不再出版,而知识的传播不应该因为纸质媒介的问题而中断,所以我将在CSDN博客中全文转发这两本书的全部内容。(出版社排版好的PDF版正...
  • Innost
  • Innost
  • 2015-08-02 09:14:25
  • 10166

[深入理解Android卷一全文-第六章]深入理解Binder

由于《深入理解Android 卷一》和《深入理解Android卷二》不再出版,而知识的传播不应该因为纸质媒介的问题而中断,所以我将在CSDN博客中全文转发这两本书的全部内容。第6章 深入理解Binde...
  • Innost
  • Innost
  • 2015-08-02 16:00:28
  • 11534

深入理解ANDROID 卷3高清完整PDF版

  • 2016年12月07日 18:08
  • 264.34MB
  • 下载

《深入理解Android 卷III》推荐序

《深入理解Android 卷III》即将发布,作者是张大伟。此书填补了深入理解Android Framework卷中的一个主要空白,即Android Framework中和UI相关的部分。在一个特别讲...
  • Innost
  • Innost
  • 2015-08-05 10:24:13
  • 20044

深入理解Android系列书籍资源分享更新

由于115网盘限制礼包下载,我现在将深入理解Android系列书籍或其他资源转移到百度网盘上,供兄弟姐妹们下载分享。1 深入理解Android:Wi-Fi,NFC和GPS卷下载地址:http://pa...
  • Innost
  • Innost
  • 2015-01-31 16:53:46
  • 37425

深入理解Android(卷2)(带全目录完整版)邓凡平.pdf

  • 2013年11月07日 10:47
  • 48.52MB
  • 下载
收藏助手
不良信息举报
您举报文章:[深入理解Android卷一全文-第三章]深入理解init
举报原因:
原因补充:

(最多只允许输入30个字)