本文由 简悦 SimpRead 转码, 原文地址 https://blog.csdn.net/wzy_1988/article/details/44999103
概述
init是一个进程,确切的说,它是Linux系统中用户空间的第一个进程。由于Android是基于Linux内核的,所以init也是Android系统中用户空间的第一个进程。init的进程号是1。作为天字第一号进程,init有很多重要的工作:
- init提供property service(属性服务)来管理Android系统的属性。
- init负责创建系统中的关键进程,包括zygote。
以往的文章一上来就介绍init的源码,但是我这里先从这两个主要工作开始。搞清楚这两个主要工作是如何实现的,我们再回头来看init的源码。
这篇文章主要是介绍init进程的属性服务。
跟init属性服务相关的源码目录如下:
- system/core/init/
- bionic/libc/bionic/
- system/core/libcutils/
属性服务
在windows平台上有一个叫做注册表的东西,它可以存储一些类似key/value的键值对。一般而言,系统或者某些应用程序会把自己的一些属性存储在注册表中,即使系统重启或应用程序重启,它还能根据之前在注册表中设置的属性值,进行相应的初始化工作。
Android系统也提供了类似的机制,称之为属性服务(property service)。应用程序可以通过这个服务查询或者设置属性。我们可以通过如下命令,获取手机中属性键值对。
1 | adb shell getprop |
例如红米Note手机的属性值如下:
1 | [ro.product.device]: [lcsh92_wet_jb9] |
在system/core/init/init.c文件的main函数中,跟属性服务的相关代码如下:
1 | property_init(); |
接下来,我们分别看一下这两处代码的具体实现。
属性服务初始化
¶创建存储空间
首先,我们先来看一下property_init函数的源码(/system/core/init/property_service.c):
1 | void property_init(void) |
property_init函数中只是简单的调用了init_property_area方法,接下来我们看一下这个方法的具体实现:
1 | static int property_area_inited = 0; |
从init_property_area函数,我们可以看出,函数首先判断属性内存区域是否已经初始化过,如果已经初始化,则返回-1。如果没有初始化,我们接下来会发现有两个关键函数**__system_property_area_init和init_workspace**应该是跟内存区域初始化相关。那我们分别分析一下这两个函数具体实现。
¶__system_property_area_init
__system_property_area_init函数位于/bionic/libc/bionic/system_properties.c文件中,具体代码实现如下:
1 | struct prop_area { |
代码比较好理解,主要内容是利用mmap映射property_filename创建了一个共享内存区域,并将共享内存的首地址赋值给全局变量__system_property_area__。
Tips
主要是想说一下struct prop_area的结构体定义很精妙。特别是char data[];
的定义。这里char data[]可以理解成动态数组指针,但是它并没有被占用内存空间。所以计算pa->bytes_used的时候,并没有计算char data[]占用的空间。但是,char data[]又是动态数组,我理解mmap分配的内存,prop_area除了头部占用了部分空间(即sizeof(prop_area)),其他的都是char data去支配了。
按照我的理解,最终属性也应该分配到char data[]所指向的内存中去。为了验证我的想法,我会跟踪property_set方法的具体实现。
¶property_set
property_set是在/system/core/init/property_service.c文件中定义的,具体源码如下:
1 | int property_set(const char *name, const char *value) |
可以看到,在property_set方法中,当添加一个(name, value)键值对时,会先在当前的属性空间中查找该键值对是否已经存在。根据我们之前的分析,这里查找的属性空间应该是prop_area结构体所指向的data区域。接下来,让我们深入代码去验证我们的想法。
__system_property_find函数是在/bionic/libc/bionic/system_properties.c文件中定义的:
1 | const prop_info *__system_property_find(const char *name) |
上面的代码已经验证了我之前所说的想法:data指针指向的区域是属性内容的起始地址。
大家感兴趣可以自己阅读代码,看一下属性键值对是如何在属性空间中存储的。(我大体看了一下,貌似是很简单的二叉查找树的形式)。
Tips
关于mmap映射文件实现共享内存IPC通信机制,可以参考这篇文章:mmap实现IPC通信机制
¶init_workspace
接下来,我们来看一下init_workspace函数的源码(/system/core/init/property_service.c):
1 | typedef struct { |
¶客户端进程访问属性内存区域
虽然属性内存区域是init进程创建的,但是Android系统希望其他进程也能够读取这块内存区域里的内容。为了做到这一点,init进程在属性区域初始化过程中做了如下两项工作:
- 把属性内存区域创建在共享内存上,而共享内存是可以跨进程的。这一点,在上述代码中是通过mmap映射/dev/__properties__文件实现的。pa_workspace变量中的fd成员也保存了映射文件的句柄。
- 如何让其他进程知道这个共享内存句柄呢?Android先将文件映射句柄赋值给__system_property_area__变量,这个变量属于bionic_lic库中的输出的一个变量,然后利用了gcc的constructor属性,这个属性指明了一个__lib_prenit函数,当bionic_lic库被加载时,将自动调用__libc_prenit,这个函数内部完成共享内存到本地进程的映射工作。
只讲原理是不行的,我们直接来看一下__lib_prenit函数代码的相关实现:
1 | void __attribute__((constructor)) __libc_prenit(void); |
__libc_init_common函数为:
1 | void __libc_init_common(uintptr_t *elfdata) |
__system_properties_init函数有回到了我们熟悉的/bionic/libc/bionic/system_properties.c文件:
1 | static int get_fd_from_env(void) |
通过对源码的阅读,可以发现,客户端通过mmap映射,可以读取属性内存的内容,但是没有权限设置属性。那客户端是如何设置属性的呢?这就涉及到下面要将的属性服务器了。
属性服务器的分析
init进程会启动一个属性服务器,而客户端只能通过与属性服务器的交互来设置属性。
¶启动属性服务器
先来看一下属性服务器的内容,它由property_service_init_action函数启动,源码如下(/system/core/init/init.c&&property_service.c):
1 | static int property_service_init_action(int nargs, char **args) |
从上述代码可以看到,init进程除了会预写入指定文件(例如:system/build.prop)属性外,还会创建一个UNIX Domain Socket,用于接受客户端的请求,构建属性。那这个socket请求是再哪里被处理的呢?
答案是:在init中的for循环处已经进行了相关处理。
更多关于UNIX Domain Socket IPC的介绍,可以考虑这篇文章:UNIX Domain Socket IPC
¶服务端处理设置属性请求
接收属性设置请求的地方是在init进程中,相关代码如下所示:
1 | int main(int argc, char **argv) |
从上述代码可以看出,当属性服务器收到客户端请求时,init进程会调用handle_property_set_fd函数进行处理,函数位置是:system/core/init/property_service.c,我们来看一下这个函数的实现源码:
1 | void handle_property_set_fd() |
当客户端的权限满足要求时,init就调用property_set进行相关处理。property_set源码实现如下:
1 | int property_set(const char *name, const char *value) |
属性服务器端的工作基本到这里就完成了。最后,我们来看一下客户端是如何发送设置属性的socket请求。
¶客户端发送请求
客户端设置属性时是调用了property_set(“sys.istest”, “true”)方法。从上述分析可知,该方法实现跟服务器端的property_set方法不同,该方法一定是发送了socket请求,该方法源码位置为:/system/core/libcutils/properties.c:
1 | int property_set(const char *key, const char *value) |
可以看到,property_set调用了__system_property_set方法,这个方法位于:/bionic/libc/bionic/system_properties.c文件中:
1 | struct prop_msg |