3. 属性系统架构
Android 属性系统看似只是“键值对读写”,实际上它是一条横跨内核、init、属性服务、共享内存和 SELinux 的完整链路。理解这条链路,才能解释为什么某些属性在 getprop 中可见却无法修改,为什么 ro. 属性重启后依然存在,以及为什么 setprop 会返回 Failed to set property。本节从架构角度拆解属性系统的五个核心对象,并给出整体数据流。
一、五个核心对象与职责
属性系统由以下部分协同工作:
- init 进程:属性系统的“根管理者”。它负责在启动早期创建属性共享内存区、加载属性文件、启动属性服务,并处理
init.rc中的setprop、on property:触发逻辑。 - 属性服务(property service):运行在 init 内部的一个服务端逻辑,接收客户端通过
__system_property_set发来的请求,执行权限与 SELinux 检查,然后写入共享内存。 - 属性文件:持久化存储,常见路径包括
/system/build.prop、/vendor/build.prop、/odm/etc/build.prop、/data/local.prop等。它们决定系统启动时属性的初始值。 - 共享内存:属性真正的“运行时数据库”。所有进程通过
mmap映射同一块匿名共享内存,实现无锁或轻量锁的快速读取。 - SELinux 上下文:为属性名和属性文件打上安全标签,决定“谁可以读、谁可以写、谁可以设置”。它是属性访问控制的关键一层。
二、整体数据流
属性系统的数据流可以概括为“文件 → 共享内存 → 进程读取;进程写入 → 属性服务 → 共享内存”。下图展示了从属性文件加载到进程读写的完整路径:
三、属性文件与加载顺序
属性文件按分区和优先级加载,后加载的同名属性会覆盖先加载的值。常见顺序如下:
| 顺序 | 属性文件路径 | 典型用途 | 是否可写 |
|---|---|---|---|
| 1 | /system/build.prop |
系统基础属性,如 ro.build.version.sdk |
只读 |
| 2 | /vendor/build.prop |
厂商硬件相关属性 | 只读 |
| 3 | /odm/etc/build.prop |
ODM 定制属性 | 只读 |
| 4 | /data/local.prop |
调试用临时属性,仅 eng/userdebug 可用 | 可写 |
提示:只有以
ro. 开头的属性是只读的,一旦设置不可修改;其他属性可以在运行时通过 setprop 修改,但必须通过属性服务和 SELinux 检查。
四、SELinux 上下文如何控制属性访问
SELinux 为每个属性名分配一个安全上下文,例如:
# property_contexts 片段
ro.build.version.sdk u:object_r:build_prop:s0
persist.sys.locale u:object_r:system_prop:s0
vendor.debug.level u:object_r:vendor_debug_prop:s0
当客户端调用 setprop 时,属性服务会检查:
- 调用进程的 SELinux 域是否允许
set该属性上下文; - 属性名是否匹配允许写入的前缀;
- 属性是否已存在且为只读。
如果任一条件不满足,就会返回失败。常见日志如下:
avc: denied { set } for property=persist.sys.locale
scontext=u:r:untrusted_app:s0
tcontext=u:object_r:system_prop:s0
tclass=property_service
五、案例:一次 setprop 失败排查
假设在应用进程中执行:
adb shell setprop persist.sys.locale zh-CN
# 返回:Failed to set property 'persist.sys.locale' to 'zh-CN'
排查路径如下:
- 确认属性名是否以
ro.开头,若是则不可写; - 查看
dmesg或logcat中的avc: denied,确认 SELinux 是否拒绝; - 检查
property_contexts中该属性的上下文,以及调用进程的域是否被允许; - 确认属性服务是否正常运行,即 init 是否已启动属性服务。
误区:很多人认为
setprop 失败是“权限不够”,于是直接 adb root。但即使 root,SELinux 仍可能拒绝写入。必须同时满足 Unix 权限和 SELinux 策略,才能成功设置属性。
六、复盘:属性系统的设计要点
属性系统之所以采用“共享内存 + 服务端写入”的混合架构,核心原因是性能与安全的平衡:
- 读多写少:属性读取极其频繁,共享内存让读取无需 IPC,几乎零开销;
- 写入集中管控:所有写操作经过属性服务,便于统一做权限和 SELinux 检查;
- 启动早期可用:init 在很早阶段就建立共享内存,保证其他进程启动时即可读取属性;
- 可持久化:
persist.属性会写入/data/property/,重启后恢复。
掌握这五个对象及其数据流,就能在遇到属性读写异常时,快速定位是文件加载、共享内存、属性服务还是 SELinux 策略的问题。