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。
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分析
[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;
};