Android提供了系统属性值,既可以在java中使用,也可以C/C++中使用,非常方便。本文主要学习相关的一些知识,帮助我们更好地理解和使用系统属性。
本文基于Android4.2、MTK6572平台的代码进行分析。
主要涉及的内容包括它的数据结构,如何实现读和写,具体某个属性值的产生等问题。
一、数据结构
每一个系统属性值作为一条记录,保存在prop_info这个结构体中:其中,name和value的最大长度分别为32和92,再加上4个字节的serial,每个系统属性占用128个字节的空间。系统目前最大支持375个系统属性(从代码注释中看到原来是247个,可能是MTK为了增加自己的一些系统属性,把这个值增大了)。
struct prop_info { char name[PROP_NAME_MAX]; unsigned volatile serial; char value[PROP_VALUE_MAX]; }; struct prop_area { unsigned volatile count; unsigned volatile serial; unsigned magic; unsigned version; unsigned reserved[4]; unsigned toc[1]; }; #define PROP_NAME_MAX 32 #define PROP_VALUE_MAX 92 #define PA_COUNT_MAX 375
toc这个数组对应了后面的系统属性。通过toc[i]就能够找到prop_info i。但是,toc[i]中存放的并不是指向prop_info i的指针,而是记录了两个信息:对应prop_info i的name的长度以及它相对于prop_area首地址的偏移量。toc存储的是unsigned int,占用4个字节,其中,最高字节保存name的长度(name最大长度为92,用8位来表示绰绰有余),较低的三个字节存储偏移量(最大偏移量为49664,用24位来表示也足够了),通过这个偏移量就能够找到对应的prop_info。
为何需要保存name的长度呢?如果不用保存它的话,其实我们可以完全不需要toc这个数组,我们一样能够通过计算偏移量找到每个prop_info。保存name的长度其实是为了提高查找系统属性的效率:我们在get/set某个系统属性时,都需要尽快根据该系统属性的name查找到它对应的prop_info结构。那么一般地,我们可能需要遍历这块空间(从0到count依次根据toc找到prop_info),然后将我们的name和该prop_info的name进行依次字符串的比较,如果相等,则查找成功,结束。而现在,我们是这样查找的:也需要依次遍历这些属性值,但是我们并不是直接将我们的name和该prop_info的name进行依次字符串的比较,而是先比较toc中存储的prop_info的长度与我们的name的长度是否相同,这是一个int型的比较,相比与字符串的比较,快了很多,如果name长度相等,我们才会进一步比较name是否完全相同,这样能够避免很多不必要的字符串比较,因此查找效率得到提高。
在Android4.4中,对这部分数据结构又进行了优化,采用类似二叉树的结构来存储属性信息,查找效率又得到了提高。
二、实现框架
系统属性的使用非常方便,既可以在java中用,也可以在C/C++中方便地使用。java中的调用接口是SystemProperties.get和SystemProperties.set;C/C++中的接口是property_get和property_set。java层之所以能够调用,还是透过jni把C/C++中的方法进行了封装,本质上这些最终都是调用property_get和property_set这两个方法。而这两个方法的实现就要依赖libc和init中对系统属性的实现,所以我们主要看这部门是如何实现的。
首先应该明确,对系统属性的操作,核心操作是有get和set两种。但是,只有几乎所有进程都可以get某个系统属性,但是只有init进程才能够直接set某个系统属性,而其它进程只能间接地通过init来set,而init进程会在真正set之前,进行一些权限的检查,这样我们就不能随意地set系统属性了。 下面的分析中,我们来分析以下几个问题:前面提到的prop_area空间是如何分配的;为何只有init进程具有set的权限;set和get的具体过程;
1.prop_area空间是如何分配的,为何只有init进程具有set的权限?
init进程创建了一个文件,并设置这个文件的大小为49664B。它以可读可写的方式打开这个文件,并持有这个文件句柄,这个文件句柄是私有的,其它进程获取不到;它又另外以只读的权限打开这个文件,并把这个句柄存储在环境变量中,其它进程可以获取到,所以其它进程可读但不可写;其它进程要想写,必须通过socket通信的方式请求init去执行写操作,这样,init就能对权限进行严格的审查了。下面来分析代码吧:
typedef struct { void *data; size_t size; int fd; } workspace; //这里传入的size大小即为49664 static int init_workspace(workspace *w, size_t size) { void *data; int fd; //首先,以可读可写的方式打开/dev/__properties__这个文件(如果文件不存在,则创建之) fd = open("/dev/__properties__", O_RDWR | O_CREAT, 0600); //下面这个函数的功能是裁剪某个文件的大小为固定大小,这样之后,这个文件的大小就为49664B了 ftruncate(fd, size); //这个方法只是把这个文件映射到我们的进程空间中,这样的好处是我们可以像操作内存一样操作这个文件,这里的data就是真正存放我们系统属性值的空间 //留意,这里映射时是可读可写的,所以init进程一定不能把这个data公开出去;另外,MAP_SHARED表明,对这块内存的修改会同步到对应的文件中。 data = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); //关闭文件,目的是为了以另一种只读的方式重新打开文件 close(fd); //重新以只读的方式打开这个文件,当然这里是只读的 fd = open("/dev/__properties__", O_RDONLY); //unlink类似remove,执行之后,其实我们在/dev目录下,已经看不到__properties__这个文件了,这样能够避免其它程序再open这个文件 //但是这块空间还没有被释放,仍然可以使用,直到close这个fd,但是似乎不需要close了,知道手机关机 unlink("/dev/__properties__"); //这里不止保存了最重要的data区域,还保存了fd和size,其它进程可以通过下面的get_property_workspace来获得fd和size,得到了fd就能够操作这个文件(只读方式) w->data = data; w->size = size; w->fd = fd; return 0; } static int init_property_area(void) { ...... init_workspace(&pa_workspace, PA_SIZE); ...... } void get_property_workspace(int *fd, int *sz) { *fd = pa_workspace.fd; *sz = pa_workspace.size; } 在init.c的service_start方法中,通过调用get_property_workspace来得到文件/dev/__properties__的id,并将其设置到环境变量中 get_property_workspace(&fd, &sz); sprintf(tmp, "%d,%d", dup(fd), sz); add_environment("ANDROID_PROPERTY_WORKSPACE", tmp); 在system_properties.c的__system_properties_init方法中会从环境变量中将fd和size取出,从而能够读这个文件: int __system_properties_init(void) { env = getenv("ANDROID_PROPERTY_WORKSPACE"); fd = atoi(env); env = strchr(env, ','); sz = atoi(env + 1); pa = mmap(0, sz, PROT_READ, MAP_SHARED, fd, 0); }
下图中,fd=8,size=49664
2.对读写的异步控制
/* ** Rules: ** - there is only one writer, but many readers ** - prop_area.count will never decrease in value ** - once allocated, a prop_info's name will not change ** - once allocated, a prop_info's offset will not change ** - reading a value requires the following steps ** 1. serial = pi->serial ** 2. if SERIAL_DIRTY(serial), wait*, then goto 1 ** 3. memcpy(local, pi->value, SERIAL_VALUE_LEN(serial) + 1) ** 4. if pi->serial != serial, goto 2 ** ** - writing a value requires the following steps ** 1. pi->serial = pi->serial | 1 ** 2. memcpy(pi->value, local_value, value_len) ** 3. pi->serial = (value_len << 24) | ((pi->serial + 1) & 0xffffff) ** */文件中的注释已经很清晰了,这里简单解释以下:
只有一个程序写,但是有很多程序读;
系统属性值的数量只会不断增加,一旦某个属性值写入,是无法将它删除的,且已经写入的属性值的偏移量也不会改变;
按照如下方式控制读写:
写的过程--先将serial与1或(pi->serial = (valuelen << 24) 所以serial的最高一个字节存储的是value的长度,而其余24位则为0),这样最低位为1,表示正在写,那么它就是dirty的,此时不应该读;写结束之后,再+1则最低位为0,那就不再是dirty的,可以读了。
读的过程--先看最低位是否为1(1则表示dirty),那么这是就要陷入while循环,一直等;循环出来后,就可以读了,可是如果读完之后发现serial跟之前的不一样了,那么说明在读的时候又有更新了,因此还得重新读,不能退出for循环。
要了解控制读写的细节,还请留意__system_property_read方法中的__futex_wait和update_prop_info方法中的__futex_wake的使用。
另:当某个系统属性是第一次add进来时,可以直接写,不用这么复杂,而只有更新某个系统属性时,才需要注意控制异步的过程。
3.利用socket通信set系统属性
对这一块不是很了解,给出大致流程:
property_service.c中的start_property_service方法创建服务端的socket。
void start_property_service(void) { ...... fd = create_socket(PROP_SERVICE_NAME, SOCK_STREAM, 0666, 0, 0); listen(fd, 8); }
system_properties.c中的send_prop_msg方法连接到server端,并发送消息
static int send_prop_msg(prop_msg *msg) { struct sockaddr_un addr; s = socket(AF_LOCAL, SOCK_STREAM, 0); memset(&addr, 0, sizeof(addr)); namelen = strlen(property_service_socket); strlcpy(addr.sun_path, property_service_socket, sizeof addr.sun_path); addr.sun_family = AF_LOCAL; alen = namelen + offsetof(struct sockaddr_un, sun_path) + 1; connect(s, (struct sockaddr *) &addr, alen); send(s, msg, sizeof(prop_msg), 0); ...... }至于服务端何时会处理client的请求,不很了解,服务端(init进程)在一个无限for循环中会不断地探测是否有消息来到,如果有,则会调用handle_property_set_fd()去处理系统属性的set:
nr = poll(ufds, fd_count, timeout); handle_property_set_fd();
三、属性值的产生和获取
四、eng版本中对开发者开放权限