概述
Property是Android系统中一个重要的概念,在Android系统内,主要用于系统配置,以及不同服务间的简单信息分享。比如设备名字,蓝牙名字,编译信息,网络dns地址,以及其他的一些基本信息。 除了简单的信息分享外,还有个功能是启动和停止系统服务。 通过设置ctl.start.xxx属性,来启动某个属性,或者设置service.xxx.exit来停止服务。
Android的系统属性Property整体上看,是键值对保存, 即Key -- Value方式。在系统运行过程中,Property是以字典树的方式存储内存中。但是也有一些固定的,不能修改的属性是存储在磁盘文件中。这些文件中存储的属性,在运行过程中是不会有交互使用的。只是在Android系统启动之初把这些固定的,不能修改的属性加载到内存中。 之所以选择字典树作为Property存储的数据结构,主要原因可能是trie这种数据结构的特点:把具有相同前缀的字符存储在同一个节点中,后续不同的字符存储为此节点的子节点。这种存储方式,能够是字符的查找速度更快。
Property服务的启动时在init进程中进行的。在init进程初始化其他服务的同时,尽量早地在启动property服务。
下面简单介绍Property启动的过程流程,大致分为如下:
1. property_init();【system/core/init/init.c】
2. property_load_boot_defaults();【system/core/init/init.c】
3. property_service_init_action;【system/core/init/init.c】
4. load_all_props; 【system/core/rootdir/init.rc】
5. queue_property_triggers_action;【system/core/init/init.c】
6. handle_property_set_fd();【system/core/init/init.c】
对于property模块而言,启动完成后,就是在一个死循环中,不停地检查是否有他服务设置属性,如果有的话,接收和处理设置的属性。在阅读源码过程中,有个概念值得提一下:内存屏障(memory barrier),以下来自百科, 也称内存栅栏,内存栅障,屏障指令等, 是一类同步屏障指令,使得CPU或编译器在对内存随机访问的操作中的一个同步点,使得此点之前的所有读写操作都执行后才可以开始执行此点之后的操作。
property模块的数据结构
Property的存储的模型大致如下:
+-----+ children +----+ children +--------+ | |-------------->| ro |-------------->| secure | +-----+ +----+ +--------+ / \ / | left / \ right left / | prop +===========+ v v v +-------->| ro.secure | +-----+ +-----+ +-----+ +-----------+ | net | | sys | | com | | 1 | +-----+ +-----+ +-----+ +===========+
根据字典树的存储方式,property相关的重要数据结构都是和字典树相关,有如下几个:
struct prop_bt
描述每个节点的信息,对应的就是属性名字中的某一部分名字的信息。这些信息包括此节点字符串,左右兄弟节点地址,子节点地址。同时在结构体声明时,也同时声明了prop_bt节点是不能赋值的,只有在构造的时候才能给这些信息初始化。不能赋值操作是通过宏 DISALLOW_COPY_AND_ASSIGN(prop_bt)来实现的。
struct prop_area {}
描述Property所在使用内存的信息,包括property的版本信息,和使用空间。
struct prop_info {}
描述某一个property的信息,比如属性[sys.boot_completed]:[1], 这个数据结构就是描述这个属性的整体信息的,包括属性名,属性值,以及属性值得长度。在这个结构体中,有个属性是serial,这个属性用来表示属性值的长度。 serial用关键字volatile修饰,这个值是经常在变化的,每次要使用这个值得时候,都要从内存去读取,而不是从register或cache读取。 另外,个人认为,这个属性除了表示属性值的长度外,还是个同步锁,同时只允许一个进程对此属性进行操作。
总结一下,以属性[sys.boot_completed]:[1]为例,属性名字sys.boot_completed以‘.’为分隔符分成两部分,需要两个prop_bt去存储。最后,在保存值得时候创建prop_info去保存[sys.boot_completed]:[1]。最后把prop_info的地址放入最后一个节点prop_bt的prop中。
下面稍微具体点来介绍Property这个模块
先从流程开始介绍:
1.property_init();【system/core/init/init.c】
在Android启动时, 在Init进程中很早就被调用了,主要作用就是初始化Property的使用的内存空间。初始化的方式就是创建一个tmfs文件系统文件/dev/properties,然后把这个文件一共享内存的方式映射到内存中,映射到内存的大小是128KB。最后,在libc中导出了这块共享内存的地址。这样,就有两种方式可以与Property进行读写操作了,一种是就是通过文件读写;另外就是内存共享读写。使用property的地址对property进行读写主要是property_service,运行在init进程中。这个过程对应的代码如下:
static int init_property_area(void) { if (property_area_inited) // flag return -1; if(__system_property_area_init()) //[bionic/libc/bionic/system_properties.cpp] return -1; if(init_workspace(&pa_workspace, 0)) return -1; fcntl(pa_workspace.fd, F_SETFD, FD_CLOEXEC);//fork a process, but this fd will be closed property_area_inited = 1; return 0; }
2. property_load_boot_defaults();【system/core/init/init.c】
在init.rc文件进行解析之前,就要加载此函数。因为default.prop文件中包含了一些重要属性**———— 要查———— **。 这个函数主要是加载Android系统根目录的/default.prop文件。这个文件的生成过程在【/build/core/Makefile】中INSTALLED_DEFAULT_PROP_TARGET模块。加载主要过程简述如下:
打开/default.prop文件, 读取过程中以行为单位进行分析(对应代码是while ((eol = strchr(sol, ‘\n‘))) {**}),
然后找出‘=’两侧的内容,然后调用 property_set(key, value)完成属性赋值过程。property_set的实现如下:
3. property_service_init_action;【system/core/init/init.c】
首先此函数是由queue_builtin_action(property_service_init_action, "property_service_init");调用触发的,
这个调用是发生在init.rc文件开始解析之后,并且已经执行完成了early-init, init系列动作之后进行的。但是这个这个函数执行是发生在after-init之前进行的。其实在第一步成功执行之后,就已经可以开始进行属性的设置和查找。只不过,在这个函数之前发生的设置属性操作,基本来自于default.prop 和 init.rc。从这两个地方过来的属性设置,都是init进程解析文件,主动发起的设置,发生在init进程中。所以可以直接对Property所在的内存空间直接进行读写,添加或修改属性。这个函数的主要功能就是创建一个socket,然后其他进程想要设置属性的时候,就可以通过socket通信,把想要设置的属性发送过来,然后交由init进程代为设置。
void start_property_service(void) { int fd; fd = create_socket(PROP_SERVICE_NAME, SOCK_STREAM, 0666, 0, 0, NULL); if(fd < 0) return; fcntl(fd, F_SETFD, FD_CLOEXEC); fcntl(fd, F_SETFL, O_NONBLOCK); listen(fd, 8); property_set_fd = fd; }
这个函数内容比较简洁,虽然函数名字的意思是启动property服务,但是实际上property服务并不是一个单独的进程中,而是运行在当前进程--init中。这个函数执行完成后,会在/dev/socket/创建一个socket文件/dev/socket/property_service。以后其它进程中可以通过这个socket文件与init进程通信,通信的内容是要init进程代为设置property属性。后面会说明这个通信过程。
4. load_all_props; 【system/core/rootdir/init.rc】
这个动作在函数的代码中是找不到的。它是init.rc中定义的一个命令,在late-init之后才会被触发。触发过程如下:
on load_all_props_action load_all_props ... on late-init trigger early-fs trigger fs trigger post-fs trigger post-fs-data # Load properties from /system/ + /factory after fs mount. Place # this in another action so that the load will be scheduled after the prior # issued fs triggers have completed. trigger load_all_props_action ...
这个动作也是从文件中加载一些系统属性,和第二步中加载/default.prop方式一样,只不过这次加载的这些属性是要在/system, /factory文件系统挂载之后才能进行的。具体过程可以参考第二步.
5. queue_property_triggers_action;【system/core/init/init.c】
当init进程执行到这儿的时候,所有系统必要的属性都已经基本设置完成。这里会逐一trigger监听属性的操作。和第二步中的最后一个动作的道理一样。
6. handle_property_set_fd();【system/core/init/init.c】
这个函数是Property模块中最为重要的函数之一。因为所有其它进程想要设置属性,都是通过socket通信进行的,当init进程中监听到有新的消息发送过来时,就会调用此函数。init进程是通过Linux的poll机制去监听Property通信用的socket的文件。
for(;;) { ... 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; } ... 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(); ... } } }
在init进程执行到最后,开启了一个无限循环,在这个循环过程中使用poll机制监听了一共三个文件,而Property得socket通信文件/dev/socket/property_service就是其中之一。当有socket有新的消息过来时,就会调用handle_property_set_fd();函数。整个过程如上面这段代码所述。handle_property_set_fd函数的流程如下:
对于在init进程中设置属性过程基本如此。但是对于其它进程是如何把要设置的属性转为消息,并通过socket发送到init进程中的这个过程还没有接触到,在下面的内容property与其他模块的通信方式和通信对象中会说明
Property的查找
由前面的叙述,我们已经知道,Property的存储是用字典树这种数据结构来存储的。使用字典树存储的目的就是为了查找速度更快,那么我们先看些Property从字典树中的查找过程:
在Property模块中的一些注意事项:
问题1: Property本身的信息是使用prop_info这个结构体描述的,而存储时的信息是用prop_bt描述的,那么在存储过程中,如何把从socket接收到的信息到生成prop_info结构体的过程是怎样的?在存储时如何把结构体prop_info,和结构体prop_bt是如何存储的?在修改已有的属性时,如何从内存中找到所需要的属性的?
答:在解答上面一系列的问题之前,我们先看过函数find_property()的流程: 这个问题可以分如下几个小点回答:
- 如何把socket信息转化为结构体prop_info的?
从socket发送过来的信息,封装成了msg结构体。这个结构体中包含了想要进行的操作是设置属性,还有>设置属性所需要的key,value值。属性设置所需要的两个变量都有了,下面创建属性信息prop_info结构体,和存储属性结构体prop_bt都是property服务的工作了。- 如何从结构体prop_bt转化到prop_info的?to_prop_obj()函数 prop_bt是局部信息,prop_info是整体信息。说法不太好
find_property() 函数完成这个工作。【bionic/libc/bionic/system_properties.cpp】
从根节点开始查找,如果没有找到的话,不要再当前函数中为prop_info申请空间。先把要设置属性的名字开始分段,以‘.’为分隔符,查找是否存在对应的节点是否存在,如果存在,那就找名字第二部分对应的节点是否存在,以此类推。如果都存在的话,那么就说明这个属性已经存在了,这时候通过to_prop_obj()函数,把其对应的信息构造成prop_info返回。如果属性名字中,有其中一部分没有找到的话,那么就要申请内存空间,创建属性,并存储。- 如何从结构体prop_info转化到prop_bt的?
find_prop_bt() 函数完成这个工作。【bionic/libc/bionic/system_properties.cpp】
流程图
无论是添加属性,更新现有属性都是通过find_property()这个函数中的分发去处理的。【bionic/libc/bionic>/system_properties.cpp】
把属性中目标名字和节点中的name做比较,按照字母升序排列,如果节点中的名字靠后,那么返回值是 < 0;反之,返回值 > 0; 如果属性中目标名字和节点的名字一样的话,那么返回值就是0. 这样比较后,如果返回值是大于0,那么就去属性字典树的右侧查找;反之,就去字典树的左侧查找。如果返回值是等于0, 那么意味着找到了此节点。
__system_property_add和__system_property_find的区别就是在查找节点过程中,如果不存在想要找的节点,是否要创建新的节点。__system_property_add是要创建。__system_property_find是不创建。
Property与其他模块的通信方式和通信对象
在Android系统中,和属性相关的操作只有两个:查找属性,读取属性和设置属性。没有删除属性操作。另外,查找属性发生在读取属性和设置属性的过程中,其它模块一般不会直接使用查找操作。在常用的读取属性和设置属性这两个操作中,读取属性是不涉及到进程间通信的。前面我们也说到了,Property属性是存储在内存中,并且进行了内存共享映射,所以在读取属性的时候,是直接从共享内存中读取的,就发生在当前进程中,没有用到进程间通信。而用到进程间通信的是设置属性操作。因为在设置属性时,可能会启动其它的服务,也可能会修改某些系统配置,这都需要进行权限检查,所以不允许进程直接对属性设置,而是通过socket通信,把要设置的属性发送给init进程,由init进程代为进行属性设置。
下面以设置属性为例,补充完成上面第六步中handle_property_set_fd()中接收消息之前的操作。 假设我们想要设置一个属性property_set("test.test.test", "1"), 首先找到property_set函数system/core/libcutils/properties.c 文件中。对于Java层中进行的属性设置,最终会通过JNI调用此函数,详细过程不再述说。property_set会调用Android系统库libc中的__system_property_set()函数在bionic/libc/bionic/system_properties.cpp文件中,在__system_property_set()函数中,把要设置的属性封装成为msg后,就开始通过socket发送这个消息。发送消息的函数是send_prop_msg(&msg);也在同一个文件。这个函数就是通信的重点。在send_prop_msg()函数中,主要是创建socket,并通过这个socket发送消息。所创建的socket要想和init进程通信,就必须知道init进程段接收端的socket文件。Android系统中,Property接收端socket文件是默认的路径/dev/socket/property_service文件。这个文件是由前面第三步中create_socket(PROP_SERVICE_NAME, SOCK_STREAM, 0666, 0, 0, NULL);创建。在create_socket()函数是Android系统自定义的函数,默认的就是在/dev/socket/目录下创建指定的socket文件。到这儿,当前进程就找到了Property在init进程中接收端的socket文件,就可以通过socket发送信息,进行属性设置了。