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 上下文:为属性名和属性文件打上安全标签,决定“谁可以读、谁可以写、谁可以设置”。它是属性访问控制的关键一层。

二、整体数据流

属性系统的数据流可以概括为“文件 → 共享内存 → 进程读取;进程写入 → 属性服务 → 共享内存”。下图展示了从属性文件加载到进程读写的完整路径:

Android 属性系统整体数据流 属性文件 /system/build.prop /vendor/build.prop /odm/etc/build.prop /data/local.prop init 进程 解析 build.prop 创建共享内存 启动属性服务 处理 on property 触发 属性共享内存 __system_property_area__ 键值对 + 序列号 + 锁 所有进程 mmap 映射 读取无需系统调用 加载 写入初始值 客户端进程 getprop / setprop __system_property_get __system_property_set 属性服务(init 内) 权限检查 + SELinux 检查 合法则写入共享内存 非法则返回失败 直接读取 set 请求 写入 SELinux 上下文 property_contexts file_contexts 控制读写权限 策略约束 读路径:进程 mmap 共享内存 → 直接读取键值,无需 IPC,性能极高。 写路径:进程通过 socket 请求属性服务 → 权限与 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 时,属性服务会检查:

  1. 调用进程的 SELinux 域是否允许 set 该属性上下文;
  2. 属性名是否匹配允许写入的前缀;
  3. 属性是否已存在且为只读。

如果任一条件不满足,就会返回失败。常见日志如下:

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 策略的问题。