Android-深入理解init
Posted 天津 唐秙
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android-深入理解init相关的知识,希望对你有一定的参考价值。
文章目录
1. init
1.1 init介绍
init是一个进程,它是Linux系统中用户空间的第一个进程,因为android是基于Linux内核的,所以init也是Android系统中用户空间的第一个进程,它的进程号是1,本节关注几个重要的职责:
1.init进程负责创建系统中的几个关键进程,尤其是zygote(后面会讲)
2.Android系统有很多属性,于是init就提供了一个property service(属性服务)来管理他们,那么这个属性服务是怎么工作的呢?
1.2 学习路线
1.init如何创建zygote
2.init的属性服务是如何工作
1.3 init分析
init进程的入口函数main,代码如下:
int main(int argc, char **argv)
int device_fd = -1;
int property_set_fd = -1;
int signal_recv_fd = -1;
int keychord_fd = -1;
int fd_count;
int s[2];
int fd;
struct sigaction act;
char tmp[PROP_VALUE_MAX];
struct pollfd ufds[4];
char *tmpdec;
char* debuggable;
//设置进程退出的信号处理函数,该函数为sigchld_handler
act.sa_handler = sigchld_handler;
act.sa_flags = SA_NOCLDSTOP;
act.sa_mask = 0;
act.sa_restorer = NULL;
sigaction(SIGCHLD, &act, 0);
//...
//创建一些文件夹,并挂载设备,这些是与Linux相关的
mkdir("/dec/socker", 0755);
mount("devpts", "/dev/pts", "devpts", 0, NULL);
mount("proc", "/proc", "proc", 0, NULL);
mount("sysfs", "/sys", "sysfs", 0, NULL);
//重定向标准输入/输出/错误输出到/dev/_null_
open_devnull_stdio();
//设置init的日志输出设备为/dev/_kmsg_,不过该文件打开后,会立即被unlink了
//这样,其他进程就无法打开这个文件读取日志信息了
log_init();
//解析init.rc配置文件
parse_config_file("/init.cr");
//.....
//下面这个函数通过读取/proc/cpuinfo得到机器的Hardware名
get_hardware_name();
snprintf(tmp, sizeof(tmp), "/init.%s.rc", hardware);
//解析这个和机器相关的配置文件 这里对应的文件为init.bravo.rc
parse_config_file(tmp);
/*
* 解析完上述两个配置文件后,会得到一系列的Action(动作),下面两句
* 代码将执行那些处于early-init阶段的Action. init将动作执行的时间划
* 分为四个阶段,所以就有了先后之分。那些动作属于哪个阶段由配置文件
* 决定,后面会介绍配置文件的相关知识。
*
* */
action_for_each_trigger("early-init", action_add_queue_tail);
drain_action_queue();
/* 创建利用Uevent与Linux内核交互的socket,关于Uevent的知识,第九章
* 中对Vold进行分析时再说
* /
device_fd = device_init();
//初始化和属性相关的资源
property_init();
//初始化/dev/keychord设备,这与调试有关
keychord_fd = open_keychord();
/*
* INIT_IMAGE_FILE定义为"/initlogo.rle", 下面这个函数将加载这个文
* 件所谓系统的开机画面,注意,他不是开机动画控制程序 bootanimation
* 加载的开机动画文件
* */
if(load_565rle_image(INIT_IMAGE_FILE))
/*
* 如果加载initlogo.rle文件失败(可能是没有这个文件),则会
* 打开/dev/ty0设备,并输出"ANDROID"的字样作为开机画面,在模
* 拟器上看到的开机画面就是它
* */
//.....
if(qemu[0])
import_kernel_cmdline(1);
//调用property_set函数设置属性项,一个属性项包括属性名和属性值
property_set("ro.bootloader", bootloader[0] ? bootloader : "unknown");
//执行位于init阶段的动作
action_for_each_trigger("init", action_add_queue_tail);
drain_action_queue();
//启动属性服务
property_set_fd = start_property_service();
/*
* 调用sockerpair函数创建两个已经connect好的socker,socketpair是Linux
* 的系统调用,man socketpair可以查到
* */
if(sockerpair(AF_UNIX, SOCK_STREAM, 0, s) == 0)
signal_fd = s[0];
signal_recv_fd = s[1];
//.....
//.......
//执行配置文件中early-boot和boot阶段的动作
action_for_each_trigger("early-boot", action_add_queue_tail);
action_for_each_trigger("boot", action_add_queue_tail);
drain_action_queue();
//......
//init 关注来自四个方面的事情
ufds[0].fd = device_fd;//device_fd用于监听来自内核的Uevent事件
ufds[0].events = POLLIN;
ufds[1].fd = property_set_fd;//property_set_fd用于监听来自属性服务器的事件
ufds[1].events = POLLIN;
//signal_recv_fd 由socketpair创建,它的事件来自另一个socket
ufds[2].fd = signal_recv_fd;
ufds[2].events = POLLIN;
fd_count = 3;
if(keychord_fd > 0)
//如果keychord设备初始化成功,则init也会关注来自这个设备的事件
ufds[3].fd = keychord_fd;
ufds[3].events = POLLIN;
fd_count++;
//......
#if BOOTCHART
//与Boot char相关
/*
* Boot chart是一个小工具,它能对系统的性能进行分析,并生成系统启动过程
* 的图表,以提供一些有价值的信息,而这些信息最大的用处就是帮助提升系统
* 的启动速度
* */
#endif
for(;;)
//从此init将进入一个无限循环
int nr, i, timeout = -1;
for(i = 0; i < fd_count; i++)
ufds[i].revents = 0;
//在循环中执行动作
drain_action_queue();
restart_processes();// 重启那些已经死去的进程
//......
#if BOOTCHART
//Boot Chart相关
#endif
//调用poll等待一些事情的发生
nr = poll(ufds, fd_count, timeout);
//ufds[2]保存的是signal_recv_fd,用于接受来自socket的消息
if(ufds[2].revents == POLLIN)
//有一个子进程去世,init要处理这个事情
read(signal_recv_fd, tmp, sizeof(tmp));
while(!wait_for_one_process(0))
;
continue;
if(ufds[0].revents == POLLIN)
handle_device_fd(device_fd);//处理Uevent事件
if(ufds[1].revents == POLLIN)
handle_property_set_fd(property_set_fd);//处理属性服务的事件
if(ufds[3].revents == POLLIN)
handle_keychord(keychord_fd);//处理keychord事件
return 0;
根据本章学习内容,可以把init的工作流程分为以下四点:
1.解析两个配置文件,我们将分析其中对init.rc文件的解析
2.执行各个阶段的动作,创建zygote的工作就是其中的某个阶段完成的
3.调用property_init初始化属性相关的资源,并且通过property_start_service启动属性服务
4.init进入一个无限循环,并且等待一些事情的发生,重点关注init如何处理来自socket和来自属性服务器的相关事情。
1.3.1 解析配置文件
在init中会解析两个配置文件,其中一个是系统配置文件init.rc,另一个是与硬件平台相关的配置文件,以HTC G7手机为例,这个配置文件为init.bravo.rc,其中bravo是硬件平台的名称,对于这两个配置文件进行解析,调用的是同一个parse_config_file函数。
int parse_config_file(const char* fn)
char* data;
data = read_file(fn, 0);//读取配置文件的内容,这个文件时init.rc
if(!data) return -1;
parse_config(fn, data);//调用parse_config做到真正的解析
return 0;
读取完文件的内容后,将调用parse_config进行解析,这个函数的代码如下:
上面就是parse_config函数,这个函数首先会找到配置文件的一个section,然后针对不同的section使用不同的解析函数去解析,section和init.rc文件的组织结构有关。
1.关键字定义
keyword.h的定义
keywords.h做了两件事:
1.第一次包含keywords.h时,声明了一些诸如do_class_start的函数,另外还定义了一个枚举,枚举值为K_class, K_mkdir等关键字
2.第二次包含keywords.h后,得到了一个keyword_info结构体数组,这个keyword_info结构体数组以前面定义的枚举值为索引,存储对应的关键字信息,这些信息包括关键字名称,处理函数,处理函数的参数个数,以及属性。
目前,关键字信息中最重要的就是symbol和flags了,什么样的关键字被认为是section呢,根据keywords.h的定义,当symbol为service的时候表示section:
KEYWORD (on, SECTION, 0, 0)
KEYWORD (service, SECTION, 0, 0)
2.解析init.rc
分析可知:
1.一个section的内容从这个标识section的关键字开始,到下一个标识section的地方结束
2.init.rc中出现了名为boot和init的section,这里的boot和init就是前面介绍的4个动作执行阶段中的boot和init,也就是说,在boot阶段执行的动作都是由boot这个section定义的。
1.3.2 解析service
解析service的入口函数是parse_new_section,代码如下:
其中,解析service时,用到了parse_service和parse_line_service这两个函数,先看init怎么组织service的
1.service结构体
init中使用了一个叫service的结构体来保存与service section相关的信息,代码如下:
service中使用的这个action结构体
parse_line_service 将根据配置文件的内容填充service结构体,那么,zygote解析完后会得到什么呢?
图中可知:
1.service_list链表将解析后的service全部链接到一起,并且是一个双向链表,前向节点用prev表示,后向节点用next表示
2.socketinfo也是一个双向链表,因为zygote只有一个socket,所以画了一个虚框socker作为链表的示范
3.onrestart通过commands指向了一个commands链表,zygote有三个commands
1.3.3 init控制service
1.启动zygote
init.rc中有这样一句话
#class_start是一个COMMAND,对应的函数为do_class_start,很重要,切记
class_start default
class_start标识一个COMMAND,对应的处理函数为do_class_start,它位于bootsection的范围内,当init进程执行到下面几句话时,do_class_start就会被执行了。
//将boot section节的command加入到执行队列
action_for_each_trigger("boot", action_add_queue_tail);
//执行队列里的命令,class是一个COMMAND,所以它对应的do_class_start会被执行。
drain_action_queue();
2.重启zygote
onrestart是在zygote重启时使用的,zygote死后,父进程init的动作:
static void sigchld_handler(int s)
//当子进程退出时,init的这个信号处理函数会被调用
write(signal_fd, &s, 1);//往signal_fd中写数据
signal_fd就是在init中通过socketpair创建的两个socket中的一个,既然会往这个signal_fd中发送数据,那么另一个socket就一定能接受到,这样就会导致init从poll函数中返回
1.3.4 属性服务
Windows平台上面有一个叫做注册表的东西,注册表可以存储一些类型key/value的键值对,一般而言,系统或某些引用程序会把自己的一些属性存储在注册表中,即使系统重启或应用程序重启,它还能够根据之前在注册表中设置的属性进行相应的初始化工作,Android平台也提供了一个类似的机制,称为属性服务,应用程序可以通过这个属性机制,查询或设置属性,读者可以用adb shell登录到真机或模拟器上,然后用getprop命令查看当前系统中有哪些属性。
其中与init.c和属性服务有关的代码有下面两行:
proprety_init();
property_set_fd = start_property_service();
1.属性服务初始化
创建存储空间,proprety_init函数代码如下:
void property_init(void)
init_property_area();//初始化属性存储区域
//加载default.prop文件
load_properties_from_file(PROP_PATH_RAMDISK_DEFAULT);
在properyty_init函数中,先调用init_property_area函数,创建一块用于存储属性的存储区域,然后加载default.prop文件中的内容,再看init_property_area是如何工作的,代码如下:
最后的赋值语句,_system_property_area_是bionic libc库中输出的一个变量,为啥要给他赋值?
因为虽然属性区域是由init进程创建的,但Android系统希望其他进程也能读取这块内存里的东西,为了做到这一点,他便做了以下两项工作:
1.把属性区域创建在共享内存上,而共享内存是可以跨进程的,这一点,已经在上面的代码中可以看见了,init_workspace函数内部将创建这个共享内存。
2.如何让别人知道这个共享内存呢?Android利用了gcc的constructor属性,这个属性指明了一个_libc_prenit函数,当bionic lib库被加载时,将自动调用这个_libc_prenit,这个函数内部就将完成共享内存到本地进程的映射工作。
客户端进程获取存储空间
上面代码的很多地方都和共享内存有关,后面讲,通过这种方式,客户端进程可以直接读取属性空间,但没有权限设置属性,客户端进程要如何设置属性呢?
2.属性服务器的分析
启动属性服务器
init进程会启动一个属性服务器,而客户端之只能通过与属性服务器交互来设置属性,先来看属性服务器的内容,它由start_property_service函数启动。
属性服务创建了一个用来接受请求的socket,可这个请求在哪里被处理呢,事实上,在init中的for循环处已经进行了相关处理了。
处理设置属性请求
接受请求的地方在init进程中,代码如下:
if (ufds[1].revents == POLLIN)
handle_property_set_fd(property_set_fd);
当属性服务器收到了客户端请求时,init会调用handle_property_set_fd进行处理,代码如下:
当客户端的权限满足要求时,init就调用property_set进行相关处理,代码如下:
客户端发送请求
客户端通过property_set发送请求,property_set由libcutils库提供的。
1.4 总结
介绍了init进程如何解析zygote,以及属性服务器的工作原理,init同时还涉及很多Linux系统相关的知识。
以上是关于Android-深入理解init的主要内容,如果未能解决你的问题,请参考以下文章