在我们的android手机启动过程中,当linux kernel加载完毕,要开始启动android时,会执行init进程,确切的说它是Linux用户空间第一个进程,进程号为1。
下面将要通过代码分析它是如何工作,来完成启动整个android系统的任务。
其实我们可以将init看做是一个普通的应用程序,我们可以手机根目录下看到它的真身,一个名为init二进制文件。Linux第一个执行的程序,作为可执行程序,必然要有main函数来作为程序入口,下面就从这个入口函数开始分析。
代码路径:/system/core/init/init.c
int main(int argc, char **argv) { int fd_count = 0; struct pollfd ufds[4]; char *tmpdev; char* debuggable; char tmp[32]; int property_set_fd_init = 0; int signal_fd_init = 0; int keychord_fd_init = 0;
上面是简单c语言的声明,不做讨论。下面判断启动参数,如果是启动ueventd时也要执行init,并且要执行ueventd_main的流程,就不在这里具体分析了。
if (!strcmp(basename(argv[0]), "ueventd")) return ueventd_main(argc, argv); ... ... //略去部分代码
下面将要创建一些文件夹,并挂载设备,都是与Linux相关的,需要注意的是可以看到debugfs被挂载到了更目录/sys/kernel/debug目录下,这个应该是MTK特别加入的,用于系统调试,只有在非用户版本下才会存在。
/* 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. */ mkdir("/dev", 0755); mkdir("/proc", 0755); mkdir("/sys", 0755); 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); #ifdef INIT_ENG_BUILD mount("debugfs", "/sys/kernel/debug", "debugfs", 0, NULL); #endif
下面这句话打开然后有关闭了/dev/.booting设备文件,目的注释里写了,但可能看不明白。我的理解是这里希望通知那些需要加载firmware的设备开始执行加载任务。 紧接着去掉了重定向标准输入输出到/dev/_null_。android源码中这样操作了,但这里应该是MTK注释掉的,并且执行了对kernel log初始化的操纵,就是创建/dev/__kmsg__文件节点,并且设置FD_CLOEXEC标志。这样做的目的是为了保存一些用户空间的log到这个文件中,android log一般通过log demo保存,在这个守护进程启动前的log就先保存到这里。
/* indicate that booting is in progress to background fw loaders, etc */ close(open("/dev/.booting", O_WRONLY | O_CREAT, 0000)); /* 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();
以下为MTK加入的内容,手机进入factory mode ,meta mode等需要都是在这里进行判断,然后选择特定的配置文件启动系统。可以看到一些宏开关,我们会根据我们在编译版本时候的参数进行判断。而init进程主要的工作也就是对不同的配置文件(*.rc)进行解析与执行,具体的解析规则参见解析配置文件一节,这里就不具体分析了。
#ifdef HAVE_AEE_FEATURE INFO("reading AEE config file\n"); #ifndef PARTIAL_BUILD init_parse_config_file("/init.aee.mtk.rc"); #else init_parse_config_file("/init.aee.customer.rc"); #endif // MTK_INTERNAL #endif // HAVE_AEE_FEATURE INFO("reading config file\n"); #ifdef USE_BUILT_IN_FACTORY ERROR("USE_BUILT_IN_FACTORY"); if (is_factory_boot()) { ERROR("This is factory boot"); if (init_parse_config_file("/init.factory.rc") < 0){ init_parse_config_file("/init.rc"); INFO("reading project config file\n"); init_parse_config_file("/init.project.rc"); } } else { if(is_meta_boot()) { ERROR("Parsing meta_init.rc ...\n"); init_parse_config_file("/meta_init.rc"); INFO("reading project config file\n"); init_parse_config_file("/meta_init.project.rc"); } else if(is_advanced_meta_boot()) { ERROR("Parsing advanced_meta_init.rc ...\n"); init_parse_config_file("/advanced_meta_init.rc"); INFO("reading project config file\n"); init_parse_config_file("/advanced_meta_init.project.rc"); } else { printf("Parsing init.rc ...\n"); init_parse_config_file("/init.rc"); INFO("reading project config file\n"); init_parse_config_file("/init.project.rc"); } } ... ...//略去部分代码
以上在对开机模式判断过程中调用了is_factory_boot()和is_meta_boot(),原理就是读取文件/sys/class/BOOT/BOOT/boot/boot_mode的数值,每次会返回一个整数值,目前知道的对应关系
Meta Mode '1' Factory Mode '4' Advanced Meta '5' ATE Factory Mode '6'
如果值正常模式启动会解析init.rc这个配置文件,这也是我们经常会需要修改的一个配置文件。 如果需要订制特殊开机模式我们可以通过加入新的boot mode数值,并在这里加入判断,然后解析我自己定制的配置文件。
/* pull the kernel commandline and ramdisk properties file in */ import_kernel_cmdline(0, import_kernel_nv); /* don't expose the raw commandline to nonpriv processes */ chmod("/proc/cmdline", 0440);
这里需要注意的是import_kernel_cmdline()从/proc/cmdline读取了内核启动参数,启动参数一般保存着一些内核启动之前的系统信息以及一些系统配置信息,所以通过这种方式告知android一些必要信息,如下边要用到的插充电器开机的情况。个人感觉这个上面的启动模式判断功能与这个有些重复,估计这和mtk自己实现方式有关。
下面的部分就是启动特定系统共同执行的操作了,首先读取设备信息,为的是找到设备对应的配置文件。解析完成配置文件,会得到一些列的Action。启动android的配置文件有好几个,并且对启动过程进行了阶段划分:early-init、init、early-boot、boot是个阶段,我们可以在每个配置文件中的Action定义启动阶段,然后顺序执行不同阶段的Action。
get_hardware_name(hardware, &revision); snprintf(tmp, sizeof(tmp), "/init.%s.rc", hardware); init_parse_config_file(tmp);
下面先将early-init阶段的Action加入工作队列,使他们能最先执行,然后又加入些Action到Action list同时也加入工作队列中。
这些Action都不是在配置文件中定义的,而是系统特定的工作,这里重要关注的的是android property系统的启动,首先初始化property系统(property_init),然后再根据bootmode,调用 property_set()导入具体的属性值(set_init_properties),比如设备名,开机原因等等。
keychord init是与调试相关的,console_init主要是对控制台的初始化,并且如果根目录下存在initlog.rle文件则会调用load_565rle_imag()将图片在lcd上显示出来,否则会在屏幕上输出字符串,但是目前MTK并没有这么处理,我们也看不到输出的控制台的“android_”字符串。
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(property_init_action, "property_init"); queue_builtin_action(keychord_init_action, "keychord_init"); queue_builtin_action(console_init_action, "console_init"); queue_builtin_action(set_init_properties_action, "set_init_properties");
下面类似地将init阶段需要执行的Action加入工作队列,并且会判断开机原因,如果是插充电器开机会执行特殊操作。
/* execute all the boot actions to get us started */ action_for_each_trigger("init", action_add_queue_tail); /* skip mounting filesystems in charger mode */ if (strcmp(bootmode, "charger") != 0) { action_for_each_trigger("early-fs", action_add_queue_tail); action_for_each_trigger("fs", action_add_queue_tail); action_for_each_trigger("post-fs", action_add_queue_tail); action_for_each_trigger("post-fs-data", action_add_queue_tail); }
这里要启动property service,然后会在check_startup_action检查service是否正常启动,如果失败就使进程退出。启动属性系统同时会四个加载属性配置文件
"/default.prop" "/system/build.prop" "/system/default.prop" "/data/local.prop"
这些文件都是经常需要我们需要关注的文件。而且属性系统是通过socket方式与其他进程通讯的。接下来的工作是初化信号,这样是为进程间通讯做准备,然后会检查property service是否已经正常启动。
queue_builtin_action(property_service_init_action, "property_service_init"); queue_builtin_action(signal_init_action, "signal_init"); queue_builtin_action(check_startup_action, "check_startup");
这里顾名思义是将配置文件中配置property属性值的Action也加入到工作队列中去,类似的也划分的不同阶段. 然后,会继续加入early-boot、boot的Action。
queue_builtin_action(queue_early_property_triggers_action, "queue_early_propety_triggers"); if (!strcmp(bootmode, "charger")) { action_for_each_trigger("charger", action_add_queue_tail); } else { action_for_each_trigger("early-boot", action_add_queue_tail); action_for_each_trigger("boot", action_add_queue_tail); } /* run all property triggers based on current state of the properties */ queue_builtin_action(queue_property_triggers_action, "queue_propety_triggers");
这里加入了Boot chart,这里就不作详细介绍了,他是一个小工具,能对系统性呢进行分析,并生成系统启动过程图表,以提供一些有价值的信息,帮助提升系统启动速度。
#if BOOTCHART queue_builtin_action(bootchart_init_action, "bootchart_init"); #endif
以上配置文件里解析的Action都添加完毕,接下来需要开始真正的执行这些Action了。
在这个循环体里每次调用execute_one_command(),从工作队列头取出一个Action开始执行,并且执行一条就从队列里删除一条。这样就能把整过工作队列按顺序执行一边。
for(;;) { int nr, i, timeout = -1; execute_one_command();
下面这条语句是是用来重启那些死去的进程的。在配置文件中我们可以设置service或者应用程序是否需要被自动重启。原理就是轮询每个service的状态,发现退出的就再次启动。
restart_processes();
下面是init配置监听事件的操纵,包括3个方面:
get_property_set_fd()监听属性服务的事件 get_signal_fd()监听其他进程的事件; get_keychord_fd() 监听来自keychord的事件;
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; } ... ...//略去部分代码
下面开启监听操作
nr = poll(ufds, fd_count, timeout); if (nr <= 0) continue;
如果没用收到消息,重新执行循环。收到事件的话根据,每各种事件的特定的处理方式进行处理。比如来自property service事件会根据事件类型设置属性值
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; }
可以看出init任务还是很重的。不过可以将init的工作流程精简成以下四点:
- 解析配置文件
- 执行各阶段的Action
- 启动property系统,初始化资源,启动property service
- 最后进入循环等待,负责service重启,监听socket事件和property service事件,并进行处理。