1、引言
在上一篇博文《Android4.4 property机制学习》的提到的属性字典树,由于当时对其结构不太了解,所以没有细说。最近根据属性树相关的代码、注释、以及内存数据对照学习之后,有了一些自己的理解,上一篇博客感觉篇幅太长,而且这里要说的内容比较独立,所以单独写一篇,可以详细补充一下。有些内容出现的比较唐突的话,可以先看一看上一篇文章或源代码。
2、正文
首先是这棵树的组织结构,源代码作者也给出了图解注释。其主要是由prop_info和prop_bt这两个结构体组成,由点(.)分割的属性名以结构体prop_bt形式组成树的树杈,prop_bt的prop成员所指向的结构体prop_info组成树叶,树叶中存放着该属性的值。
// kitkat\bionic\libc\bionic\system_properties.c
/*
* Properties are stored in a hybrid trie/binary tree structure.
* Each property's name is delimited at '.' characters, and the tokens are put
* into a trie structure. Siblings at each level of the trie are stored in a
* binary tree. For instance, "ro.secure"="1" could be stored as follows:
*
* +-----+ children +----+ children +--------+
* | |-------------->| ro |-------------->| secure |
* +-----+ +----+ +--------+
* / \ / |
* left / \ right left / | prop +===========+
* v v v +-------->| ro.secure |
* +-----+ +-----+ +-----+ +-----------+
* | net | | sys | | com | | 1 |
* +-----+ +-----+ +-----+ +===========+
*/
只看这些,似乎没有直观的感受。还记得上篇文章中说到"/dev/__properties__"这个文件是映射到内存的,即属性数据是存储到这里的,那么我们就通过adb从设备上拿到这个文件,对比代码和内存数据,就可以一个字节一个字节的对比查看了吧。既然是内存数据,肯定是二进制形式,用Notepad++打开这个__properties__文件,果然全是乱码。不要慌,在Notepad++里下载一个HexEditor的插件,至于怎么安装这个插件,百度一下吧,有很多教程。安装好了之后,按顺序点击【插件】——>【HEX-Editor】——>【View in Editor】,就可以以16进制形式打开这个内存数据文件了。这个文件在映射进内存的时候,是以pa_size=128*1028为大小操作的,所以这跟文件大小也就是128KB。
看内存数据之前,我们再次回顾一下上篇文章初始化属性的时候,函数map_prop_area_rw()中的初始化操作:
// kitkat\bionic\libc\bionic\system_properties.c
struct prop_area {
unsigned bytes_used;
unsigned volatile serial;
unsigned magic;
unsigned version;
unsigned reserved[28];
char data[0];
};
typedef struct prop_area prop_area;
…………
static int map_prop_area_rw()
{
pa = mmap(NULL, pa_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
…………
memset(pa, 0, pa_size);
pa->magic = PROP_AREA_MAGIC;
pa->version = PROP_AREA_VERSION;
/* reserve root node */
pa->bytes_used = sizeof(prop_bt);
/* plug into the lib property services */
__system_property_area__ = pa;
……
}
可以看到,通过mmap函数将该文件映射进内存之后,将该这块内存首地址辗转传给了prop_area类型的全局变量__system_property_area__,并对其成员做了赋值操作。也就是说这块内存地址首部是一个prop_area结构体的空间,所以对照内存数据来看看吧(注意:不同设备具体有些数据会不同,请同步对比查看):
因为四位二进制的表示范围是0~f,八位二进制表示范围是00~ff,而八位二进制又等一个字节,所以16进制显示下,一组数表示一个字节。因为结构体prop_area的第一个成员bytes_used,是一个无符号四字节整数,所以bytes_used在内存中的数据为【60 ce 00 00】。又因为arm架构内存数据采用小端存储模式,即数据高位在后,低位在前,所以成员bytes_used真正的数据为【00 00 ce 60】。bytes_used这个成员表示了这块内存中,紧接在prop_area这个首部之后,属性树真正所占用的字节空间大小,后面我们会对【00 00 ce 60】这个16进制数据进行验证。通过magic和version这两个成员数据,也可以证明我们的这里对比查看方式是正确的:
// kitkat\bionic\libc\include\sys\_system_properties.h
#define PROP_AREA_MAGIC 0x504f5250
#define PROP_AREA_VERSION 0xfc6ed0ab
16进制格式下,每行16字节,即每行有4个无符号整形的空间。所以用于预留空间的reserved占据了28/4=7行的空间。虽然声明中,char data[0]表示data所占空间为0,但这个data这个指针是指向结构体prop_area空间末尾的下一个字节的。这个字节的地址是【00 00 08 00】,后续所有的添加、查找、赋值操作,都是以这个字节空间为坐标,进行偏移定位的。
上面的初始化操作中,可以看到,将bytes_used设置为一个prop_bt空间的大小。这是为了紧跟在prop_area之后,预留一个prop_bt空间,将其作为整棵树的根节点,而且data也是指向这个根节点首部的。那么根据结构体prop_bt和prop_info来看看后续的内存空间吧:
// kitkat\bionic\libc\bionic\system_properties.c
struct prop_info {
unsigned volatile serial;
char value[PROP_VALUE_MAX]; // #define PROP_VALUE_MAX 92
char name[0];
};
typedef struct prop_info prop_info;
#################################################
typedef volatile uint32_t prop_off_t;
struct prop_bt {
uint8_t namelen;
uint8_t reserved[3];
prop_off_t prop;
prop_off_t left;
prop_off_t right;
prop_off_t children;
char name[0];
};
typedef struct prop_bt prop_bt;
可以看到结构体prop_info所占空间=4*1+1*92=96字节,在16进制每行16字节的情况下,刚好占据6行。但是在代码中,实际创建一个prop_info结构体实例时,所申请的空间可不止这些:
// kitkat\bionic\libc\bionic\system_properties.c
static prop_info *new_prop_info(const char *name, uint8_t namelen,
const char *value, uint8_t valuelen, prop_off_t *off)
{
prop_off_t off_tmp;
prop_info *info = new_prop_obj(sizeof(prop_info) + namelen + 1, &off_tmp);
if (info) {
memcpy(info->name, name, namelen);
info->name[namelen] = '\0';
info->serial = (valuelen << 24);
memcpy(info->value, value, valuelen);
info->value[valuelen] = '\0';
ANDROID_MEMBAR_FULL();
*off = off_tmp;
}
return info;
}
从代码中可以看出,函数new_prop_info()通过调用new_prop_obj()函数来创建prop_info结构体实例时,所传递的的size参数,是在prop_info结构体大小的基础上,加上当前属性名的长度+1。这是因为其将当前的属性名复制到了结构体prop_info末尾、指针name所指向的后续字节空间,并且在属性名末尾加上了'\0',用于结束字符串。
在后续的赋值操作中,将属性值长度左移24位的值,赋值给成员serial 。假设属性值长度为7,二进制格式为:
0000 0000 0000 0000 0000 0000 0000 0111
<< 24 =
0000 0111 0000 0000 0000 0000 0000 0000
用16进制表示一下就是,【00 00 00 07】 << 24 = 【07 00 00 00】。又因为是小端存储模式,所以这个serial在内存中的存储格式将会是【00 00 00 07】。这里根据我的理解,为什么是左移24位,而不是8位或者16、32位。这是因为左移操作是针对属性值的长度大小,而属性值长度大小有上限(#define PROP_VALUE_MAX 92)限制的,92小于一个字节的大小256(无符号),即有效数据只占据了一个字节(8位),所以左移24位只是把实际有效数据从四字节的低八位挪到了高八位而已,并不会改变其有效位的数据。
注意末尾还有一个对指针偏移量off的赋值操作,这里的off的值就是这个新创建的prop_info实例在内存中,相对于根节点起始地址的偏移量,也就是相对于前面说到的prop_area末尾的data的偏移量,具体来说就是相对于【00 00 08 00】的偏移量。那么这个偏移量的值off_tmp是怎么算出来的呢,那就来看看new_prop_obj()函数吧:
// kitkat\bionic\libc\bionic\system_properties.c
#define ALIGN(x, a) (((x) + (a - 1)) & ~(a - 1))
static void *new_prop_obj(size_t size, prop_off_t *off)
{
prop_area *pa = __system_property_area__;
size = ALIGN(size, sizeof(uint32_t));
if (pa->bytes_used + size > pa_data_size)
return NULL;
*off = pa->bytes_used;
__system_property_area__->bytes_used += size;
return __system_property_area__->data + *off;
}
可以看到最终所申请空间的size大小是通过宏ALIGN进行了32位对其的,即所占用的空间size必然是4字节的倍数。而偏移量off的值,就是创建新的prop_info实例前,已用空间大小bytes_used的值,也就是在紧挨着已经占用的空间后面创建。创建实例完成后,已用空间大小bytes_used在原值的基础上加上了size的大小。
// kitkat\bionic\libc\bionic\system_properties.c
static prop_bt *new_prop_bt(const char *name, uint8_t namelen, prop_off_t *off)
{
prop_off_t off_tmp;
prop_bt *bt = new_prop_obj(sizeof(prop_bt) + namelen + 1, &off_tmp);
if (bt) {
memcpy(bt->name, name, namelen);
bt->name[namelen] = '\0';
bt->namelen = namelen;
ANDROID_MEMBAR_FULL();
*off = off_tmp;
}
return bt;
}
树杈prop_bt的创建过程和树叶prop_info的类似,就不多说了。既然大概知道了树叶和树杈是如何使用空间的,那么就对比下图中的内存数据来看看吧。
注意我们是接着上边的数据,从【00 00 00 80】的位置开始的,是根节点起始位置,也是data的位置,是后续所有偏移位置的参考点。
根节点:根据prop_bt的成员结构,先来分析根节点。根据我下图标出来的内容,可以看到根节点除了指出了孩子节点(children)的偏移量外,其他成员数据都为0,即根节点是一个空节点。那么如何根据children的偏移量【00 00 00 14】来找到根节点的children呢?很简单,就是在我们说的参考点【00 00 00 80】的基础上加上偏移量【00 00 00 14】= 【00 00 00 94】。根据这个结果可以找到横坐标为【00 00 00 90】,纵坐标为【4】的位置。就是图中第二行第二个四字节开始的位置,也就是我在第二行标出namelen的位置。从这里开始的一个prop_bt的size就是根节点的children节点:
"ro"节点:【namelen:02】、【reserved:00 00 00】、【prop:00 00 00 00】、【left:00 00 a0 9c】、【right:00 00 06 08】、【children:00 00 00 2c】、【name:ro】。可看出这个节点名称为"ro",没有属性值(prop为0),但有left、right、children这三个子节点。children的数据之后就是name,即"ro"这两个ascii码的16进制数据,占两个字节,'\0'用于结束字符串,占一个字节,又因为需要四字节内存对其,所以再添加了一个空字节。看看下一个字节的位置【00 00 00 ac】减去参考点位置【00 00 00 80】= 【00 00 00 2c】,差值刚好是"ro"节点的children偏移量。即紧挨着"ro"节点的是它的children节点:
"bootmode"节点:【namelen:08】、【reserved:00 00 00】、【prop:00 00 00 4c】、【left:00 00 00 b8】、【right:00 00 01 44】、【children:00 00 00 00】、【name:bootmode】。注意字符串"bootmode"末尾还有个占用一字节的'\0'用于结束字符串,所以在在字符串"bootmode"之后的【00 00 00 00】是用来内存对其的,也是属于当前节点空间的。下一字节的起始位置【00 00 00 cc】-【00 00 00 80】 =【00 00 00 4c】,正好是"bootmode"的prop节点,存储的是prop_info数据。
"ro.bootmode"节点:【serial:07 00 00 00】、【value:"unknow"】、【name:"ro.bootmode"】。首先这里可以验证serial的值就是"unknown"的长度【00 00 00 07】左移24位得到的结果,其次作为一个叶子节点,这里存储了一个属性键值:"ro.bootmode : unknown"。
…………
按照上面的计算偏移量的方式,可以把这块内存中每个节点的数据都提取出来。但由于数据太多,把每个数据都提取出来太麻烦,也没有意义。我尽量向后多提取了一些,画出了如下树形图用来说明情况:
图中只有偏移量没有节点name的prop_bt节点,因为子节点多而且太占地方,就没去提取数据,也没画。大家重点关注标出了子节点(left、right、children)的prop_bt节点,和所有的prop_info节点。可以发现一些规律的,比如,一个prop_bt节点,可以没有left或right节点,但一定有children或prop节点,而且是要么只有children节点,要么只有prop节点,即children和prop是互斥存在的。此时再回去看看源代码作者的注释图,似乎就能理解为什么是画成那样了。以"ro.allow.mock.location"为例子,在设定上"mock"节点要么作为属性名,拥有"ro.allow.mock"这个属性名和属性值,要么作为树杈节点,组成" ro.allow.mock.* "属性。
还记得前面我们说过要验证bytes_used值【00 00 ce 60】的正确性,其实也就是在参考点【00 00 00 80】+【00 00 ce 60】=【00 00 ce e0】,就是说在【00 00 ce e0】这个位置之前都是属性树的节点数据,在这个位置之后就全是memset()函数所设置的0了。那么眼见为实,贴图看看:
如图所示,到【00 00 ce e0】位置之前的最后一个四字节都是存储了具体数据的,在该位置之后,就全是0了,所以前面的byte_used的验证成功。不过出现了一个问题就是,最后一个prop_info结构中的serial数据似乎不对,本应该是"dhclient.pid"的长度【00 00 00 0c】,却变成了【05 00 02 32】。查看源代码,发现了端倪:
// kitkat\bionic\libc\bionic\system_properties.c
int __system_property_update(prop_info *pi, const char *value, unsigned int len)
{
prop_area *pa = __system_property_area__;
if (len >= PROP_VALUE_MAX)
return -1;
pi->serial = pi->serial | 1;
ANDROID_MEMBAR_FULL();
memcpy(pi->value, value, len + 1);
ANDROID_MEMBAR_FULL();
pi->serial = (len << 24) | ((pi->serial + 1) & 0xffffff);
__futex_wake(&pi->serial, INT32_MAX);
pa->serial++;
__futex_wake(&pa->serial, INT32_MAX);
return 0;
}
原来是每次更改属性值的时候,会将该属性叶子节点的serial做一定的变化。但是我们前面ro开头属性的serial为什么是正确的呢,那当然是因为"ro."开头的属性值是不可更改的啦,上篇文章有提到哦。即如果属性在首次add之后,serial的值是符合之前左移24位的规则的,但如果后续又进行了update操作,那么serial的值就会不符合前面的规则了。而是根据update函数这里的语句进行位操作的结果了:
pi->serial = (len << 24) | ((pi->serial + 1) & 0xffffff)
可以看到除了更改prop_info的serial之外,还对这块内存的头部数据prop_area中的serial做了+1操作。除了这里还有__system_property_add()函数也对prop_area中的serial做了+1操作。其中原因理解起来也不难,会对属性树内容做出更改的,也就只有add和update这两个接口,每次添加或者修改一个属性内容,以原子操作对prop_area中的serial进行+1,serial代表着当前整个属性树的一个状态,属性树内容每做一次修改,serial所代表的状态就变化一次。
// kitkat\bionic\libc\bionic\system_properties.c
int __system_property_add(const char *name, unsigned int namelen,
const char *value, unsigned int valuelen)
{
prop_area *pa = __system_property_area__;
const prop_info *pi;
if (namelen >= PROP_NAME_MAX)
return -1;
if (valuelen >= PROP_VALUE_MAX)
return -1;
if (namelen < 1)
return -1;
pi = find_property(root_node(), name, namelen, value, valuelen, true);
if (!pi)
return -1;
pa->serial++;
__futex_wake(&pa->serial, INT32_MAX);
return 0;
}
源代码中用来查找属性的__system_property_find()函数和用来添加的__system_property_add()函数中都调用了find_propperty()函数:
// kitkat\bionic\libc\bionic\system_properties.c
static const prop_info *find_property(prop_bt *trie, const char *name,
uint8_t namelen, const char *value, uint8_t valuelen,
bool alloc_if_needed)
{
const char *remaining_name = name;
while (true) {
char *sep = strchr(remaining_name, '.');
bool want_subtree = (sep != NULL);
uint8_t substr_size;
prop_bt *root;
if (want_subtree) {
substr_size = sep - remaining_name;
} else {
substr_size = strlen(remaining_name);
}
if (!substr_size)
return NULL;
if (trie->children) {
root = to_prop_obj(trie->children);
} else if (alloc_if_needed) {
root = new_prop_bt(remaining_name, substr_size, &trie->children);
} else {
root = NULL;
}
if (!root)
return NULL;
trie = find_prop_bt(root, remaining_name, substr_size, alloc_if_needed);
if (!trie)
return NULL;
if (!want_subtree)
break;
remaining_name = sep + 1;
}
if (trie->prop) {
return to_prop_obj(trie->prop);
} else if (alloc_if_needed) {
return new_prop_info(name, namelen, value, valuelen, &trie->prop);
} else {
return NULL;
}
}
首先根据__system_property_find()跟踪一下调用过程,即在这棵树里面查找指定属性。可以看到其将属性名以点(.)分割开来,依旧以上面的"ro.allow.mock.location"为例子。将其分割为"ro"、"allow"、"mock"、"location"这四个节点,首先通过find_prop_bt()函数找到"ro"节点,然后在"ro"节点的子节点中找到"allow"节点,按照此规律最终找到该属性的"location"节点(需要注意的是,属性树中可能有多个"location"节点,但是符合"ro"—>"allow"—>"mock"—>"location"这个树中上下文结构的只有这一个)。找到这个"location"节点后,根据prop_bt结构体中的prop成员,通过to_prop_obj()函数,在data的基础上查找prop偏移量的位置,最终找到存储"ro.allow.mock.location"这个属性的值的prop_info树叶。
而__system_property_add()函数调用时的不同点在于将最后一个参数alloc_if_needed置为true,使得在查找该节点的过程中,如果该属性名中以点(.)分割的某个节点在属性树中不存在时,就根据树中上下文去申请空间来创建这些不存在的节点。将该属性名的最后一个prop_bt节点创建好之后,还需要去创建了一个prop_info节点,即树叶,用于存储这个新加入的属性的值。
现在就剩下最后一个问题了,那就是find_prop_bt()函数是如何在属性树中组织并查找属性名中以点(.)分割的各个节点的呢,一起来看看函数细节吧:
// kitkat\bionic\libc\bionic\system_properties.c
static prop_bt *find_prop_bt(prop_bt *bt, const char *name, uint8_t namelen,
bool alloc_if_needed)
{
while (true) {
int ret;
if (!bt)
return bt;
ret = cmp_prop_name(name, namelen, bt->name, bt->namelen);
if (ret == 0) {
return bt;
} else if (ret < 0) {
if (bt->left) {
bt = to_prop_obj(bt->left);
} else {
if (!alloc_if_needed)
return NULL;
bt = new_prop_bt(name, namelen, &bt->left);
}
} else {
if (bt->right) {
bt = to_prop_obj(bt->right);
} else {
if (!alloc_if_needed)
return NULL;
bt = new_prop_bt(name, namelen, &bt->right);
}
}
}
}
static int cmp_prop_name(const char *one, uint8_t one_len, const char *two,
uint8_t two_len)
{
if (one_len < two_len)
return -1;
else if (one_len > two_len)
return 1;
else
return strncmp(one, two, one_len);
}
依然是先来看查找的过程,可以看到其在组织该属性树的时候,是根据strncmp()函数对各个属性名中的节点字符串进行字典比较,如果比较的结果是要查找的目标节点比当前节点小,那么就去当前节点的left子树中查找节点。如果比当前节点大,就去right子树中查找。直到找到和目标字符串比较结果相同的children节点。再来看看add的过程,相比查找过程,只不过是多了当左子树或右子树不存在时,直接在该左子树或右子树的位置上创建该节点的操作而已。
3、结语
上一篇文章《Android4.4 property机制学习》中,很多内容是在各位前辈的经验和解析基础上的学习。但这边博文完全是俺骑着共享单车上班的路上,突然想到可以把那个内存映射文件可以从设备里面获取到并以16进制进行解析的。内容都是对比源码和内存数据后,根据自己的理解写出来的,所以有不正确的地方欢迎指正!