Android 系统启动 - init 进程

前言

我们之前在 Android 系统启动流程简析 中提到:Android 系统内核启动之后,除了初始化各种软硬件环境,加载驱动程序,挂载根文件系统等操作之外,最重要的一个操作就是在系统中找到可执行程序 init ,启动 init 进程。

下面我们对 init 进程启动源码进行简析。但在简析之前,需要我们预先了解一些前置知识。

前置知识

File Path
init 进程 /system/core/init/init.cpp
init.rc 脚本 /system/core/rootdir/init.rc
readme.txt(Android Init Language) /system/core/init/readme.txt
  • init.rc 文件格式:init 进程在启动过程中,会启动许多关键的系统服务,如果在源码中一项项启动会显得很繁琐,而且不易扩展,因此 Android 引入了 init.rcinit.rc 是一个配置脚本,其内部使用一种名为 Android Init Language 的语言编写而成,详细内容可在 /system/core/init/readme.txt 中查看。

 这里我们就简单介绍下 init.rc 的一些主要配置语法:

 ☛ init.rc 内部主要包含五种类型语句:ActionServiceCommandOptionsImport。其中,主要的配置选项为:ActionService,Trigger 和 Command 是对 Action 的补充,Options 是对 Service 的补充。Import 用于导入其他 .rc 文件并进行解析。下面简单介绍下 ActionService 的配置语法:

Action:以 on 开头,trigger 是判断条件,command 是具体执行一些操作,当满足 trigger 条件时,执行这些 command

on <trigger> [&& <trigger>]* //设置触发器  
       <command> //触发器触发之后要完成的操作
       <command>
       <command>

Service:服务是 init 进程启动的程序、当服务退出时 init 进程会视情况重启服务。其语法以 service 开头,name 是指定这个服务的名称,pathname 表示这个服务的执行文件路径,argument 表示传递给执行文件的参数,option 表示这个服务的一些配置。

service <name> <pathname> [ <argument> ]*   
       <option>
       <option>
       ...

 ☛ .rc 文件是由一个个 Section 组成。action 加上 trigger 以及一些 command 组成一个Section;service 加上一些 option,也组成一个Section ;

:在 Android 7.0 以前,init 进程只解析根目录下的 init.rc 文件,但是随着版本的迭代,init.rc 越来越臃肿,所以在 7.0 以后,init.rc 一些业务被分拆到 /system/etc/init,/vendor/etc/init,/odm/etc/init 三个目录下。

init 进程启动流程 简析

init 进程的入口函数为:main,其位于:/system/core/init/init.cpp,具体内容如下:


int main(int argc, char** argv) {
    // 如果不是 ueventd,则进入
    if (!strcmp(basename(argv[0]), "ueventd")) {
        /* 该函数定义在在system/core/init/ueventd.cpp
         * init 进程会创建子进程 ueventd,uevented 主要负责设备节点文件的创建,
         * 创建方式有两种:冷插拔(Cold Plug) 和 热插拔(Hot Plug)
         */
        return ueventd_main(argc, argv);
    }

    if (!strcmp(basename(argv[0]), "watchdogd")) {
        /* 该函数定义在/system/core/init/watchdogd.cpp,主要用于启动“看门狗”
         *
         * “看门狗”本身是一个定时器电路,内部会不断的进行计时(或计数)操作,
         * 计算机系统和”看门狗”有两个引脚相连接,正常运行时每隔一段时间就会
         * 通过其中一个引脚向”看门狗”发送信号,”看门狗”接收到信号后会将计时器
         * 清零并重新开始计时,而一旦系统出现问题,进入死循环或任何阻塞状态,
         * 不能及时发送信号让”看门狗”的计时器清零,当计时结束时,
         * ”看门狗”就会通过另一个引脚向系统发送“复位信号”,让系统重启。 
         */
        return watchdogd_main(argc, argv);
    }

    // Clear the umask.
    umask(0);

    // 注册环境变量PATH
    // _PATH_DEFPATH 定义在 bionic/libc/include/paths.h 中
    add_environment("PATH", _PATH_DEFPATH);

    bool is_first_stage = (argc == 1) || (strcmp(argv[1], "--second-stage") != 0);

    // Get the basic filesystem setup we need put together in the initramdisk
    // on / and then we'll let the rc file figure out the rest.
    if (is_first_stage) {
        // 挂载 tmpfs 到 /dev 目录
        mount("tmpfs", "/dev", "tmpfs", MS_NOSUID, "mode=0755");
        // 创建文件夹 /dev/pts
        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);
    }

    // We must have some place other than / to create the device nodes for
    // kmsg and null, otherwise we won't be able to remount / read-only
    // later on. Now that tmpfs is mounted on /dev, we can actually talk
    // to the outside world.
    open_devnull_stdio();
    // 初始化内核log,位于节点/dev/kmsg
    klog_init();
    // 设置 log 级别
    klog_set_level(KLOG_NOTICE_LEVEL);

    NOTICE("init%s started!\n", is_first_stage ? "" : " second stage");

    if (!is_first_stage) {
        // Indicate that booting is in progress to background fw loaders, etc.
        close(open("/dev/.booting", O_WRONLY | O_CREAT | O_CLOEXEC, 0000));

        // 定义在 /system/core/init/property_service.cpp
        // 初始化属性服务
        property_init();

        // If arguments are passed both on the command line and in DT,
        // properties set in DT always have priority over the command-line ones.
        process_kernel_dt(); // 处理DT属性
        process_kernel_cmdline(); // 处理命令行属性

        // Propogate the kernel variables to internal variables
        // used by init as well as the current required properties.
        export_kernel_boot_props();// 处理其他的一些属性
    }

    // Set up SELinux, including loading the SELinux policy if we're in the kernel domain.
    // 初始化 selinux 安全策略
    selinux_initialize(is_first_stage);

    // If we're in the kernel domain, re-exec init to transition to the init domain now
    // that the SELinux policy has been loaded.
    if (is_first_stage) {
        if (restorecon("/init") == -1) {
            ERROR("restorecon failed: %s\n", strerror(errno));
            security_failure();
        }
        char* path = argv[0];
        char* args[] = { path, const_cast<char*>("--second-stage"), nullptr };
        // 调用 execv 重新执行 main 函数,进入 second-stage 阶段
        if (execv(path, args) == -1) {
            ERROR("execv(\"%s\") failed: %s\n", path, strerror(errno));
            security_failure();
        }
    }

    // These directories were necessarily created before initial policy load
    // and therefore need their security context restored to the proper value.
    // This must happen before /dev is populated by ueventd.
    INFO("Running restorecon...\n");
    restorecon("/dev");
    restorecon("/dev/socket");
    restorecon("/dev/__properties__");
    restorecon_recursive("/sys");

    epoll_fd = epoll_create1(EPOLL_CLOEXEC);
    if (epoll_fd == -1) {
        ERROR("epoll_create1 failed: %s\n", strerror(errno));
        exit(1);
    }

    signal_handler_init();

    // 加载default.prop文件
    property_load_boot_defaults();
    // 启动属性服务,开启一个socket监听系统属性的设置
    start_property_service();

    // 解析 init.rc
    init_parse_config_file("/init.rc");

    // 执行rc文件中触发器为 on early-init 的语句
    action_for_each_trigger("early-init", action_add_queue_tail);

    // Queue an action that waits for coldboot done so we know ueventd has set up all of /dev...
    // 等待冷插拔设备初始化完成
    queue_builtin_action(wait_for_coldboot_done_action, "wait_for_coldboot_done");
    // ... so that we can start queuing up actions that require stuff from /dev.
    queue_builtin_action(mix_hwrng_into_linux_rng_action, "mix_hwrng_into_linux_rng");
    // 设备组合件的初始化操作
    queue_builtin_action(keychord_init_action, "keychord_init");
    // 屏幕显示 Android 静态 Logo
    queue_builtin_action(console_init_action, "console_init");

    // Trigger all the boot actions to get us started.
    // 触发rc文件中触发器为on init的语句
    action_for_each_trigger("init", action_add_queue_tail);

    // Repeat mix_hwrng_into_linux_rng in case /dev/hw_random or /dev/random
    // wasn't ready immediately after wait_for_coldboot_done
    queue_builtin_action(mix_hwrng_into_linux_rng_action, "mix_hwrng_into_linux_rng");

    // Don't mount filesystems or start core system services in charger mode.
    char bootmode[PROP_VALUE_MAX];
    // 当处于充电模式时,将 charger 加入执行队列;否则将 late-init 加入队列
    if (property_get("ro.bootmode", bootmode) > 0 && strcmp(bootmode, "charger") == 0) {
        action_for_each_trigger("charger", action_add_queue_tail);
    } else {
        action_for_each_trigger("late-init", action_add_queue_tail);
    }

    // Run all property triggers based on current state of the properties.
    queue_builtin_action(queue_property_triggers_action, "queue_property_triggers");

    while (true) {
        if (!waiting_for_exec) {
            execute_one_command();
            restart_processes();
        }

        int timeout = -1;
        if (process_needs_restart) {
            timeout = (process_needs_restart - gettime()) * 1000;
            if (timeout < 0)
                timeout = 0;
        }

        if (!action_queue_empty() || cur_action) {
            timeout = 0;
        }

        bootchart_sample(&timeout);

        epoll_event ev;
        // 循环等待事件发生
        int nr = TEMP_FAILURE_RETRY(epoll_wait(epoll_fd, &ev, 1, timeout));
        if (nr == -1) {
            ERROR("epoll_wait failed: %s\n", strerror(errno));
        } else if (nr == 1) {
            ((void (*)()) ev.data.ptr)();
        }
    }

    return 0;
}

从源码中看到,init 进程分为两个阶段:first-stagesecond-stage
第一个阶段 first-stage 主要做的事为:挂载一些必要的文件系统:tmpfs,devpts,proc,sysfs等,然后初始化 selinux 安全策略,在 selinux 初始化完成后就立即重新执行main函数,进入第二阶段 second-stage,也就是将 init 进程从内核态转化为用户态执行。

first-stagesecond-stage 都会做一些同样的工作,比如设置环境变量,设置日志,设置不同阶段的 selinux 策略。对于 second-stage 来说,其主要做的就是:初始化和启动属性服务,解析 init.rc 文件,并按顺序先后执行触发器: on early-init -> init -> late-init。

属性服务 的作用是让 init 进程可以对其他程序进行权限控制。在 Android 中,对属性的设置都统一交由 init 进程管理,其他进程不能直接修改属性,而必须通过属性服务申请。其实属性服务的是一个跨进程通讯,使用的通讯机制是 socket。这里我们就不深入代码讲解了,感兴趣的请自行查看。

对于 init.rc 文件的具体解析过程,我们这里也不做讲解。但简单提一下,init_parse_config_file("/init.rc"); 该函数源码位于:/system/core/init/init_parser.cpp ,主要做的就是对 init.rc 的文件内容进行解析,将解析出的 services 和 actions 等添加到运行队列中,等待 trigger 触发器的触发运行。

我们重点讲下 Zygote 的启动点。

启动 Zygote 进程

我们知道,Zygote 的启动是在 init 进程解析 init.rc 后启动的,那我们先来看下 init.rc 文件的内容:

import /init.environ.rc
import /init.usb.rc
import /init.${ro.hardware}.rc
import /init.usb.configfs.rc
import /init.${ro.zygote}.rc
import /init.trace.rc

on early-init
    # Set init and its forked children's oom_adj.
    write /proc/1/oom_score_adj -1000
···

这里看到 Zygote 的启动脚本被 import 了进来:import /init.${ro.zygote}.rc, ${ro.zygote} 会被替换成 ro.zyogte 的属性值,这个是由不同的硬件厂商自己定制的, 有四个值,zygote32、zygote64、zygote32_64、zygote64_32 ,也就是说可能有四种 .rc 文件。

这里我们选用 64 位处理器的版本来分析:/system/core/rootdir/init.zygote64.rc

service zygote /system/bin/app_process64 -Xzygote /system/bin --zygote --start-system-server
    class main
    socket zygote stream 660 root system
    onrestart write /sys/android_power/request_state wake
    onrestart write /sys/power/state on
    onrestart restart media
    onrestart restart netd
    writepid /dev/cpuset/foreground/tasks

service 选项用于通知 init 进程启动 zygote 程序,zygote 的执行程序路径为:/system/bin/app_process64,init 进程启动 zygote 时,传递的参数为: -Xzygote /system/bin --zygote --start-system-server。

class 的作用是用于批量管理 service。因此这里class main表示 zygote 服务属于类main。后续启动 main 的时候,就会启动 zygote 服务。

前面我们说过,init 进程在解析完 init.rc 文件后,会依次执行触发器:on early-init -> init -> late-init。我们再来看下 init.rc 文件内容:

on late-init
···
on nonencrypted
    class_start main // zygote 属于类 main
    class_start late_start
···

也就是说在 late-init 触发时,就会启动类 main 管理的服务,也就是会启动 zygote。

class_start 对应的处理方法是 do_class_start,该方法定义在system\core\init\builtins.cpp 中:

int do_class_start(int nargs, char **args){
        /* Starting a class does not start services
         * which are explicitly disabled.  They must
         * be started individually.
         */
    service_for_each_class(args[1], service_start_if_not_disabled);
    return 0;
}

该方法内部又会调用 service_for_each_class
/system/core/init/init_parser.cpp

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) {
        svc = node_to_item(node, struct service, slist);
        if (!strcmp(svc->classname, classname)) {
            func(svc);
        }
    }
}

该方法遍历 service_list 链表,每找到一个 classnamemain 的 service,就调用 service_start_if_not_disabled 来进一步处理,service_start_if_not_disabled 方法源码如下:
/system/core/init/builtins.cpp

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

可以看到,最终是调用 service_start 来启动服务。service_start 源码如下:
/system/core/init/init.cpp

void service_start(struct service *svc, const char *dynamic_args)
{
    ···
    // 如果Service已经运行,则不启动
    if (svc->flags & SVC_RUNNING) {
        return;
    }
    ···
    struct stat s;
    // 判断Service要启动的执行文件是否存在
    if (stat(svc->args[0], &s) != 0) {
        ERROR("cannot find '%s', disabling '%s'\n", svc->args[0], svc->name);
        svc->flags |= SVC_DISABLED;
        return;
    }
    ···
    NOTICE("Starting service '%s'...\n", svc->name);

    // fork 函数创建子进程
    pid_t pid = fork();
    if (pid == 0) { // 运行在子进程当中
        struct socketinfo *si;
        struct svcenvinfo *ei;
        char tmp[32];
        int fd, sz;

        umask(077);
        ··· 
        if (!dynamic_args) {
            // 加载可执行文件
            if (execve(svc->args[0], (char**) svc->args, (char**) ENV) < 0) {
                ERROR("cannot execve('%s'): %s\n", svc->args[0], strerror(errno));
            }
        ···
        _exit(127);
    }
    ···
}

该函数会通过调用 fork 函数来创建子进程,并在子进程中调用 execve 来加载可执行文件,对于 zygote 服务来说,也就是会执行 /system/bin/app_process64。那接下来该可执行文件的 main 函数就会被调用:
framework/cmds/app_process/app_main.cpp

int main(int argc, char* const argv[])
{
    ···
    // Process command line arguments
    // ignore argv[0]
    argc--;
    argv++;

    // --zygote : Start in zygote mode
    // --start-system-server : Start the system server.
    // --application : Start in application (stand alone, non zygote) mode.
    // --nice-name : The nice name for this process.
    ··· 
    // Parse runtime arguments.  Stop at first unrecognized option.
    ···
    // 解析参数
    ++i;  // Skip unused "parent dir" argument.
    while (i < argc) {
        const char* arg = argv[i++];
        if (strcmp(arg, "--zygote") == 0) {
            zygote = true;
            niceName = ZYGOTE_NICE_NAME;
        } else if (strcmp(arg, "--start-system-server") == 0) {
            startSystemServer = true;
        } else if (strcmp(arg, "--application") == 0) {
            application = true;
        } else if (strncmp(arg, "--nice-name=", 12) == 0) {
            niceName.setTo(arg + 12);
        } else if (strncmp(arg, "--", 2) != 0) {
            className.setTo(arg);
            break;
        } else {
            --i;
            break;
        }
    }
    ···
    if (zygote) {
        runtime.start("com.android.internal.os.ZygoteInit", args, zygote);
    } else if (className) {
        runtime.start("com.android.internal.os.RuntimeInit", args, zygote);
    } else {
        fprintf(stderr, "Error: no class name or --zygote supplied.\n");
        app_usage();
        LOG_ALWAYS_FATAL("app_process: no class name or --zygote supplied.");
        return 10;
    }
}

该函数会先对参数进行解析,对照 init.zygote64.rc 传递的参数:-Xzygote /system/bin --zygote --start-system-server,可以解析得出zygote=true,startSystemServer =true。于是最终会通过 AppRuntime.start 方法启动 zygote。

到此,init 进程就成功启动了 zygote 进程。

总结

init 进程启动时,总共经历两个阶段:内核态用户态

内核态 阶段下,init 进程主要做挂载一些必要的设备节点(tmpfs,devpts,proc,sysfs····),初始化 selinux 安全策略等操作,然后切换到 用户态(通过传递参数 --second-stage,并调用 execv 进行重启切换);

用户态 阶段下,init 进程最主要的一个操作就是解析 init.rc 文件,并按照顺序先后执行触发器:on early-init -> init -> late-init。

init 进程每遇到一个服务(service)时,就会通过 fork 出一个子进程来启动该服务。

zygote 在 init.rc 中是作为一个服务(service)存在,当 init.rc 文件被解析完成后,init 进程最终会在执行触发器 late-init 的过程中启动 zygote 服务(zygote 服务进程就是 init 进程 fork 出来的一个子进程,在该进程最后,会通过 AppRuntime.start 真正启动 zygote)。

参考

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271