init进程

Author Avatar
罗炜光 9月 25, 2016
  • 在其它设备中阅读本文章

本篇基于android2.2.3

init进程是Android启动后,由内核启动的第一份用户级进程。

内核启动过程

init进程是在顺序执行完start_kernel()函数,init_post()函数,run_init_process()函数后,最后启动执行的

init/main.c

static noinline int init_post(void)
{
    if (execute_command) {
        run_init_process(execute_command);
    }
    run_init_process("/sbin/init");
    run_init_process("/etc/init");
    run_init_process("/bin/init");
    run_init_process("/bin/sh");
    ...
}

init_post()函数调用run_init_process()函数,获取注册在exxecute_command中的进程文件路径,执行execve()系统调用。execve()函数执行由参数传递过来的文件路径下的进程。

init执行

init/init.c

1.init进程注册信号处理器(即指定信号对应的处理函数)(2.3版本后没有这步)

    act.sa_handler = sigchld_handler;
    act.sa_flags = SA_NOCLDSTOP;
    act.sa_mask = 0;
    act.sa_restorer = NULL;
    sigaction(SIGCHLD, &act, 0);

2.对umask进行清零(umask设置了用户创建文件的默认权限)

umask命令允许你设定文件创建时的缺省模式,对应每一类用户(文件属主、同组用户、其他用户)存在一个相应的umask值中的数字。对于文件来说,这一数字的最大值分别是6。系统不允许你在创建一个文本文件时就赋予它执行权限,必须在创建后用chmod命令增加这一权限。目录则允许设置执行权限,这样针对目录来说,umask中各个数字最大可以到7。

    umask(0);

3.创建linux中根文件系统的目录,并挂载分区

    mkdir("/dev", 0755);
    mkdir("/proc", 0755);
    mkdir("/sys", 0755);

    mount("tmpfs", "/dev", "tmpfs", 0, "mode=0755");
    mkdir("/dev/pts", 0755);
    mkdir("/dev/socket", 0755);
    mount("devpts", "/dev/pts", "devpts", 0, NULL);
    mount("proc", "/proc", "proc", 0, NULL);
    mount("sysfs", "/sys", "sysfs", 0, NULL);

4.生成/dev/_null_节点文件,标准输入,标准输出,标准错误文件描述符重定向到_null_

    open_devnull_stdio();

5.生成/dev/_kmsg_节点文件记录log

    log_init();

6.解析init.rc文件(init.rc文件在Android系统运行过程中用于通用的环境设置及与进程相关的定义)

    parse_config_file("/init.rc");

7.初始化QEMU设备,设置模拟器环境(2.3版本后没有这步)

    qemu_init();

8.从”/proc/cmdline”中读取内核命令行参数,并在读取完后修改此文件的权限,禁止非授权用户操作此文件

    import_kernel_cmdline(0);

9.从”/proc/cpuinfo”中读取系统的CPU硬件信息。

    get_hardware_name();

10.根据读取的硬件信息来解析特定于硬件的配置信息

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

11.触发在init脚本文件中名字为early-init的action,并且执行其commands,其实是: on early-init

    action_for_each_trigger("early-init", action_add_queue_tail);
    drain_action_queue();

12.初始化动态设备管理,设备文件有变化时反应给内核

    device_fd = device_init();

13.加载启动动画,如果动画打开失败,则在屏幕上打印: A N D R O I D字样。

    if( load_565rle_image(INIT_IMAGE_FILE) ) {
    fd = open("/dev/tty0", O_WRONLY);
    if (fd >= 0) {
    const char *msg;
        msg = "\n"
    "\n"
    "\n"
    "\n"
    "\n"
    "\n"
    "\n"
    "\n"
    "\n"
    "\n"
    "\n"
    "\n"
    "\n"
    "\n"
    "             A N D R O I D ";
    write(fd, msg, strlen(msg));
    close(fd);
    }
    }

14.触发在init脚本文件中名字为init的action,并且执行其commands,其实是:on init

    action_for_each_trigger("init", action_add_queue_tail);
    drain_action_queue();

15.启动系统属性服务: system property service

property_set_fd = start_property_service();

16.创建socket用来处理进程信号

    if (socketpair(AF_UNIX, SOCK_STREAM, 0, s) == 0) {
        signal_fd = s[0];
        signal_recv_fd = s[1];
        fcntl(s[0], F_SETFD, FD_CLOEXEC);
        fcntl(s[0], F_SETFL, O_NONBLOCK);
        fcntl(s[1], F_SETFD, FD_CLOEXEC);
        fcntl(s[1], F_SETFL, O_NONBLOCK);
        }

17.触发在init脚本文件中名字为early-boot和boot的action,并且执行其commands

    action_for_each_trigger("early-boot", action_add_queue_tail);
    action_for_each_trigger("boot", action_add_queue_tail);
    drain_action_queue();

18.启动所有属性变化触发命令,其实是: on property:ro.xx.xx=xx

    queue_all_property_triggers();
    drain_action_queue();

19.事件处理循环

    for(;;) {
    int nr, i, timeout = -1;
    for (i = 0; i < fd_count; i++)
        ufds[i].revents = 0;
    drain_action_queue();
    restart_processes();
    if (process_needs_restart) {
        timeout = (process_needs_restart - gettime()) * 1000;
        if (timeout < 0)
            timeout = 0;
    }

分析及运行init.rc文件

分析init.rc文件

动作列表用于创建所需目录,以及为某些特定文件指定权限。

服务列表用于记录初始化程序需要启动的一些程序

Android init脚本语言的规范

Android初始化语言包含了四种类型的声明:

  • Actions(动作)
  • Commands(命令)
  • Services(服务)
  • Options(选项)

  • 初始化语言以行为单位,以空格间隔的语言符号组成。

  • C风格的反斜杠转义符可以用来在语言符号中插入空格。
  • 双引号也可以用来防止文本被空格分成多个语言符号。
  • 当反斜杠在行末时,作为折行符。
  • 以#开始的行为注释行
  • Actions和Service隐含声明一个新的段落,所有该段落下的Command与Option的声明皆属于该段落
  • Actions和Service的名称是唯一的。在它们之后声明相同命名的类将被视为无效
Actions(行为)

Actions其实就是一系列的Commands(命令)。Actions都有一个trigger(触发器),它被用于决定action的执行时间。当一个符合action触发条件的事件发生时,action会被加入到执行队列的末尾,除非它已经在队列里了。队列中的每一个action都被依次提取出,而这个action中的每个command(命令)都将被依次执行。
Actions的形式如下:

        on <trigger>
           <command1>
           <command2>
           <command3>

on后面跟着一个触发器,当trigger被触发时,command1,command2,command3,会依次执行,直到下一个Action或下一个Service。
简单来说,Actions就是Android在启动时定义的一个启动脚本,当条件满足时,会执行该脚本,脚本里都是一些命令commands,不同的脚本用on来区分。

Triggers(触发器)

Triggers(触发器)是一个用于匹配特定事件类型的字符串,用于使Actions发生。

  • boot
    当init程序执行,并载入/init.conf文件时触发.

  • <name>=<value>
    当改变属性值时触发

  • device-added-<path>
    当添加设备时触发.

  • device-removed-<path>
    当设备移除时触发.

  • service-exited-<name>
    当指定的服务退出时触发.

Services(服务)

Services(服务)是一个程序,它在初始化时启动,并在退出时可选择让其重启。Services(服务)的形式如下:

service <name> <pathname> [ <argument> ]*
           <option>
           <option>
           ...
  • name:服务名
  • pathname:当前服务对应的程序位置
  • option:当前服务设置的选项
Options(选项)

Options(选项)是一个Services(服务)的修饰。他们影响Services(服务)在何时,并以何种方式运行。

  • critical
    据设备相关的关键服务,如果在4分钟内,此服务重复启动了4次,那么设备将会重启进入还原模式。
  • disabled
    服务不会自动运行,必须按照名称明确指定后才可以启动
  • setenv <name> <value>
    设置环境变量
  • socket <name> <type> <perm> [ <user> [ <group> ] ]
    在/dev/socket/下创建一个unix domain的socket,并传递创建的文件描述符fd给服务进程.其中type必须为dgram或stream,seqpacket.用户名和组名默认为0
  • user <username>
    在执行此服务之前先切换用户名。当前默认为root.
  • group <groupname> [ <groupname> ]*
    类似于user,切换组名,默认组名为root
  • oneshot
    当此服务退出时不会自动重启.
  • class <name>
    给服务指定一个类属,这样方便操作多个服务同时启动或停止.默认情况下为default.
  • onrestart
    当服务重启时执行一条指令,
Commands(命令)
  • exec <path> [ <argument> ]*
    fork并执行指定路径下的程序,并传递参数.这将阻塞init进程直到程序执行完毕
  • export <name> <value>
    设置全局环境参数,此参数被设置后对所有进程都有效.
  • ifup <interface>
    使指定的网络接口”上线”,相当激活指定的网络接口
  • import <filename>
    导入一个额外的init配置文件.
  • hostname <name>
    设置主机名
  • chdir <directory>
    改变工作目录.
  • chmod <octal-mode> <path>
    改变指定文件的读取权限.
  • chown <owner> <group> <path>
    改变指定文件的拥有都和组名的属性.
  • chroot <directory>
    改变进行的根目录.
  • class_start <serviceclass>
    启动所有指定服务类下的未运行服务
  • class_stop <serviceclass>
    停止指定服务类下的所有已运行的服务
  • domainname <name>
    设置域名
  • insmod <path>
    安装指定路径的模块.
  • mkdir <path> [mode] [owner] [group]
    用指定参数创建一个目录,在默认情况下,创建的目录读取权限为755.用户名为root,组名为root.
  • mount <type> <device> <dir> [ <mountoption> ]* 类似于linux的mount(挂载)指令
  • setkey
    TBD(To Be Determined),待定.
  • setprop <name> <value>
    设置属性及对应的值.
  • setrlimit <resource> <cur> <max>
    设置资源的rlimit(资源限制)
  • start <service>
    如果指定的服务未启动,则启动它.
  • stop <service>
    如果指定的服务当前正在运行,则停止它.
  • symlink <target> <path>
    创建一个符号链接.
  • sysclktz <mins_west_of_gmt>
    设置系统基准时间.
  • trigger <event>
    触发一个事件。用于将一个action与另一个 action排列
  • write <path> <string> [ <string> ]*
    往指定的文件写字符串.
Properties(属性)

Init更新一些系统属性以提供对正在发生的事件的监控能力:

  • init.action
    当前正在执行的动作,如果没有则为空字符串””
  • init.command
    当前正在执行的命令.没有则为空字符串.
  • init.svc.<name>
    当前某个服务的状态,可为”stopped”, “running”, “restarting”

init.rc文件分析函数

init/main.c调用parse_config_file(const char *fn)函数来分析init.rc脚本文件

    int parse_config_file(const char *fn)
    {
        char *data;
        data = read_file(fn, 0);//读取文件
        if (!data) return -1;
        parse_config(fn, data);//分析读入的字符串
        DUMP();
        return 0;
    }

parse_config()函数会分析read_file()函数返回的字符串,并生成动作列表(Action List)与服务列表(Service List)

static void parse_config(const char *fn, char *s)
{
    ...
    for (;;) {
        switch (next_token(&state)) {//以行为单位分割参数传递过来的字符串
            ...
            case T_NEWLINE:
            if (nargs) {
                int kw = lookup_keyword(args[0]);//返回init.rc脚本中每行首个单词在keyword_list结构体数组中的数组编号
                if (kw_is(kw, SECTION)) {//判断是否为SECTION
                    state.parse_line(&state, 0, 0);
                    parse_new_section(&state, kw, nargs, args);
                } else {
                    state.parse_line(&state, nargs, args);
                }
            nargs = 0;
            }
            break;
            ...
        }
    }
}

KEYWORD宏转换为keyword_info结构体数组形成的列表,其形式为:【“列表编号”】={“关键字”,“组”,“参数个数”,“映射函数”}

parse_new_section()函数将kw_is()宏筛选出的命令,分别注册动动作列表或服务列表中。在parse_new_section()函数会将服务列表与动作列表分别保存到全局变量service_listaction_list

执行early-init动作列表

Android 中的ueventd是一个守护进程,主要作用是接收uevent来创建或删除/dev/xxx(设备节点)

执行init动作列表

在“on init”根文件系统挂载部分,主要挂载/system与/data两个目录。两个目录挂载完毕后,Android的根文件系统就准备好了

动作列表的运行

void drain_action_queue(void)
{
    struct listnode *node;
    struct command *cmd;
    struct action *act;
    int ret;

    while ((act = action_remove_queue_head())) {
        INFO("processing action %p (%s)\n", act, act->name);
        list_for_each(node, &act->commands) {
            cmd = node_to_item(node, struct command, clist);//从action_queue中取出动作列表,并转换成command结构体
            ret = cmd->func(cmd->nargs, cmd->args);
            INFO("command '%s' r=%d\n", cmd->args[0], ret);
        }
    }
}

action_remove_queue_head()函数用来获取全局链表action_queue的head,action_queue保存有由待执行的命令构成的动作列表

command结构体的func变量指定与动作列表中的命令相对应的函数,即各命令的映射函数

服务列表的运行

on boot段落中,最后一行命令为class_start,init进程通过该命令运行“service”段落中的所有程序。class_start命令对应的执行函数为do_class_start()

int do_class_start(int nargs, char **args)
{
    service_for_each_class(args[1], service_start_if_not_disabled);
    return 0;
}

do_class_start调用service_for_each_class

void service_for_each_class(const char *classname, void (*func)(struct service *svc))  
{  
    struct listnode *node;  
    struct service *svc;  
    list_for_each(node, &service_list) {  // 遍历service的结构体
        svc = node_to_item(node, struct service, slist); // 从slist里取出每一个结构体  
        if (!strcmp(svc->classname, classname)) {  // 如果名字是匹配的话,就会进入这个判断  
            func(svc);  // 执行service_start_if_not_disable, 并且将当前的service结构体给传递进去  
        }  
    }  
}

service_for_each_class()会遍历service_list链表,找到所有和classname匹配的service节点,如果这个节点没有被disabled的话,那么就启动其对应的服务

service_start_if_not_disabled

static void service_start_if_not_disabled(struct service *svc)
{
    if (!(svc->flags & SVC_DISABLED)) {
        service_start(svc, NULL);
    }
}

service_start

void service_start(struct service *svc, const char *dynamic_args)  
{  
    struct stat s;  
    pid_t pid;  
    int needs_console;  
    int n;  


    // 这个service即将被启动,将其从disable或reset的状态给移除掉,置其为重新运行的状态  
    svc->flags &= (~(SVC_DISABLED|SVC_RESTARTING));
    svc->time_started = 0;  


    // 如果这个service仍然是运行态的话,即return  
    if (svc->flags & SVC_RUNNING) {  
        return;  
    }  

    needs_console = (svc->flags & SVC_CONSOLE) ? 1 : 0;  
    if (needs_console && (!have_console)) {  
        ERROR("service '%s' requires console\n", svc->name);  
        svc->flags |= SVC_DISABLED;  
        return;  
    } // 如果这个service的flags是初始console,但是这个已经启动了的话,就会设置当前的flags为disabled  

    if (stat(svc->args[0], &s) != 0) {// 如果要执行的这个service的start的command不存在的话,返回error  
        ERROR("cannot find '%s', disabling '%s'\n", svc->args[0], svc->name);  
        svc->flags |= SVC_DISABLED;  
        return;  
    }  

    ...

    NOTICE("starting '%s'\n", svc->name);  

    // fork一个子进程,即所有从init.rc启动的service,都是一个子进程
    pid = fork();

    if (pid == 0) {  // pid = 0, 进入到子进程中  
        struct socketinfo *si;  
        struct svcenvinfo *ei;  
        char tmp[32];  
        int fd, sz;  


        // 得到属性存储空间的信息并加入到环境变量中  
        get_property_workspace(&fd, &sz);  
        sprintf(tmp, "%d,%d", dup(fd), sz);  
        add_environment("ANDROID_PROPERTY_WORKSPACE", tmp);  


        // 将service自己声明的envvars加入到环境变量中  
        for (ei = svc->envvars; ei; ei = ei->next)  
            add_environment(ei->name, ei->value);  

        // 根据socket info设置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) {  
                publish_socket(si->name, s);  
            }  
        }  

        if (svc->ioprio_class != IoSchedClass_NONE) {  
            if (android_set_ioprio(getpid(), svc->ioprio_class, svc->ioprio_pri)) {  
                ERROR("Failed to set pid %d ioprio = %d,%d: %s\n",  
                      getpid(), svc->ioprio_class, svc->ioprio_pri, strerror(errno));  
            }  
        }  

        if (needs_console) {  
            setsid();  
            open_console();  
        } else {  
            zap_stdio();  
        }  

#if 0  
        for (n = 0; svc->args[n]; n++) {  
            INFO("args[%d] = '%s'\n", n, svc->args[n]);  
        }  
        for (n = 0; ENV[n]; n++) {  
            INFO("env[%d] = '%s'\n", n, ENV[n]);  
        }  
#endif  

        setpgid(0, getpid());  

        if (svc->gid) { // 设置gid  
             setgid(svc->gid);
        }  
        if (svc->nr_supp_gids) {  
           setgroups(svc->nr_supp_gids, svc->supp_gids);
        }  
        if (svc->uid) {  // 设置uid  
             setuid(svc->uid);
        }  

        // 因为dynamic_args设置的为null,我们在第一次从init.rc启动的时候,一定会进入到这个判断。  
        if (!dynamic_args) { 
            //执行当前的service的启动的命令,也就是说从这边开始,我们就可以理解为已经从init进程中,去像kernel执行init一样,就去执行各个service所对应的启动函数了!  
            if (execve(svc->args[0], (char**) svc->args, (char**) ENV) < 0) { 
                ERROR("cannot execve('%s'): %s\n", svc->args[0], strerror(errno));  
            }  
        } else {  
            char *arg_ptrs[INIT_PARSER_MAXARGS+1];  
            int arg_idx = svc->nargs;  
            char *tmp = strdup(dynamic_args);  
            char *next = tmp;  
            char *bword;  

            ...
        }
    ...
}

生成设备驱动节点

init进程通过两种方式创建设备节点。第一种,以预先定义的设备信息为基础,当init进程被启动运行时,同一创建设备节点文件,这种连接已定义的设备的方法,称为“冷拔插”,第二种,在系统运行时,当有设备插入USB端口时,init经常就会接收到这一事件,为插入的设备动态创建设备节点文件,这种在系统运行的状态下连接设备,称为“热拔插

从内核2.6X开始引入udev(userspace device)实用程序。udev以守护进程的形式运行,当设备驱动被加载时,它会掌握主设备号,次设备号,以及设备类型,而后在“/dev”目录下自动创建设备节点文件

在系统运行中,若某个设备被插入,内核就会加载与该设备相关的驱动程序。而后驱动程序会调用启动函数probe(),将主设备号,次设备号,以及设备类型保存到“/sys”文件系统中。然后发出uevent,并传递给udev守护进程

uevent是内核向用户空间进程传递信息的信号系统,即在添加或删除设备时,内核使用uevent将设备信息传递给用户空间。uevent包含设备名称、类别、主设备号、次设备号、设备节点文件创建的目录等信息,并将这些信息传递给udev守护进程

系统内核启动后,udev进程运行在用户空间内,它无法处理内核启动过程中发生的uevent。虽然内核空间内的设备驱动程序可以正常运行,但由于未创建访问设备驱动所需的设备节点文件,将会出现应用程序无法使用相关设备的问题。

Linuxx系统中,在udec守护经常运行前,通过提供与加载的设备驱动程序冷拔插机制,来解决设备节点文件没被创建的问题。

当内核启动后,冷拔插机制启动udev守护进程,从/sys目录下读取事先注册好的设备信息,而后引发与各设备相对应的uevent,创建设备节点文件。Android也采用这种处理方式来创建设备节点文件,不同的是使用init进程来扮演udev守护进程的角色。

创建静态设备节点

内核启动完毕后,init进程启动,对于像Binder驱动程序这样无法创建设备节点文件的驱动,将采用冷拔插方式进行处理。init进程事先获知等待冷拔插处理的驱动程序,并事先定义好各驱动程序的设备节点文件。在Android源代码的devices.c文件中,列出了init进程创建的节点文件的目录。

static struct perms_ devperms[] = {
87    { "/dev/null",          0666,   AID_ROOT,       AID_ROOT,       0 },
88    { "/dev/zero",          0666,   AID_ROOT,       AID_ROOT,       0 },
89    { "/dev/full",          0666,   AID_ROOT,       AID_ROOT,       0 },
90    { "/dev/ptmx",          0666,   AID_ROOT,       AID_ROOT,       0 },
91    { "/dev/tty",           0666,   AID_ROOT,       AID_ROOT,       0 },

    ...

    }

在冷拔插处理时,init进程会引起devperms结构体,在/dev 目录下创建设备节点文件。devperms结构体中,分别列出了等待冷拔插处理的设备节点文件的名称、访问权限、用户ID、组ID。若想为用户定义的新设备创建设备节点文件,需要将相关驱动信息添加到devperms结构体中。

init经常进行冷拔插处理的步骤。
init进程调用device_init()函数

int device_init(void)
{
    ...
     fd = open_uevent_socket();
     ...
    t0 = get_usecs();
    coldboot(fd, "/sys/class");
    coldboot(fd, "/sys/block");
    coldboot(fd, "/sys/devices");
    t1 = get_usecs();
    log_event_print("coldboot %ld uS\n", ((long) (t1 - t0)));
    ...
}

deviice_init()函数先创建一个套接字,用来接收uevenr。再通过coldboot()函数调用do_coldboot()函数,对内核启动时注册到/sys,目录下的驱动程序,进行冷拔插处理。
do_coldboot

static void do_coldboot(int event_fd, DIR *d)
{
    ...
     fd = openat(dfd, "uevent", O_WRONLY);
    if(fd >= 0) {
        write(fd, "add\n", 4);
        close(fd);
        handle_device_fd(event_fd);
    }
    ...
}

do_coldboot()函数接收参数传递过来的目录路径,通过该路径查找到保存的uevent文件,向相关文件写入“add”信息,而后强制引起uevent。然后在handler_ddevice_fd()函数中接收相关的uevent,获取uevent的信息

handle_device_fd

void handle_device_fd(int fd)
{
    for(;;) {
    ...
    struct uevent uevent;
    parse_event(msg, &uevent);

    handle_device_event(&uevent);
    ...
    }
}

handle_device_fd()函数在收到uevent时,调用parse_event()函数,将uevent信息希尔uevent结构体。
uevent

struct uevent {
    const char *action;
    const char *path;
    const char *subsystem;
    const char *firmware;
    int major;
    int minor;
};

向uevent结构体写完信息后,调用handle_device_event()函数,创建节点文件。

handle_device_event

static void handle_device_event(struct uevent *uevent)
{
    ...
    if(!strncmp(uevent->subsystem, "block", 5)) {
    block = 1;
    base = "/dev/block/";
    mkdir(base, 0755);
    }
    ...
    if(!strcmp(uevent->action, "add")) {
        make_device(devpath, block, uevent->major, uevent->minor);
        return;
    }
    ...
}

handle_device_event()先检查uevent结构体的subsystem变量,而后在/dev目录下创建子目录。subsystem根据硬件用途的不同而表示不同的组。若硬件是存储设备,则subsystem是block,创建的目录为/dev/block。

在创建完所有下层目录后,调用make_device()函数,创建设备节点文件

make_device()

static void make_device(const char *path, int block, int major, int minor)
301{
    ...
    mode = get_device_perm(path, &uid, &gid) | (block ? S_IFBLK : S_IFCHR);
    dev = (major << 8) | minor;
    setegid(gid);
    mknod(path, mode, dev);
    chown(path, uid, -1);
    setegid(AID_ROOT);

}

make_device()函数从设备节点文件列表中获取用户ID、组ID信息。而后调用mknod函数,创建设备节点文件。

创建动态设备节点

init经常支持热拔插处理,在系统运行中为新的设备创建节点文件。热插拔由init进程的事件处理循环来完成
init.c

int main(int argc, char **argv)
{
     for(;;) {
     ...
     nr = poll(ufds, fd_count, timeout);
     ...
     if (ufds[0].revents == POLLIN)
        handle_device_fd(device_fd);
    ...
     }
}

init经常的事件处理循环循环调用poll()函数监听来自驱动程序的uevent,而后调用handle_device_fd()函数,创建设备节点文件。

处理子进程终止

若init启动的某个进程终止,则会对系统的运行产生影响。比如“服务管理器”,它是应用程序使用系统服务必须运行的进程。如果该进程出现意外终止,那么进程间的通讯、图像输出、音频输出等功能将无法使用。因此,在init启动的进程中,除了一小部分外,其他大部分进程出现意外终止时,init进程要重新启动它们。

当init的子进程意外终止时,会向父进程init进程传递SIGCHLD信号,init进程接收该信号,检查进程选项是否设置为oneshot,若设置oneshot,init进程将放弃重启进程,否则重启进程。

init进程中的事件处理循环,当其子进程终止时,init会接收传递过来的SIGCHLD信号,并调用与之相对应的处理函数sigchld_handler()

static void sigchld_handler(int s)
{
    write(signal_fd, &s, 1);
}

signal_fd记录信号标号,由套接字对创建,信号编号被传递至接收端套接字描述符signal_recv_fd中。由于接收信号编号的signal_recv_fd已被注册至POLL,wait_for_one_process()就会被调用执行

    ...
    for(;;) {
        ...
        //当发生SIGCHLD信号时,程序就会从事件监听状态中跳出,而后执行poll()函数
        nr = poll(ufds, fd_count, timeout);
        if(nr <= 0)
            continue;
        if (ufds[2].revents == POLLIN) {
            read(signal_recv_fd, tmp, sizeof(tmp));
            while (!wait_for_one_process(0));
            continue;
        }
        ...
    }
    ...

当ufds[2]被注册进数据,即触发数据输出事件时,signal_recv_fd即调用并执行wait_for_one_process()函数。wait_for_one_process()函数在产生SIGCHLD信号的进程的服务列表中,检查进程的设置选项。若选项非oneshot,则添加重启选项(SVC_RESTARTING).oneshot选项被定义在init.rc文件的service部分中,若进程带有oneshot选项。进程终止时不会被重启

wait_for_one_process

static int wait_for_one_process(int block)
{
    ...
    while ( (pid = waitpid(-1, &status, block ? 0 : WNOHANG)) == -1 && errno == EINTR );
    ...
    svc = service_find_by_pid(pid);
    ...
    if (!(svc->flags & SVC_ONESHOT)) {
        kill(-pid, SIGKILL);
        ...
    }

    //删除进程持有的所有socketDescriptor
    for (si = svc->sockets; si; si = si->next) {
        ...
        unlink(tmp);
    }

    //删除SVC_RUNNING
    svc->pid = 0;
    svc->flags &= (~SVC_RUNNING);

    //将已设置进程标记为SVC_DISABLED,并从wait_for_one_process()函数中跳出,相关进程将不被重新启动
    if (svc->flags & SVC_ONESHOT) {
        svc->flags |= SVC_DISABLED;
    }

    if (svc->flags & SVC_DISABLED) {
        notify_service_state(svc->name, "stopped");
        return 0;
    }

    ...
    //向当前服务项的标记中添加SVC_RESTART。
    svc->flags |= SVC_RESTARTING;

     //检查待重启的进程在init.rc文件中是否带有onrestart选项
    list_for_each(node, &svc->onrestart.commands) {
        cmd = node_to_item(node, struct command, clist);
        cmd->func(cmd->nargs, cmd->args);
    }
    ...
}
  • 当产生信号的进程被终止时,waitpid()函数用来回收进程所占用的资源,它带有三个参数。其中,第一个参数pid为欲等待的子进程的识别码,设置为-1,表示查看所有子进程是否发出SIGCHLD信号,第二个参数status,用于返回子进程的结束状态,第三个参数决定waitpid()函数是否应用阻塞处理方式。waitpid()函数返回pid值,返回值即是产生SIGCHLD信号的进程的pid号
  • service_find_by_pid()函数用来取出与服务列表中终止经常相关的服务项目
  • 在取出的服务项目选项中,检查SVC_ONESHOT是否已设置。SVC_ONESHOT表示经常仅运行一次,带有此选项的进程在运行一次后,不会被重新启动,由kill(-pid,SIGKILL)函数终止

当wait_for_one_process()函数 执行完毕后,事件处理循环中的restart_process()函数就会被调用执行。

static void restart_processes()
{
    process_needs_restart = 0;
    service_for_each_flags(SVC_RESTARTING,restart_service_if_needed);
}

restart_processes()函数运行服务列表中带有SVC_RESTART标记的进程。当一个带有此标记的进程被终止,产生SIGCHLD信号时,restart_processes()函数将重新启动它。

属性服务

属性变更请求是init事件处理循环处理的另一个事件。在Android平台中,为了让运行中的所有进程共享系统运行时所需要的各种设置值,系统开辟了属性存储区域,并提供了访问该区域的API.属性由键与值构造,其表现形式为“键=值”

在Android平台中,访问属性值时,添加了访问权限控制,增强了访问的安全性。

系统中所有运行的进程都可以访问属性值,但仅有init进程才能修改属性值。

在其他进程修改属性值时,必须向init进程提出请求,最终由init进程负责修改属性值。在此过程中,init进程会先检查各属性的访问权限,而后再修改属性值

当属性值更改后,若定义在init.rc文件中的某个特定条件得到满足,则与此条件相匹配的动作就会发生

init经常的main()函数中,调用property_init()函数,用来初始化属性域

int main(int argc, char **argv)
{
    ...
    property_init();
    ...
}

property_init

void property_init(void)
{
    init_property_area();
    load_properties_from_file(PROP_PATH_RAMDISK_DEFAULT);
}

property_init()函数首先在内存中开辟一块共享区域,而后将其作用在ashmen(Android Shared Menory)。外部进程可以访问这块共享内存域,获取属性值,但它们不能通过直接访问共享内存域的方法来更改属性值。一个进程若想更改属性值,必须先向init进程提交属性变更请求,由init进程更改共享内存中的属性值

init_property_area()函数被调用执行后,所创建的属性域被初始化。

属性域的起始1024个字节作为属性域头,用来保存管理属性表所需要的一些数值。其余31616个字节空间被划分成247块,每块大小为128字节,用来保存属性值。

访问属性值使用property_get()
修改属性值使用property_set()

在属性域完成初始化之后,就会从指定的文件中读取初始值,并设置为属性值
init的main()函数中调用了start_property_service()函数,用来创建启动属性服务所需要的Unix域套接字,并保存套接字描述符。

int main(int argc, char **argv)
{
    ...
    property_set_fd = start_property_service();
    ...
}

start_property_service

int start_property_service(void)
{
    int fd;
    //读取存储在各文件中的基本设置值,将它们设置为属性值
    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目录中的属性值
    load_persistent_properties();
    //创建名称为/dev/socket/property_service的Unix域套接字
    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);
    return fd;
}

/data/property目录中保存着系统运行中其他进程新生成的属性值或更改的属性值,属性的key被用作文件名,value被保存在文件中。

通过上面创建的Unix域套接字,接收到属性变更请求后,init进程就会调用handle_property_set_fd()函数

void handle_property_set_fd(int fd)
{
    ...
    struct ucred cr;
    ...
    //从套接字获取SO_PEERCRED值,以便检查传递信息的进程的访问权限
    if (getsockopt(s, SOL_SOCKET, SO_PEERCRED, &cr, &cr_size) < 0)     {
        close(s);
        ERROR("Unable to recieve socket options\n");
        return;
    }
    ...
    switch(msg.cmd) {
    case PROP_MSG_SETPROP:
        msg.name[PROP_NAME_MAX-1] = 0;
        msg.value[PROP_VALUE_MAX-1] = 0;
        if(memcmp(msg.name,"ctl.",4) == 0) {
            //检查访问权限,仅有system server、root以及相关进程才能使用ctl消息,终止或启动进程
            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((char*) msg.name, (char*) msg.value);
            } 
            ...
        }
        break;
    default:
        break;
    }
}

在struct ucred结构体,存储着传递次那个系的进程的uid、pid与gid值。通过此结构体中的值,以及消息的类型,检查进程的访问权限。

在属性消息中,以“ctl”开头的消息并非请求更改系统属性值的消息,而是请求进程启动与终止的消息。除此之外,其他消息都被用来更改系统的属性值,
check_perms()函数检查访问权限。各属性的访问权限采用Linux的uid进行区分,其定义如下
property_perms

struct {
    const char *prefix;
    unsigned int uid;
    unsigned int gid;
} property_perms[] = {
    { "net.rmnet0.",      AID_RADIO,    0 },
    { "net.gprs.",        AID_RADIO,    0 },
    { "net.ppp",          AID_RADIO,    0 },
    { "ril.",             AID_RADIO,    0 },
    { "gsm.",             AID_RADIO,    0 },
    { "persist.radio",    AID_RADIO,    0 },
    { "net.dns",          AID_RADIO,    0 },
    { "net.",             AID_SYSTEM,   0 },
    { "dev.",             AID_SYSTEM,   0 },
    { "runtime.",         AID_SYSTEM,   0 },
    { "hw.",              AID_SYSTEM,   0 },
    { "sys.",             AID_SYSTEM,   0 },
    { "service.",         AID_SYSTEM,   0 },
    { "wlan.",            AID_SYSTEM,   0 },
    { "dhcp.",            AID_SYSTEM,   0 },
    { "dhcp.",            AID_DHCP,     0 },
    { "vpn.",             AID_SYSTEM,   0 },
    { "vpn.",             AID_VPN,      0 },
    { "debug.",           AID_SHELL,    0 },
    { "log.",             AID_SHELL,    0 },
    { "service.adb.root", AID_SHELL,    0 },
    { "persist.sys.",     AID_SYSTEM,   0 },
    { "persist.service.", AID_SYSTEM,   0 },
    { "persist.security.", AID_SYSTEM,   0 },
    { NULL, 0, 0 }
};

若要在系统运行中biang属性设置,应充分考虑各属性的访问权限。
property_set()函数会接着调用property_changed()函数

int property_set(const char *name, const char *value)
{
    ...
     property_changed(name, value);
    ...
}

property_changed

void property_changed(const char *name, const char *value)
{
    if (property_triggers_enabled) {
        queue_property_triggers(name, value);
        drain_action_queue();
    }
}

在init.rc脚本文件中,记录着某个属性改变后要采取的动作,动作执行的条件以“on property:<key>=<value>”形式给出。当某个条件相关的键值被设定后,与该条件相关的触发其就会被触发。

参考资料

Amdroid框架揭秘
Android init进程启动
Android init进程启动
Android启动流程分析(十) action的执行和service的启动