Android Property机制

概述

Android系统(本文以Android 4.4为准)的属性(Property)机制有点儿类似Windows系统的注册表,其中的每个属性被组织成简单的键值对(key/value)供外界使用。

​ 我们可以通过在adb shell里敲入getprop命令来获取当前系统的所有属性内容,而且,我们还可以敲入类似“getprop 属性名”的命令来获取特定属性的值。另外,设置属性值的方法也很简单,只需敲入“setprop 属性名 新值”命令即可。

可是问题在于我们不想只认识到这个层次,我们希望了解更多一些Property机制的运作机理,而这才是本文关心的重点。

​ 说白了,Property机制的运作机理可以汇总成以下几句话:

  1. 系统一启动就会从若干属性脚本文件中加载属性内容;
  2. 系统中的所有属性(key/value)会存入同一块共享内存中;
  3. 系统中的各个进程会将这块共享内存映射到自己的内存空间,这样就可以直接读取属性内容了;
  4. 系统中只有一个实体可以设置、修改属性值,它就是属性服务(Property Service);
  5. 不同进程只可以通过socket方式,向属性服务发出修改属性值的请求,而不能直接修改属性值;
  6. 共享内存中的键值内容会以一种字典树的形式进行组织。

Property机制的示意图如下:

Property Service

init进程里的Property Service

Property Service实体其实是在init进程里启动的。我们知道,init是Linux系统中用户空间的第一个进程。它负责创建系统中最关键的几个子进程,比如zygote等等。在本节中,我们主要关心init进程是如何启动Property Service的。

我们查看core/init/Init.c文件,可以看到init进程的main()函数,它里面和property相关的关键动作有:

  1. 间接调用system_property_area_init():打开属性共享内存,并记入system_property_area变量;
  2. 间接调用init_workspace():只读打开属性共享内存,并记入环境变量;
  3. 根据init.rc,异步激发property_service_init_action(),该函数中会:
    • 加载若干属性文本文件,将具体属性、属性值记入属性共享内存;
    • 创建并监听socket;
  4. 根据init.rc,异步激发queue_property_triggers_action(),将刚刚加载的属性对应的激发动作,推入action列表。

main()中的调用关系如下:

初始化属性共享内存

​ 我们可以看到,在init进程的main()函数里,辗转打开了一个内存文件“/dev/properties”,并把它设定为128KB大小,接着调用mmap()将这块内存映射到init进程空间了。这个内存的首地址被记录在system_property_area全局变量里,以后每添加或修改一个属性,都会基于这个system_property_area变量来计算位置。

​ 初始化属性内存块时,为什么要两次open那个/dev/properties文件呢?我想原因是这样的:第一次open的句柄,最终是给属性服务自己用的,所以需要有读写权限;而第二次open的句柄,会被记入pa_workspace.fd,并在合适时机添加进环境变量,供其他进程使用,因此只能具有读取权限。

第一次open时,执行的代码如下:

1
fd = open(property_filename, O_RDWR | O_CREAT | O_NOFOLLOW | O_CLOEXEC | O_EXCL, 0444);

传给open()的参数标识里指明了O_RDWR,表示用“读写方式”打开文件。另外O_NOFOLLOW标识主要是为了防止我们打开“符号链接”,不过我们知道,properties文件并不是符号链接,所以当然可以成功open。O_CLOEXEC标识是为了保证一种独占性,也就是说当init进程打开这个文件时,此时就算其他进程也open这个文件,也会在调用exec执行新程序时自动关闭该文件句柄。O_EXCL标识和O_CREATE标识配合起来,表示如果文件不存在,则创建之,而如果文件已经存在,那么open就会失败。第一次open动作后,会给system_property_area赋值,然后程序会立即close刚打开的句柄。

第二次open动作发生在接下来的init_workspace()函数里。此时会再一次打开properties文件,这次却是以只读模式打开的:

1
int fd = open(PROP_FILENAME, O_RDONLY | O_NOFOLLOW);

打开的句柄记录在pa_workspace.fd处,以后每当init进程执行socket命令,并调用service_start()时,会执行类似下面的句子:

1
2
3
get_property_workspace(&fd, &sz);   // 读取pa_workspace.fd 
sprintf(tmp, "%d,%d", dup(fd), sz);
add_environment("ANDROID_PROPERTY_WORKSPACE", tmp);

说白了就是把pa_workspace.fd的句柄记入一个名叫“ANDROID_PROPERTY_WORKSPACE”的环境变量去。

system/core/init/Init.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/* add_environment - add "key=value" to the current environment */
int add_environment(const char *key, const char *val)
{
int n;

for (n = 0; n < 31; n++) {
if (!ENV[n]) {
size_t len = strlen(key) + strlen(val) + 2;
char *entry = malloc(len);
snprintf(entry, len, "%s=%s", key, val);
ENV[n] = entry;
return 0;
}
}

return 1;
}

这个环境变量在日后有可能被其他进程拿来用,从而将属性内存区映射到自己的内存空间去,这个后文会细说。

​ 接下来,main()函数在设置好属性内存块之后,会调用queue_builtin_action()函数向内部的action_list列表添加action节点。关于这部分的详情,可参考其他讲述Android启动机制的文档,这里不再赘述。我们只需知道,后续,系统会在合适时机回调“由queue_builtin_action()的参数”所指定的property_service_init_action()函数就可以了。

初始化属性服务

property_service_init_action()函数只是在简单调用start_property_service()而已,后者的代码如下:

core/init/Property_service.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void start_property_service(void)
{
int fd;

load_properties_from_file(PROP_PATH_SYSTEM_BUILD);
load_properties_from_file(PROP_PATH_SYSTEM_DEFAULT);

/* Read vendor-specific property runtime overrides. */
vendor_load_properties();

load_override_properties();
/* Read persistent properties after all default values have been loaded. */
load_persistent_properties();

fd = create_socket(PROP_SERVICE_NAME, SOCK_STREAM, 0666, 0, 0);
if(fd < 0) return;
fcntl(fd, F_SETFD, FD_CLOEXEC);
fcntl(fd, F_SETFL, O_NONBLOCK);

listen(fd, 8);
property_set_fd = fd;
}

其主要动作无非是加载若干属性文件,然后创建并监听一个socket接口。

加载属性文本文件

start_property_service()函数首先会调用load_properties_from_file()函数,尝试加载一些属性脚本文件,并将其中的内容写入属性内存块里。从代码里可以看到,主要加载的文件有:

  • /system/build.prop
  • /system/default.prop(该文件不一定存在)
  • /data/local.prop
  • /data/property目录里的若干脚本

处理属性设置命令

我们还是先回到前文init进程处理属性设置动作的地方:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void handle_property_set_fd()
{
. . . . . .
if(memcmp(msg.name,"ctl.",4) == 0) {
. . . . . .
} else {
if (check_perms(msg.name, cr.uid, cr.gid, source_ctx)) {
property_set((char*) msg.name, (char*) msg.value);
} else {
ERROR("sys_prop: permission denied uid:%d name:%s\n",
cr.uid, msg.name);
}
. . . . . .
close(s);
}
. . . . . .
break;
. . . . . .
}
}
check_perms()

要设置普通属性,也是要具有一定权限哩。请看上面的check_perms()一句。该函数的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
static int check_perms(const char *name, unsigned int uid, unsigned int gid, char *sctx)
{
int i;
unsigned int app_id;

if(!strncmp(name, "ro.", 3))
name +=3;

if (uid == 0)
return check_mac_perms(name, sctx);

app_id = multiuser_get_app_id(uid);
if (app_id == AID_BLUETOOTH) {
uid = app_id;
}

for (i = 0; property_perms[i].prefix; i++) {
if (strncmp(property_perms[i].prefix, name,
strlen(property_perms[i].prefix)) == 0) {
if ((uid && property_perms[i].uid == uid) ||
(gid && property_perms[i].gid == gid)) {

return check_mac_perms(name, sctx);
}
}
}

return 0;
}

主要也是在查表,property_perms表的定义如下:

这其实很容易理解,比如要设置“sys.”打头的系统属性,进程的uid就必须是AID_SYSTEM,否则阿猫阿狗都能设置系统属性,岂不糟糕。

property_set()

权限检查通过之后,就可以真正设置属性了。在前文“概述”一节中,我们已经说过,只有Property Service(即init进程)可以写入属性值,而普通进程最多只能通过socket向Property Service发出设置新属性值的请求,最终还得靠Property Service来写。那么我们就来看看Property Service里具体是怎么写的。

总体说来,property_set()会做如下工作:

  1. 判断待设置的属性名是否合法;
  2. 尽力从“属性共享内存”中找到匹配的prop_info节点,如果能找到,就调用system_property_update(),当然如果属性是以“ro.”打头的,说明这是个只读属性,此时不会update的;如果找不到,则调用system_property_add()添加属性节点。
  3. 在update或add动作之后,还需要做一些善后处理。比如,如果改动的是“net.”开头的属性,那么需要重新设置一下net.change属性,属性值为刚刚设置的属性名字。
  4. 如果要设置persist属性的话,只有在系统将所有的默认persist属性都加载完毕后,才能设置成功。persist属性应该是那种会存入可持久化文件的属性,这样,系统在下次启动后,可以将该属性的初始值设置为系统上次关闭时的值。
  5. 如果将“selinux.reload_policy”属性设为“1”了,那么会进一步调用selinux_reload_policy()。这个意味着要重新加载SEAndroid策略。
  6. 最后还需调用property_changed()函数,其内部会执行init.rc中指定的那些和property同名的action。

原文:https://my.oschina.net/youranhongcha/blog/389640