Android6.0系统启动流程分析一:init进程

到了Android6.0,Init进程使用c++来写了,不过没有关系,它和c写的init没有太大的区别。
Init进程的入口代码是:system\core\init\init.cpp
main函数:


int main(int argc, char** argv) {
    if (!strcmp(basename(argv[0]), "ueventd")) {
        return ueventd_main(argc, argv);
    }

    if (!strcmp(basename(argv[0]), "watchdogd")) {
        return watchdogd_main(argc, argv);
    }

    // Clear the umask.
    umask(0);

    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) {
        mount("tmpfs", "/dev", "tmpfs", MS_NOSUID, "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);
    }

    // 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();
    klog_init();
    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));

        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();
        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_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 };
        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();

    property_load_boot_defaults();
    start_property_service();

    init_parse_config_file("/init.rc");

    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");
    queue_builtin_action(console_init_action, "console_init");

    // Trigger all the boot actions to get us started.
    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];
    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;
}

1.这个函数是否往下执行取决于传入的参数,如果第0个参数的basename为ueventd,则执行ueventd_main(argc, argv);如果basename为watchdogd_main,则执行watchdogd_main(argc, argv);只有basename不为这二者时,才会继续往下执行。
2.如果argv[1]不为”–second-stage”或者只有一个参数的话,那么is_first_stage就为true,就会创建/dev/pts和”/dev/socket”两个设备文件节点,并挂载一个文件系统。可以看出来init进程分两个阶段,不同的阶段有不同的行为。具体的内涵鄙人还没搞明白。
3.启动属性服务。创建一个socket,并在之后的死循环中监听这个socket返回的文件描述符。
3.解析init.rc。这个过程也是我最感兴趣的,也是最重要的复杂的。
4.对各个阶段的action排序。
5.进入死循环。
6.第一次进入死循环后,action_queue里面有很多时间,因此需要不断调用execute_one_command来执行命令。此时,action_queue_empty为假,timeout 为0,init线程不会在epoll_wait方法中休眠,因为设置的timeout=0哦,这一点曾一度困扰了我。
7.所有的命令执行完后,init进程进入休眠,监听property_set_fd和signal_read_fd两个文件描述符,一点他们有事件过来,立刻被唤醒,进而做事件处理。

init.rc梳理

在我们分析init.rc的解析过程之前,我们还需要先对init.rc有个基本的认识。
先看一张我根据理解绘制的图:
这里写图片描述
从图来看,init.rc主要有section组成,section由on,import,section三个关键字标示。其中on标示的section叫做action。
import就不用说了,和c语言中的include功能有点类似。
service格式如下

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

action后面会跟一个触发器,然后另起一行开始放置命令(command),格式如下:

on <trigger>  
   <command>  
   <command>  
   <command>  

跟在service后面的是option,跟在action后面的是command.command都会对应一个处理函数,定义在keywords.h中:

...
    KEYWORD(loglevel,    COMMAND, 1, do_loglevel)
    KEYWORD(mkdir,       COMMAND, 1, do_mkdir)
    KEYWORD(mount_all,   COMMAND, 1, do_mount_all)
    KEYWORD(mount,       COMMAND, 3, do_mount)
    ...

命名也是很有规则的。比如mkdir,对应的函数就是do_mkdir。我们看看do_mkdir做了什么:

int do_mkdir(int nargs, char **args)
{
    mode_t mode = 0755;
    int ret;

    /* mkdir <path> [mode] [owner] [group] */

    if (nargs >= 3) {
        mode = strtoul(args[2], 0, 8);
    }

    ret = make_dir(args[1], mode);
    /* chmod in case the directory already exists */
    if (ret == -1 && errno == EEXIST) {
        ret = fchmodat(AT_FDCWD, args[1], mode, AT_SYMLINK_NOFOLLOW);
    }
    if (ret == -1) {
        return -errno;
    }

    if (nargs >= 4) {
        uid_t uid = decode_uid(args[3]);
        gid_t gid = -1;

        if (nargs == 5) {
            gid = decode_uid(args[4]);
        }

        if (lchown(args[1], uid, gid) == -1) {
            return -errno;
        }

        /* chown may have cleared S_ISUID and S_ISGID, chmod again */
        if (mode & (S_ISUID | S_ISGID)) {
            ret = fchmodat(AT_FDCWD, args[1], mode, AT_SYMLINK_NOFOLLOW);
            if (ret == -1) {
                return -errno;
            }
        }
    }

    return e4crypt_set_directory_policy(args[1]);
}

其实就是调用了make_dir并做了一些权限等方面的操作。所以,跟在action后面的命令并不能随随便便乱加,而是要确保这个命令被定义了,不然就会出错。

init.rc的解析过程(以import为例)

因为init.rc的第一行代码就是Import语句。万事开头难,只要我们理清了第一行的解析过程,后面行的解析分析起来就不怎么费劲了。所以下面我们主要看看init.rc中第一行的解析过程。
init.tc的解析函数为:init_parse_config_file

int init_parse_config_file(const char* path) {
    INFO("Parsing %s...\n", path);
    Timer t;
    std::string data;
    if (!read_file(path, &data)) {
        return -1;
    }

    data.push_back('\n'); // TODO: fix parse_config.
    parse_config(path, data);
    dump_parser_state();

    // MStar Android Patch Begin
    INFO("(Parsing %s took %.2fs.)\n", path, t.duration());
    // MStar Android Patch End
    return 0;
}

这个函数把/init.rc中的内容读出来,并让data这个string类型的变量指向它。
把读出来的data传递给parse_config函数做真正的解析工作。parse_config函数如下:

static void parse_config(const char *fn, const std::string& data)
{
    char *args[UEVENTD_PARSER_MAXARGS];

    int nargs = 0;
    parse_state state;
    state.filename = fn;
    state.line = 1;
    state.ptr = strdup(data.c_str());  // TODO: fix this code!
    state.nexttoken = 0;
    state.parse_line = parse_line_no_op;
    for (;;) {
        int token = next_token(&state);
        switch (token) {
        case T_EOF:
            parse_line(&state, args, nargs);
            return;
        case T_NEWLINE:
            if (nargs) {
                parse_line(&state, args, nargs);
                nargs = 0;
            }
            state.line++;
            break;
        case T_TEXT:
            if (nargs < UEVENTD_PARSER_MAXARGS) {
                args[nargs++] = state.text;
            }
            break;
        }
    }
}

我看到这个函数的时候,我想起了xml解析方法之一的pull解析,感觉挺像的。每次循环都会找到一个token,token就是一个特定的符号,然后根据这个toke做不同的处理。这里使用到了parse_state结构,启动以如下:

struct parse_state
{
    char *ptr;
    char *text;
    int line;
    int nexttoken;
    void *context;
    void (*parse_line)(struct parse_state *state, int nargs, char **args);
    const char *filename;
    void *priv;
};

这个就够中:ptr执行init.rc字符流的,text后面会用到,用来保存参数,line当然就是行数了,nexttoken保存下一个token,filename保存init.rc的文件描述符,filename当然是/init.rc了.parse_line是一个函数指针。context暂时没明白…state.priv 指向Import的一个文件链表。
我们打开Init.rc看看,从头分析它的解析过程。

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

init.rc前面几行都是import语句,我们看看一开始的解析流程。
这个时候,parse_satate的状态为:

    state.filename = fn;
    state.line = 1;
    state.ptr = strdup(data.c_str());  // TODO: fix this code!
    state.nexttoken = 0;
    state.parse_line = parse_line_no_op;
        list_init(&import_list);
    state.priv = &import_list;

step 1.第一次循环

然后进入死循环,第一次调用next_token函数:

int next_token(struct parse_state *state)
{
    char *x = state->ptr;
    char *s;

    if (state->nexttoken) {
        int t = state->nexttoken;
        state->nexttoken = 0;
        return t;
    }

    
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值