概述
Android系统(本文以Android 4.4为准)的属性(Property)机制有点儿类似Windows系统的注册表,其中的每个属性被组织成简单的键值对(key/value)供外界使用。
我们可以通过在adb shell里敲入getprop命令来获取当前系统的所有属性内容,而且,我们还可以敲入类似“getprop 属性名”的命令来获取特定属性的值。另外,设置属性值的方法也很简单,只需敲入“setprop 属性名 新值”命令即可。
可是问题在于我们不想只认识到这个层次,我们希望了解更多一些Property机制的运作机理,而这才是本文关心的重点。
说白了,Property机制的运作机理可以汇总成以下几句话:
- 系统一启动就会从若干属性脚本文件中加载属性内容;
- 系统中的所有属性(key/value)会存入同一块共享内存中;
- 系统中的各个进程会将这块共享内存映射到自己的内存空间,这样就可以直接读取属性内容了;
- 系统中只有一个实体可以设置、修改属性值,它就是属性服务(Property Service);
- 不同进程只可以通过socket方式,向属性服务发出修改属性值的请求,而不能直接修改属性值;
- 共享内存中的键值内容会以一种字典树的形式进行组织。
Property机制的示意图如下:

Property Service
init进程里的Property Service
Property Service实体其实是在init进程里启动的。我们知道,init是Linux系统中用户空间的第一个进程。它负责创建系统中最关键的几个子进程,比如zygote等等。在本节中,我们主要关心init进程是如何启动Property Service的。
我们查看core/init/Init.c文件,可以看到init进程的main()函数,它里面和property相关的关键动作有:
- 间接调用system_property_area_init():打开属性共享内存,并记入system_property_area变量;
- 间接调用init_workspace():只读打开属性共享内存,并记入环境变量;
- 根据init.rc,异步激发property_service_init_action(),该函数中会:
- 加载若干属性文本文件,将具体属性、属性值记入属性共享内存;
- 创建并监听socket;
- 根据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 | get_property_workspace(&fd, &sz); // 读取pa_workspace.fd |
说白了就是把pa_workspace.fd的句柄记入一个名叫“ANDROID_PROPERTY_WORKSPACE”的环境变量去。
system/core/init/Init.c
1 | /* add_environment - add "key=value" to the current environment */ |
这个环境变量在日后有可能被其他进程拿来用,从而将属性内存区映射到自己的内存空间去,这个后文会细说。
接下来,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 | void start_property_service(void) |
其主要动作无非是加载若干属性文件,然后创建并监听一个socket接口。
加载属性文本文件
start_property_service()函数首先会调用load_properties_from_file()函数,尝试加载一些属性脚本文件,并将其中的内容写入属性内存块里。从代码里可以看到,主要加载的文件有:
- /system/build.prop
- /system/default.prop(该文件不一定存在)
- /data/local.prop
- /data/property目录里的若干脚本
处理属性设置命令
我们还是先回到前文init进程处理属性设置动作的地方:
1 | void handle_property_set_fd() |
check_perms()
要设置普通属性,也是要具有一定权限哩。请看上面的check_perms()一句。该函数的代码如下:
1 | static int check_perms(const char *name, unsigned int uid, unsigned int gid, char *sctx) |
主要也是在查表,property_perms表的定义如下:

这其实很容易理解,比如要设置“sys.”打头的系统属性,进程的uid就必须是AID_SYSTEM,否则阿猫阿狗都能设置系统属性,岂不糟糕。
property_set()
权限检查通过之后,就可以真正设置属性了。在前文“概述”一节中,我们已经说过,只有Property Service(即init进程)可以写入属性值,而普通进程最多只能通过socket向Property Service发出设置新属性值的请求,最终还得靠Property Service来写。那么我们就来看看Property Service里具体是怎么写的。
总体说来,property_set()会做如下工作:
- 判断待设置的属性名是否合法;
- 尽力从“属性共享内存”中找到匹配的prop_info节点,如果能找到,就调用system_property_update(),当然如果属性是以“ro.”打头的,说明这是个只读属性,此时不会update的;如果找不到,则调用system_property_add()添加属性节点。
- 在update或add动作之后,还需要做一些善后处理。比如,如果改动的是“net.”开头的属性,那么需要重新设置一下net.change属性,属性值为刚刚设置的属性名字。
- 如果要设置persist属性的话,只有在系统将所有的默认persist属性都加载完毕后,才能设置成功。persist属性应该是那种会存入可持久化文件的属性,这样,系统在下次启动后,可以将该属性的初始值设置为系统上次关闭时的值。
- 如果将“selinux.reload_policy”属性设为“1”了,那么会进一步调用selinux_reload_policy()。这个意味着要重新加载SEAndroid策略。
- 最后还需调用property_changed()函数,其内部会执行init.rc中指定的那些和property同名的action。