init是android运行的第一个程序,在init中,android首先配置一些内核与用户空间交互所需的文件路径,接着开启属性服务,初始化一系列属性信息,解析配置文件,并根据解析的结果开启相关服务进程。init还会循环监听属性服务、多点触控、信号的请求,并作出响应处理。本文分析init流程及大体框架,并对相关知识做出分析。
init流程
分析init流程,就看init.c的main函数。精简后的框架代码如下:
int main(int argc, char **argv) { ... mkdir("/dev", 0755); mkdir("/proc", 0755); mkdir("/sys", 0755); ... open_devnull_stdio(); klog_init(); property_init(); ... init_parse_config_file("/init.rc"); ... action_for_each_trigger("early-init", action_add_queue_tail); queue_builtin_action(wait_for_coldboot_done_action, "wait_for_coldboot_done"); 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"); ... for(;;) { int nr, i, timeout = -1; execute_one_command(); restart_processes(); if (!property_set_fd_init && get_property_set_fd() > 0) { ufds[fd_count].fd = get_property_set_fd(); ufds[fd_count].events = POLLIN; ufds[fd_count].revents = 0; fd_count++; property_set_fd_init = 1; } if (!signal_fd_init && get_signal_fd() > 0) { ufds[fd_count].fd = get_signal_fd(); ufds[fd_count].events = POLLIN; ufds[fd_count].revents = 0; fd_count++; signal_fd_init = 1; } if (!keychord_fd_init && get_keychord_fd() > 0) { ufds[fd_count].fd = get_keychord_fd(); ufds[fd_count].events = POLLIN; ufds[fd_count].revents = 0; fd_count++; keychord_fd_init = 1; } if (process_needs_restart) { timeout = (process_needs_restart - gettime()) * 1000; if (timeout < 0) timeout = 0; } if (!action_queue_empty() || cur_action) timeout = 0; nr = poll(ufds, fd_count, timeout); if (nr <= 0) continue; for (i = 0; i < fd_count; i++) { if (ufds[i].revents & POLLIN) { if (ufds[i].fd == get_property_set_fd()) handle_property_set_fd(); else if (ufds[i].fd == get_keychord_fd()) handle_keychord(); else if (ufds[i].fd == get_signal_fd()) handle_signal(); } } } return 0; }
open_devnull_stdio函数用于把标准输出重定向,每个标准输出需要有一个文件作为承载,这里使用__null__作为临时承载。
void open_devnull_stdio(void) { int fd; static const char *name = "/dev/__null__"; if (mknod(name, S_IFCHR | 0600, (1 << 8) | 3) == 0) { fd = open(name, O_RDWR); unlink(name); if (fd >= 0) { dup2(fd, 0); dup2(fd, 1); dup2(fd, 2); if (fd > 2) { close(fd); } return; } } exit(1); }
这里有几个小知识点:
1.dup2会首先关闭dst文件描述符指向的文件,然后将dst文件描述符绑定到src所指向的文件上。
2.系统输出有3个,分别为0、1、2,代表stdin、stdout及stderr。
3.unlink类似于引用计数-1,当一个文件的打开引用计数为0时,调用close函数会删除该文件。
property_init函数创建了一块共享内存,用于属性的存取,所有进程均可以访问。
属性文件的解析
属性文件的解析是init的重要工作之一,在实际开发工作中,调整服务启动顺序、配置开机启动服务都需要对这部分内容有所了解,因此单独列出来分析。
static void parse_config(const char *fn, char *s) { struct parse_state state; struct listnode import_list; struct listnode *node; char *args[INIT_PARSER_MAXARGS]; int nargs; nargs = 0; state.filename = fn; state.line = 0; state.ptr = s; state.nexttoken = 0; state.parse_line = parse_line_no_op; list_init(&import_list); state.priv = &import_list; for (;;) { switch (next_token(&state)) { case T_EOF: state.parse_line(&state, 0, 0); goto parser_done; case T_NEWLINE: state.line++; if (nargs) { int kw = lookup_keyword(args[0]); if (kw_is(kw, SECTION)) { state.parse_line(&state, 0, 0); parse_new_section(&state, kw, nargs, args); } else { state.parse_line(&state, nargs, args); } nargs = 0; } break; case T_TEXT: if (nargs < INIT_PARSER_MAXARGS) { args[nargs++] = state.text; } break; } } parser_done: list_for_each(node, &import_list) { struct import *import = node_to_item(node, struct import, list); int ret; INFO("importing '%s'", import->filename); ret = init_parse_config_file(import->filename); if (ret) ERROR("could not import file '%s' from '%s'\n", import->filename, fn); } }
init_parse_config_file函数最终会调用parse_config做实际的解析。实际的解析工作是基于token的,可以理解为分词。首先将一行的分词保存为多个参数,然后当遇到换行符时,分析第一个参数是哪个关键词。parse_config会首先调用当前的parse_line函数做处理,然后若关键词属于SECTION,调用parse_new_section做解析。
parse_new_section也会根据不同的关键词做不同的处理,同时将parse_line设置对对应的处理函数。
static void parse_new_section(struct parse_state *state, int kw, int nargs, char **args) { printf("[ %s %s ]\n", args[0], nargs > 1 ? args[1] : ""); switch(kw) { case K_service: state->context = parse_service(state, nargs, args); if (state->context) { state->parse_line = parse_line_service; return; } break; case K_on: state->context = parse_action(state, nargs, args); if (state->context) { state->parse_line = parse_line_action; return; } break; case K_import: parse_import(state, nargs, args); break; } state->parse_line = parse_line_no_op; }
解析工作实际做的事情,就是将各个service及action放到对应的全局链表中。其中parse_service将service加到service_list中,parse_action将action加到action_list中。一共有三个全局链表:service_list、action_list、action_queue,service_list及action_list分别存储解析出来的全部service及action,而action_queue用于存储等待执行的action。
完成配置文件的解析后,init会调用action_for_each_trigger,将action_list中符合时间节点要求的action,加入到action_queue中等待执行。例如:action_for_each_trigger("early-init", action_add_queue_tail); 就是将early-init阶段要执行的action加入到action_queue中。
queue_builtin_action函数会直接创建action,并添加到action_list及action_queue中。
那么这些action什么时候被执行呢,答案就是,当init进入for死循环后,会调用execute_one_command函数。如果当前没有正在执行的命令,execute_one_command会找到action_queue头的action,然后找到这个action中应该执行的command,调用command的执行处理函数执行命令。
void execute_one_command(void) { int ret, i; char cmd_str[256] = ""; if (!cur_action || !cur_command || is_last_command(cur_action, cur_command)) { cur_action = action_remove_queue_head(); cur_command = NULL; if (!cur_action) return; INFO("processing action %p (%s)\n", cur_action, cur_action->name); cur_command = get_first_command(cur_action); } else { cur_command = get_next_command(cur_action, cur_command); } if (!cur_command) return; ret = cur_command->func(cur_command->nargs, cur_command->args); if (klog_get_level() >= KLOG_INFO_LEVEL) { for (i = 0; i < cur_command->nargs; i++) { strlcat(cmd_str, cur_command->args[i], sizeof(cmd_str)); if (i < cur_command->nargs - 1) { strlcat(cmd_str, " ", sizeof(cmd_str)); } } INFO("command '%s' action=%s status=%d (%s:%d)\n", cmd_str, cur_action ? cur_action->name : "", ret, cur_command->filename, cur_command->line); } }
pollfd
pollfd用于存放Socket文件描述符,其定义如下:
struct pollfd { int fd; /* 想查询的文件描述符. */ short int events; /* fd 上,我们感兴趣的事件*/ short int revents; /* Types of events that actually occurred. */ };
其中events为我们感兴趣的事件,revents为实际发生的事件。我们可以使用events注册我们想要监听的事件,在poll函数返回时,通过revents与不同时间&操作,得知实际发生的事件。具体用法可参考init的for循环。
listnode分析
Android init定义了一种数据结构listnode,用于双向链表[Tip:双向链表是环形的]。奇妙的是listnode中只有指向前后节点的指针,并没有保存数据,那么listnode如何使用呢?
秘密就在两个宏里
#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER) #define node_to_item(node, container, member) \ (container *) (((char*) (node)) - offsetof(container, member))
offsetof的作用比较好理解,是获得成员MEMBER在TYPE结构体中的位置偏移。那么node_to_item呢?
其实node_to_item的作用是将一个保存有listnode成员的结构体,根据listnode地址,找到结构体本身。char*的作用是将listnode地址位对齐,再减去listnode节点本身在结构体中的偏移量,即获得了结构体本身地址。这样用的好处是,一个结构体可以通过包含多个listnode成员,使数据同时包含在多个双向链表中。
使用实例如下:
struct action { /* node in list of all actions */ struct listnode alist; /* node in the queue of pending actions */ struct listnode qlist; /* node in list of actions for a trigger */ struct listnode tlist; unsigned hash; const char *name; struct listnode commands; struct command *current; };