深入理解“init”

作者: 程序员丶星霖 | 来源:发表于2017-04-05 19:30 被阅读162次

深入理解“init”

一、概述

init是一个进程,是Linux系统中用户空间的第一个进程。由于Android是基于Linux内核的,所以init也是Android系统中用户空间的第一个进程,它的进程号是1。

init有很多重要的工作职责,如下:

  • init进程负责创建系统中的几个关键进程,如zygote,它是Java世界的开创者。
  • Android系统有很多属性,于是init就提供了一个property service(属性服务)来管理它们。

二、nit分析

init的工作流程精简为一下四点:

  • 解析两个配置文件;
  • 执行各个阶段的动作,创建zygote的工作就是在其中的某个阶段完成的。
  • 调用property_init初始化属性相关的资源,并且通过property_start_service启动属性服务。
  • init进入一个无限循环,并且等待一些事情的发生。重点关注init如何处理来自socket和来自属性服务器的相关事情。

1.解析配置文件

在init中会解析两个配置文件,一个是系统配置文件init.rc,另一个是与硬件平台相关的配置文件。对这两个配置文件进行解析,调用的是同一个parse_config_file函数。如下例子所示:

int parse_config_file(const char *fn)
{
    char *data;
    data = read_file(fn,0);//读取配置文件的内容
    if(!data) return -1;
    parse_config(fn,data);//调用parse_config做真正的解析。
    return 0;
}

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

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;
            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首先会找到配置文件的一个section,然后针对不同的section使用不同的解析函数来解析。
1、关键字定义
keywords.h文件定义了init中使用的关键字。
keywords.h主要完成了两件事:

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

2、解析init.rc

  • 一个section的内容从这个标识section的关键字开始,到下一个标识section的地方结束。
  • init.rc中出现了名为boot和init的section。也就是说,在boot阶段执行的动作都是由boot这个section定义的。

2.解析service

解析service时,用到了parse_service和parse_line_service这两个函数。
1、service结构体
init中使用了一个叫service的结构体来保存于service section相关的信息。代码如下:

struct service{
    //listnode是一个特殊的结构体,在内核代码中用得非常多,主要用来将结构体链接成一个双向链表。
    //init中有一个全局的service_list,专门用来保存解析配置文件后得到的service。
    struct listnode slist;
    const char *name;//service的名字
    const char *classname;//service所属class的名字,默认是"defult"。
    unsigned flags;service的属性
    pid_t pid;//进程号
    time_t time_started;//上一次启动的时间
    time_t time_crashed;//上一次死亡的时间
    int nr_crashed;//死亡次数
    uid_t uid;
    gid_t gid;
    gid_t supp_gids[NR_SVC_SUPP_GIDS];
    size_t nr_supp_gids;
    /*
    有些service需要使用socket,下面这个socketinfo用来描述socket的相关信息。
    zygote也使用了socket,配置文件中的内容是socket zygote stream 666。
    它表示创建一个AF_STREAM类型的socket,这个socket的名字为"zygote",读写权限是666。
     */
    struct socketinfo *sockets;
    //service一般运行在一个单独的进程中,envvars用来描述创建这个进程时所需的环境变量信息。
    struct svcenvinfo *envvars;
    /*
    虽然关键字onrestart标识一个OPTION,可是这个OPTION后面跟着COMMAND,
    下面的action结构体可用来存储command信息。
     */
    struct action onrestart;

    //与keychord相关的内容
    int *keycodes;
    int nkeycodes;
    int keychord_id;
    //io优先级设置
    int ioprio_class;
    int ioprio_pri;
    //参数个数
    int nargs;
    //用于存储参数
    char *args[1];
};

service中使用的action结构体的代码如下:

struct action{
    /*
    一个action结构体可存放在三个双向链表中,其中alist用于存储所有的action,
    qlist用于链接那些等待执行的action,tlist用于链接那些待某些条件满足后就需要执行的action。
     */
    struct listnode alist;
    struct listnode qlist;
    struct listnode tlist;

    unsigned hash;
    const char *name;

    struct listnode commands;
    struct command *current;
};

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

3、了解parse_line_service
parse_line_service将根据配置文件的内容填充service结构体。zygote解析后的结果如下图所示:

zygote解析.jpg

从上图可知:

  • service_list链表将解析后的service全部链接到了一起,并且是一个双向链表,前向节点用prev表示,后向节点用next表示。
  • socketinfo也是一个双向链表,因为zygote只有一个socket,所以画了一个虚框socket作为链表的示范。
  • onrestart通过commands指向一个commands链表,zygote有三个commands。

3.init控制service

1、启动zygote
class_start标识一个COMMAND,对应的处理函数为do_class_start,它位于boot section的范围内。当init进程执行到下面几句话时,do_class_start就会被执行了。

//将boot  section节的command加入到执行队列。
action_for_each_trigger("boot",action_add_queue_tail);
//执行队列里的命令,class是一个COMMAND,所以它对应的do_class_start会被执行。
drain_action_queue();

下面是do_class_start函数:

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_for_each_class(args[1],service_start_if_not_disabled);
    return 0;
}

zygote的这个service的classname的值就是"default",所以会针对这个service调用service_start_if_not_disabled函数。代码如下:

static void service_start_if_not_disabled(struct service *svc)
{
    if(!(svc->flags&SVC_DISABLED)){
        service_start(svc,NULL);//zygote可不设置SVC_DISABLED
    }
}

2、重启zygote
zygote死后,它的父进程init会有如下操作:

static void sigchld_handler(int s)
{
    //当子进程退出时,init的这个信号处理函数会被调用。
    write(signal_fd,&s,1);//往signal_fd中写数据。
}

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

zygote的重启将在下面的代码中进行:

for(;;){
    int nr,i,timeout=-1;
    for(i=0;i<fd_count;i++)
        ufds[i].revents = 0;
        drain_action_queue();//poll函数返回后,会进入下一轮的循环。
        restart_pocesses();//这里会重启所有flag标识为SVC_RESTARTING的service。
        ......
}

4.属性服务

应用程序可通过Android平台提供的属性服务机制,查询或设置属性。
init.c和属性服务相关的代码如下:

property_init();
property_set_fd = start_property_service();

1、属性服务初始化

  1. 创建存储空间:property_init函数代码如下所示:
void propert_init(void)
{
    init_property_area();//初始化属性存储区域。
    //加载default.prop文件
    load_properties_from_file(PROP_PATH_RAMDISK_DEFAULT);   
}

在property_init函数中,先调用init_property_area函数,创建一块用于存储属性的存储区域,然后加载default.prop文件中的内容。

虽然属性区域是由init进程创建的,但是Android系统希望其他进程也能读取这块内存里的东西。因此做了以下工作:

  • 把属性区域创建在共享内存上,而共享内存是可以跨进程的。
  • Android利用gcc的construstor属性指明了一个_libc_prenit函数,当bionic libc库被加载时,将自动调用这个_libc_prenit,这个函数内部就将完成共享内存到本地进程的映射工作。
  1. 客户端进程获取存储空间。

2、属性服务器的分析

  1. 启动属性服务器:init进程会启动一个属性服务器,而客户端只能通过与属性服务器交互来设置属性。
  2. 处理设置属性请求:接收请求的地方在init进程中,代码如下:
if(ufds[1].revents == POLLIN)
    handle_property_set_fd(property_set_fd);

当属性服务器收到客户端请求时,init会调用handle_property_set_fd进行处理。
当客户端的权限满足要求时,init就调用property_set进行相关处理。

  1. 客户端发送请求:客户端通过property_set发送请求,property_set由libcutils库提供。

好的,《深入理解Android》真的不错,差距还是很大的,努力!!!

我的微信公众号.jpg

相关文章

网友评论

    本文标题:深入理解“init”

    本文链接:https://www.haomeiwen.com/subject/otgtattx.html