Android 属性服务(Property Service)是 Android 系统中一个极其重要但又容易被人忽视的底层基础设施。从系统启动到应用运行,从 getprop / setprop 命令行工具到 SystemProperties Java API,属性系统承载了全局配置、进程通信和系统状态管理的职责。本文将基于 Android 11 (API 30) 的 AOSP 源码,深入剖析属性服务的架构演进、实现原理和使用场景。
一、属性服务概述
1.1 什么是 Android 属性系统
Android 属性系统是一个全局的、基于键值对的系统配置存储和通信机制。它类似于 Linux 的 /proc 文件系统或 Windows 的注册表,但设计上更轻量,专门针对嵌入式设备优化。属性值在整个系统范围内可见,可以被任何进程读取(受 SELinux 策略限制),但只能由 init 进程写入。
核心特点:
- 全局共享:所有进程可读取同一份属性数据(通过共享内存 mmap)
- 写入集中化:只有 init 进程的 property_service 能实际修改属性值,其他进程通过 socket 向 init 发送写入请求
- 不可持久化(默认):普通属性在重启后丢失,只有
persist.*前缀的属性会写入闪存 - 变更通知:属性值变化时,可通过 epoll 机制通知监听者
1.2 属性系统在系统启动中的角色
在 init 进程的第二阶段(SecondStageMain)中,属性服务是第一批被初始化的核心组件:
// system/core/init/init.cpp |
二、旧版属性系统架构(Android 9 及之前)
2.1 架构概览
在 Android 9 及更早版本中,属性系统采用经典的”init 集中写”模式:
┌──────────────┐ setprop ┌──────────────┐ |
关键源码路径:
system/core/init/property_service.cpp— init 端属性服务system/core/libcutils/properties.cpp— libcutils 的属性读写 APIbionic/libc/bionic/system_property_api.cpp— bionic C API(__system_property_get/set)frameworks/base/core/java/android/os/SystemProperties.java— Java 层 API
2.2 核心数据结构
旧版属性系统使用 trie(前缀树)来组织属性键名,但是一个简化的二叉查找树实现:
// bionic/libc/include/sys/_system_properties.h (旧版) |
整个属性区域的内存布局:
[prop_area header] |
2.3 属性读取流程 (getprop)
由于属性区域是只读共享内存,读取过程不需要与 init 进程通信:
// bionic/libc/bionic/system_property_api.cpp |
2.4 属性写入流程 (setprop)
写入必须通过 init 进程中转:
// system/core/libcutils/properties.cpp |
init 进程端接收并处理写入请求:
// system/core/init/property_service.cpp (旧版) |
三、新一代属性系统(Android 10+ Property ABI)
3.1 为什么要重新设计
旧版属性系统存在几个固有问题:
- 安全粒度不足:所有进程可以读取同一块共享内存中的所有属性,只能通过 SELinux 控制 set,无法控制 get
- 写时竞争:init 写入时,读进程可能读到不完整的数据(虽然有序列号重试,但本质是 workaround)
- 可扩展性:只有一块共享内存,无法按安全域隔离
- 性能问题:序列号自旋在极端并发场景下效率低
Google 在 Android 10 中重写了属性系统,引入了 property_contexts 文件来控制每个属性的读/写权限,并将属性按命名空间进行隔离。
3.2 新版架构
┌──────────────────────────────────────────────────────┐ |
Android 10+ 将属性区域拆分为多个独立的文件:
/dev/__properties__/property_info— 属性元信息(名称到区域的映射)/dev/__properties__/system/build.prop映射的属性 → system 区域/dev/__properties__/vendor/build.prop映射的属性 → vendor 区域- 等等
3.3 property_contexts 文件
property_contexts 是 SELinux 策略的一部分,定义了哪些属性属于哪个安全标签,从而控制不同进程对属性的访问权限。
# system/sepolicy/private/property_contexts |
格式说明:
ro.boot.— 前缀匹配(以 ro.boot. 开头的所有属性)exact string— 精确字符串匹配类型u:object_r:bootloader_prop:s0— SELinux 安全上下文标签
进程要读取一个属性时,需要具有对应 SELinux type 的访问权限。例如,只有具有 bootloader_prop 读权限的进程才能读 ro.boot.* 属性。
3.4 按命名空间隔离——Treble 兼容性
VTS (Vendor Test Suite) 要求 vendor 和 system 的接口独立演进。属性隔离是这一要求的具体体现:
// system/core/property_service/libpropertyinfoserializer/PropertyInfo.cpp |
当一个进程尝试读取某个属性时,属性系统首先查找 property_info 找到该属性所在的 mmap 文件,然后仅从该文件中读取数据。系统进程通常映射 system 和 vendor 两份区域,而 vendor 进程可能只映射 vendor 区域。
3.5 新版本中的数据结构变化
Android 10+ 重新设计了属性内存布局,使用更高效的哈希表而不是二叉查找树:
// system/core/property_service/libpropertyinfoparser/include/property_info_parser/property_info_parser.h |
属性读取 API 的位置也发生了迁移:
- 旧版:
bionic/libc/bionic/system_property_api.cpp(在 bionic 中) - 新版:
system/core/libsystem/libsystem_properties.cpp(在 libsystem 中,向下兼容封装)
四、属性的生命周期
4.1 属性来源
系统中的属性有多种来源,按加载顺序排列:
第一阶段(init 第一阶段,挂载文件系统阶段):
// system/core/init/init_first_stage.cpp |
第二阶段(init 第二阶段,属性服务初始化后):
// system/core/init/init.cpp |
property_load_boot_defaults() 会加载多个属性文件:
// system/core/init/property_service.cpp |
运行阶段(应用可以通过 API 设置属性):
// Java 层 |
// Native 层 |
4.2 属性文件格式
build.prop 文件格式很简单,每行一个键值对:
# /system/build.prop (示例) |
解析实现:
// system/core/init/property_service.cpp |
4.3 持久化属性 (persist.*)
以 persist. 开头的属性会在写入时被立即保存到 flash 存储中,重启后仍然有效:
// system/core/init/persistent_properties.cpp |
持久化属性文件存储在 /data/property/ 目录下(Android 9+)。每个属性一个独立文件,文件名为属性名,内容为属性值。
五、属性变化通知与触发器机制
5.1 init 端的 epoll 通知
当属性值被修改时,init 进程会向 epoll fd 写入通知,唤醒正在等待属性变化的代码:
// system/core/init/property_service.cpp |
5.2 属性触发器在 init.rc 中的使用
init.rc 中大量使用 on property:<name>=<value> 触发器:
# system/core/rootdir/init.rc |
init 的主事件循环会在 epoll_wait 返回后检查是否有属性触发器被满足:
// system/core/init/action_manager.cpp |
六、ctl.* 控制属性:驱动服务启动/停止
6.1 ctl.start / ctl.stop 机制
Android 属性系统中有一类特殊的属性,前缀为 ctl.(control),用于通过属性机制间接控制服务的启动和停止:
# 通过 setprop 启动 zygote_secondary 服务 |
注意:ctl.start 和 ctl.stop 不与普通的 ro.*、sys.* 属性一样存储在共享内存中。它们是非持久化的控制消息。
6.2 init 端的处理
// system/core/init/property_service.cpp |
6.3 控制属性与 SELinux 权限
控制属性是系统的薄弱点,因为它可以直接启动/停止关键服务。Android 通过 SELinux 严格控制哪些进程可以设置 ctl.* 属性:
# system/sepolicy/public/init.te |
七、Java 层 SystemProperties API
7.1 SystemProperties 类
Android 框架层通过 SystemProperties 类提供 Java 语言的属性访问接口:
// frameworks/base/core/java/android/os/SystemProperties.java |
7.2 JNI 层实现
// frameworks/base/core/jni/android_os_SystemProperties.cpp |
注意 PROP_VALUE_MAX = 92 这个限制:Android 属性值的最大长度为 91 个字符(加 null 终止符共 92 字节)。这意味着属性不适合存储大数据,只适合存储短配置值。
八、重要系统属性详解
8.1 ro.* (Read-Only)属性
以 ro. 开头的属性一旦设置就不能再修改。这些属性通常在系统启动早期(init 第一阶段)由 bootloader 或内核命令行设置,或者从 build.prop 文件加载:
ro.boot.hardware # 硬件平台名 |
read-only 属性的实现:
// system/core/init/property_service.cpp |
8.2 persist.* 属性
如前面所述,持久化属性保存在 /data/property/ 目录中。常用场景:
persist.sys.locale # 系统语言设置 |
8.3 sys.* 属性
sys. 前缀的属性表示系统运行时状态,不是只读也不是持久化,可以被系统服务修改。常见的有:
sys.boot_completed # 启动完成标志 (值为 "1") |
8.4 debug.* 属性
用于调试和开发,通常在 eng/userdebug 构建中可用:
debug.atrace.tags.enableflags # atrace 启用标志 |
九、SELinux 与属性安全
9.1 属性权限控制模型
SELinux 通过以下机制控制属性访问:
- prop_contexts:定义每个属性的 SELinux type
- property.te:定义属性的 SELinux type
- 各 .te 文件:定义各域对属性的 set/get 权限
# system/sepolicy/public/property.te |
9.2 属性写入的 SELinux 检查流程
// system/core/init/property_service.cpp |
十、内核参数传递与 First Stage 属性
10.1 内核命令行参数的传递
Android 系统通过内核命令行向 init 传递初始属性:
# 来自 bootloader 的典型内核命令行 |
在 init 第一阶段,这些参数被解析为早期的 ro.boot.* 属性:
// system/core/init/init_first_stage.cpp |
10.2 DT (Device Tree) 属性
除了内核命令行,Android 也支持从 Device Tree 中读取属性:
// system/core/init/init.cpp |
十一、核心面试题
Q1:Android 属性系统为什么采用”集中写、任意读”的架构,而不是像传统文件系统那样使用文件锁?
答:这种设计有几个考量:(1) 性能:读取路径走 mmap 共享内存,零拷贝、零系统调用、零上下文切换,极其高效;(2) 一致性:所有写入都由唯一的 init 进程串行处理,避免了锁竞争、写写冲突和死锁问题;(3) 安全性:集中写入使得权限检查集中在一点,init 可以在写入前统一执行 SELinux 检查;(4) 变更通知:集中写入使得属性变更时 init 可以统一通知所有监听者(如 init.rc 触发器)。
Q2:property_set() 与 __system_property_set() 有什么关系?为什么有两个 API?
答:property_set()(在 libcutils 中)是高层 API,它向 init 的 property socket 发送消息,init 端收到后执行 SELinux 检查、更新共享内存、处理持久化等。__system_property_set()(在 bionic 中)是底层 API,它直接写入共享内存但不经过 init 的权限检查。__system_property_set() 只能被 init 进程自身调用(init 已经是特权进程),其他进程调用会因为没有 SELinux 写权限而失败。实际代码中,除 init 之外的所有进程都应该使用 property_set()。
Q3:Android 10 引入的 Property ABI 改变对 Treble 兼容性有什么帮助?
答:Property ABI 改革将属性按分区隔离(system/vendor/product),确保 vendor 进程只能访问 vendor 分区定义的属性,不能访问 system 分区的内部属性。这解决了 Treble 之前的一个核心痛点——vendor 实现可能依赖未文档化的 system 属性,当 system 升级时这些属性消失或变化导致兼容性断裂。现在通过 property_contexts 和分区隔离,VTS 可以自动验证 vendor 实现只使用了已声明为 public 的属性接口。
Q4:PROP_VALUE_MAX 为什么是 92 字节?如果属性值需要存更长的数据怎么办?
答:92 字节是历史遗留的设计限制,源自 Android 早期共享内存固定页大小(128 字节结构体,部分用于元数据,剩余 92 给值)。这个限制一直延续到 Android 10+ 新版本。如果需要存储更长的数据,有几种替代方案:(1) 使用 persist.* 属性 + 文件存储组合(属性中存文件路径);(2) 使用 SystemConfig / SettingsProvider 数据库;(3) 使用自定义 native 服务通过 Binder 通信。属性系统的设计初衷是存储简短的全局配置项,长数据不应该放在语义简单的键值对系统中。
Q5:当系统同时存在 ro.boot.hardware(命令行设置)和 DT 中相同的属性时,哪个优先级更高?为什么?
答:DT 中设置的属性优先于命令行属性。从安全角度考虑,bootloader 和 DT 是片上系统中最受信任的组件(在 TCB 中),而内核命令行可能被攻击者通过修改 boot image 轻易篡改。因此 Android init 采用了 DT 优先级高于内核命令行的策略。实际上这个检查是通过 SELinux 的 neverallow 规则强制执行的——一旦一个 ro.boot.* 属性被设置,任何后续的写入尝试都会被 IsReadOnlyProperty() 拒绝,而加载的顺序是 DT 优先于命令行解析(process_kernel_dt() 在 process_kernel_cmdline() 之前调用)。
AOSP 核心路径参考:
system/core/init/property_service.cpp— init 端属性服务(写入处理、属性文件加载)system/core/init/property_service.h— property_init / start_property_service 声明system/core/init/persistent_properties.cpp— 持久化属性读写system/core/init/init.cpp— init 主函数(属性服务初始化流程)system/core/property_service/libpropertyinfoserializer/— Android 10+ 属性信息序列化system/core/libcutils/properties.cpp— property_get / property_set APIbionic/libc/bionic/system_property_api.cpp— __system_property_get / __system_property_setframeworks/base/core/java/android/os/SystemProperties.java— Java 层 APIframeworks/base/core/jni/android_os_SystemProperties.cpp— JNI 实现system/sepolicy/private/property_contexts— 属性的 SELinux 上下文定义system/sepolicy/public/property.te— 属性 type 声明





