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
在prop_area结构体中,count代表当前系统属性的总数,magic和version都是固定的值,reserverd是保留字,暂时无用,最值得一提的就是toc这个数组。这块存储系统属性的空间一共占用了49664个字节。紧接着toc数组之后的是在prop_area结构体中未定义的,它们分别连续地存放了375个系统属性(申请了可以存放376个属性的空间,最后一块空间应该没有用到)。
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中对系统属性的实现,所以我们主要看这部门是如何实现的。
下面的分析中,我们来分析以下几个问题:前面提到的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版本中对开发者开放权限