3.2.4 属性服务

我们知道,Windows平台上有一个叫注册表的东西。注册表可以存储一些类似key/value的键值对。一般而言,系统或某些应用程序会把自己的一些属性存储在注册表中,即使系统重启或应用程序重启,它还能够根据之前在注册表中设置的属性,进行相应的初始化工作。Android平台也提供了一个类似的机制,称之为属性服务(property service)。应用程序可通过这个属性机制,查询或设置属性。读者可以用adb shell登录到真机或模拟器上,然后用getprop命令查看当前系统中有哪些属性。比如我的HTC G7测试结果,如图3-2所示(图中只显示了部分属性)。

3.2.4 属性服务 - 图1

图 3-2 HTC G7属性示意图

这个属性服务是怎么实现的呢?下面来看代码,其中与init.c和属性服务有关的代码有下面两行:


property_init();

property_set_fd=start_property_service();


分别来看看它们。

1.属性服务初始化

(1)创建存储空间

先看property_init函数,代码如下所示:


[—>property_service.c]

void property_init(void)

{

init_property_area();//初始化属性存储区域。

//加载default.prop文件。

load_properties_from_file(PROP_PATH_RAMDISK_DEFAULT);

}


在properyty_init函数中,先调用init_property_area函数,创建一块用于存储属性的存储区域,然后加载default.prop文件中的内容。再看init_property_area是如何工作的,它的代码如下所示:


[—>property_service.c]

static int init_property_area(void)

{

prop_area*pa;

if(pa_info_array)

return-1;

/*

初始化存储空间,PA_SIZE是这块存储空间的总大小,为32768字节,pa_workspace为workspace类型的结构体,下面是它的定义:

typedef struct{

void*data;//存储空间的起始地址

size_t size;//存储空间的大小

int fd;//共享内存的文件描述符

}workspace;

init_workspace函数调用Android系统提供的ashmem_create_region函数创建一块共享内存。关于共享内存的知识我们在第7章会接触,这里只需把它当做一块普通的内存就可以了。

*/

if(init_workspace(&pa_workspace,PA_SIZE))

return-1;

fcntl(pa_workspace.fd,F_SETFD,FD_CLOEXEC);

//在32768个字节的存储空间中,有PA_INFO_START(1024)个字节用来存储头部信息。

pa_info_array=(void)(((char)pa_workspace.data)+PA_INFO_START);

pa=pa_workspace.data;

memset(pa,0,PA_SIZE);

pa->magic=PROP_AREA_MAGIC;

pa->version=PROP_AREA_VERSION;

//system_property_area这个变量由bionic libc库输出,有什么用呢?

system_property_area=pa;

return 0;

}


上面的内容比较简单,不过最后的赋值语句可是大有来头。system_property_area是bionic libc库中输出的一个变量,为什么这里要给它赋值呢?

原来,虽然属性区域是由init进程创建的,但Android系统希望其他进程也能读取这块内存里的东西。为了做到这一点,它便做了以下两项工作:

把属性区域创建在共享内存上,而共享内存是可以跨进程的。这一点,已经在上面的代码中见到了,init_workspace函数内部将创建这个共享内存。

如何让其他进程知道这个共享内存呢?Android利用了gcc的constructor属性,这个属性指明了一个libc_prenit函数,当bionic libc库被加载时,将自动调用这个libc_prenit,这个函数内部就将完成共享内存到本地进程的映射工作。

(2)客户端进程获取存储空间

关于上面的内容,来看相关代码:


[—>libc_init_dynamic.c]

//constructor属性指示加载器加载该库后,首先调用__libc_prenit函数。这一点和Windows上

//动态库的DllMain函数类似。

voidattribute((constructor))__libc_prenit(void);

void__libc_prenit(void)

{

……

__libc_init_common(elfdata);//调用这个函数。

……

}

__libc_init_common函数为:

[—>libc_init_common.c]

void__libc_init_common(uintptr_t*elfdata)

{

……

__system_properties_init();//初始化客户端的属性存储区域。

}

[—>system_properties.c]

int__system_properties_init(void)

{

prop_area*pa;

int s,fd;

unsigned sz;

char*env;

……

//还记得在“启动zygote”一节中提到的添加环境变量的地方吗?属性存储区域的相关信息

//就是在那儿添加的,这里需要取出来使用了。

env=getenv("ANDROID_PROPERTY_WORKSPACE");

//取出属性存储区域的文件描述符。关于共享内存的知识,第7章中将会进行介绍。

fd=atoi(env);

env=strchr(env,',');

if(!env){

return-1;

}

sz=atoi(env+1);

//映射init创建的那块内存到本地进程空间,这样本地进程就可以使用这块共享内存了。

//注意,映射的时候指定了PROT_READ属性,所以客户端进程只能读属性,而不能设置属性。

pa=mmap(0,sz,PROT_READ,MAP_SHARED,fd,0);

if(pa==MAP_FAILED){

return-1;

}

if((pa->magic!=PROP_AREA_MAGIC)||(pa->version!=PROP_AREA_VERSION)){

munmap(pa,sz);

return-1;

}

system_property_area=pa;

return 0;

}


上面的代码中有很多地方与共享内存有关,在第7章中会对与共享内存有关的问题进行介绍,大家也可先行学习有关共享内存的知识。

总之,通过这种方式,客户端进程可以直接读取属性空间,但没有权限设置属性。客户端进程又是如何设置属性的呢?

2.属性服务器的分析

(1)启动属性服务器

init进程会启动一个属性服务器,而客户端只能通过与属性服务器交互来设置属性。先来看属性服务器的内容,它由start_property_service函数启动,代码如下所示:


[—>Property_servie.c]

int start_property_service(void)

{

int fd;

/*

加载属性文件,其实就是解析这些文件中的属性,然后把它设置到属性空间中去。Android系统一共提供了四个存储属性的文件,它们分别是:

define PROP_PATH_RAMDISK_DEFAULT"/default.prop"

define PROP_PATH_SYSTEM_BUILD"/system/build.prop"

define PROP_PATH_SYSTEM_DEFAULT"/system/default.prop"

define PROP_PATH_LOCAL_OVERRIDE"/data/local.prop"

*/

load_properties_from_file(PROP_PATH_SYSTEM_BUILD);

load_properties_from_file(PROP_PATH_SYSTEM_DEFAULT);

load_properties_from_file(PROP_PATH_LOCAL_OVERRIDE);

//有一些属性是需要保存到永久介质上的,这些属性文件则由下面这个函数加载,这些文件

//存储在/data/property目录下,并且这些文件的文件名必须以persist.开头。这个函数

//很简单,读者可自行研究。

load_persistent_properties();

//创建一个socket,用于IPC通信。

fd=create_socket(PROP_SERVICE_NAME,SOCK_STREAM,0666,0,0);

if(fd<0)return-1;

fcntl(fd,F_SETFD,FD_CLOEXEC);

fcntl(fd,F_SETFL,O_NONBLOCK);

listen(fd,8);

return fd;

}


属性服务创建了一个用来接收请求的socket,可这个请求在哪里被处理呢?事实上,在init中的for循环处已经进行相关处理了。

(2)处理设置属性请求

接收请求的地方在init进程中,代码如下所示:


[—>init.c:main函数片断]

if(ufds[1].revents==POLLIN)

handle_property_set_fd(property_set_fd);


当属性服务器收到客户端请求时,init会调用handle_property_set_fd进行处理。这个函数的代码如下所示:


[—>property_service.c]

void handle_property_set_fd(int fd)

{

prop_msg msg;

int s;

int r;

int res;

struct ucred cr;

struct sockaddr_un addr;

socklen_t addr_size=sizeof(addr);

socklen_t cr_size=sizeof(cr);

//先接收TCP连接。

if((s=accept(fd,(struct sockaddr*)&addr,&addr_size))<0){

return;

}

//取出客户端进程的权限等属性。

if(getsockopt(s,SOL_SOCKET,SO_PEERCRED,&cr,&cr_size)<0){

……

return;

}

//接收请求数据。

r=recv(s,&msg,sizeof(msg),0);

close(s);

……

switch(msg.cmd){

case PROP_MSG_SETPROP:

msg.name[PROP_NAME_MAX-1]=0;

msg.value[PROP_VALUE_MAX-1]=0;

/*

如果是ctl开头的消息,则认为是控制消息,控制消息用来执行一些命令,例如用adb shell登录后,输入setprop ctl.start bootanim就可以查看开机动画了,如果要关闭就输入setprop ctl.stop bootanim,是不是很有意思呢?

*/

if(memcmp(msg.name,"ctl.",4)==0){

if(check_control_perms(msg.value,cr.uid,cr.gid)){

handle_control_message((char)msg.name+4,(char)msg.value);

}

……

}else{

//检查客户端进程是否有足够的权限。

if(check_perms(msg.name,cr.uid,cr.gid)){

//然后调用property_set设置。

property_set((char)msg.name,(char)msg.value);

}

……

}

break;

default:

break;

}

}


当客户端的权限满足要求时,init就调用property_set进行相关处理,这个函数比较简单,代码如下所示:


[—>property_service.c]

int property_set(const charname,const charvalue)

{

prop_area*pa;

prop_info*pi;

int namelen=strlen(name);

int valuelen=strlen(value);

……

//从属性存储空间中寻找是否已经存在该属性。

pi=(prop_info*)__system_property_find(name);

if(pi!=0){

//如果属性名以ro.开头,则表示是只读的,不能设置,所以直接返回。

if(!strncmp(name,"ro.",3))return-1;

pa=system_property_area

//更新该属性的值。

update_prop_info(pi,value,valuelen);

pa->serial++;

__futex_wake(&pa->serial,INT32_MAX);

}else{

//如果没有找到对应的属性,则认为是增加属性,所以需要新创建一项。注意,Android最多支持

//247项属性,如果目前属性的存储空间中已经有247项,则直接返回。

pa=system_property_area

if(pa->count==PA_COUNT_MAX)return-1;

pi=pa_info_array+pa->count;

pi->serial=(valuelen<<24);

memcpy(pi->name,name,namelen+1);

memcpy(pi->value,value,valuelen+1);

pa->toc[pa->count]=

(namelen<<24)|(((unsigned)pi)-((unsigned)pa));

pa->count++;

pa->serial++;

__futex_wake(&pa->serial,INT32_MAX);

}

//有一些特殊的属性需要特殊处理,这里主要是以net.change开头的属性。

if(strncmp("net.",name,strlen("net."))==0){

if(strcmp("net.change",name)==0){

return 0;

}

property_set("net.change",name);

}else if(persistent_properties_loaded&&

strncmp("persist.",name,strlen("persist."))==0){

//如果属性名以persist.开头,则需要把这些值写到对应的文件中去。

write_persistent_property(name,value);

}

/*

还记得init.rc中的下面这句话吗?

on property:persist.service.adb.enable=1

start adbd

待persist.service.adb.enable属性置为1后,就会执行start adbd这个command,这是通过property_changed函数来完成的,它非常简单,读者可以自己阅读。

*/

property_changed(name,value);

return 0;

}


好,属性服务端的工作已经了解了,下面看客户端是如何设置属性的。

(3)客户端发送请求

客户端通过property_set发送请求,property_set由libcutils库提供,代码如下所示:


[—>properties.c]

int property_set(const charkey,const charvalue)

{

prop_msg msg;

unsigned resp;

……

msg.cmd=PROP_MSG_SETPROP;//设置消息码为PROP_MSG_SETPROP。

strcpy((char*)msg.name,key);

strcpy((char*)msg.value,value);

//发送请求。

return send_prop_msg(&msg);

}

static int send_prop_msg(prop_msg*msg)

{

int s;

int r;

//建立和属性服务器的socket连接。

s=socket_local_client(PROP_SERVICE_NAME,

ANDROID_SOCKET_NAMESPACE_RESERVED,

SOCK_STREAM);

if(s<0)return-1;

//通过socket发送出去。

while((r=send(s,msg,sizeof(prop_msg),0))<0){

if((errno==EINTR)||(errno==EAGAIN))continue;

break;

}

if(r==sizeof(prop_msg)){

r=0;

}else{

r=-1;

}

close(s);

return r;

}


至此,属性服务器就介绍完了。总体来说,还算比较简单。