《Android4.4 property机制学习》补充篇——属性树与其在内存中的存储结构

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进制进行解析的。内容都是对比源码和内存数据后,根据自己的理解写出来的,所以有不正确的地方欢迎指正!

 

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值