Android系统属性添加实战:从原理到SELinux权限配置全解析

Android系统属性添加实战:从原理到SELinux权限配置全解析

1. 项目概述:为什么需要添加系统属性?

在Android开发,特别是系统定制和底层调试中,我们经常会遇到一个需求:需要在系统层面定义一个全局可访问的配置项或状态标志。这个配置项可能是一个简单的开关(比如debug.sys.foo.enable),也可能是一个复杂的字符串(比如ro.product.custom.feature)。如果只是在应用层通过SharedPreferences或者数据库来存储,它的作用域仅限于单个应用,无法被系统服务、其他应用,甚至是init进程、Zygote等早期启动的组件读取。这时,“系统属性”就成为了一个绝佳的解决方案。

你可以把Android的系统属性(System Properties)理解成一个全局的、键值对形式的“公告栏”。任何进程,只要拥有相应的权限,都可以去读取(getprop)或设置(setprop)这个公告栏上的信息。系统本身也大量使用它,例如我们熟悉的ro.build.version.sdk(只读的系统SDK版本)、persist.sys.timezone(持久化的时区设置)等。当你需要实现一个功能,其配置需要被内核、initZygoteSystemServer以及上层的多个App共同感知和遵守时,添加一个新的系统属性几乎是必经之路。

我最近在为一个设备定制功能时,就需要添加一个属性来控制某个硬件模块的低功耗模式开关。这个开关需要在init.rc脚本中根据属性值决定是否加载特定内核模块,在HAL层读取属性来配置硬件寄存器,同时还要在Settings应用中提供一个界面让用户修改它。整个过程走下来,对Android属性系统的运作机制有了更深的体会。这篇文章,我就结合这个实际案例,拆解一下在Android系统中添加一个自定义属性的完整流程、背后的原理以及那些容易踩坑的细节。

2. 系统属性机制深度解析

在动手添加之前,我们必须先搞清楚系统属性是怎么工作的。这能帮你理解后续每一步操作的意义,以及在出问题时知道该从哪里入手排查。

2.1 属性系统的架构与核心组件

Android的属性系统并非一个简单的内存哈希表。它是一个基于共享内存和Socket通信的C/S(客户端/服务器)架构,设计上兼顾了效率、安全性和持久化。

1. 属性服务(property_service这是整个系统的核心,也是“服务器”端。它不是一个独立进程,而是作为init进程的一部分运行。在init启动的早期阶段,它会初始化属性服务。这个服务主要负责几件事:

  • 权限控制:检查客户端进程是否有权限设置某个属性(基于property_contexts文件)。
  • 持久化存储:对于以persist.开头的属性,将其值写入到/data/property/目录下的对应文件中,确保重启后不丢失。
  • 变更通知:当属性值发生变化时,通知所有对此属性感兴趣的客户端进程。

2. 共享内存区域这是属性存储的“公告栏”本身。在系统启动时,init会创建一块共享内存(/dev/__properties__),并将所有属性的初始键值对加载进去。这块内存被映射到所有进程的地址空间。进程读取属性(__system_property_find/__system_property_get)本质上是一个本地内存查找操作,速度极快,这就是为什么getprop命令几乎瞬时返回。

3. 客户端库(libc中的属性访问API)我们常用的property_get/property_set(Java层是SystemProperties.get/set)这些函数,都封装在bionic(Android的C库)中。对于“读”操作,客户端库直接查询共享内存。对于“写”操作,客户端库则通过一个Unix Domain Socket(/dev/socket/property_service)将请求发送给property_service,由它来统一处理。

4. 属性分类与命名规范属性名不是随便起的,它的前缀有明确含义:

  • ro.:只读属性。通常在/system/build.prop中定义,一旦初始化后,任何进程都无法修改。常用于描述系统静态信息,如ro.product.model
  • persist.:持久化属性。设置后会被写入/data分区,重启后保留。常用于用户配置,如persist.sys.locale
  • ctl.:控制属性。用于向init发送命令,启动或停止服务。例如ctl.startservicename
  • vendor.hw.sys.等:这些是常见的命名空间,用于区分不同模块定义的属性,避免冲突。例如vendor.camera.aux.packagelist

注意:自定义属性强烈建议使用vendor.debug.(用于调试)或项目特定的前缀(如com.mycompany.),不要直接使用sys.等系统保留命名空间,以免与未来系统更新产生冲突。

2.2 属性加载流程:从源码到运行时

一个属性是如何从源代码变成运行时可以被getprop查看到的呢?这个过程涉及多个阶段:

  1. 编译时定义:在AOSP源码树的各个*.prop文件(如system/core/rootdir/etc/prop.defaultdevice/厂商/设备/system.prop)中定义属性的默认值。
  2. 构建阶段合并:在编译系统镜像(system.imgvendor.img)时,构建系统会将所有*.prop文件合并,并生成最终的/system/build.prop/vendor/build.prop等文件。
  3. init进程加载:在开机启动的init阶段,property_service会按顺序加载这些*.prop文件,将属性初始化到共享内存中。加载顺序决定了优先级,后加载的文件中的属性值会覆盖先加载的。
  4. 运行时修改:通过setprop或代码property_set进行的修改,会经由property_service处理。如果是persist.属性,还会写文件持久化。

理解这个流程至关重要。比如,你发现在device.mk里添加的属性没生效,那可能是加载顺序被覆盖了,或者你修改的文件根本没有被编译进镜像。

3. 添加自定义属性的完整实操路径

下面,我以添加一个控制自定义硬件模块低功耗模式的属性vendor.power.lpm.enable为例,展示从源码修改到编译验证的完整步骤。假设我们的代码在AOSP的device/mycompany/mydevice/目录下。

3.1 第一步:规划与定义属性

首先,明确需求:

  • 属性名vendor.power.lpm.enable
  • 类型:应为persist.,因为这是一个用户配置,需要重启保留。但初始默认值可以通过ro.vendor.power.lpm.enable来设定。
  • 默认值:默认为1(开启)。
  • 访问权限:需要被init脚本、hal守护进程、system_app(Settings)读写。

3.2 第二步:在设备配置中声明属性默认值

最规范的做法是在设备专属的system.prop文件中定义。这个文件通常位于device/mycompany/mydevice/system.prop。如果不存在,可以创建它。

# device/mycompany/mydevice/system.prop # 定义只读的默认值,用于初始化。注意这里用了`ro.`,确保它只是一个初始默认值源。 ro.vendor.power.lpm.enable=1 # 如果需要,也可以直接定义可写的persist属性初始值,但通常通过ro.来初始化更清晰。 # persist.vendor.power.lpm.enable=1

关键点:为什么这里用ro.?因为ro.vendor.power.lpm.enable这个只读属性会在init早期被加载到共享内存。随后,property_service会检查是否存在同名的persist.属性(即persist.vendor.power.lpm.enable)。如果存在(比如上次设置后持久化保存在/data里),则使用持久化的值;如果不存在,property_service自动将ro.vendor.power.lpm.enable的值复制给persist.vendor.power.lpm.enable,作为其初始值。这是一种常见的模式。

3.3 第三步:配置属性访问权限(property_contexts

不是所有进程都能修改vendor.开头的属性。我们需要在sepolicy(SELinux策略)中配置,但更直接的是在property_contexts文件中声明。这个文件定义了属性名与安全上下文(security context)的映射,property_service根据它来判断谁有权限setprop

找到或创建你设备对应的property_contexts文件。对于vendor属性,通常修改device/mycompany/mydevice/sepolicy/vendor/property_contexts

# device/mycompany/mydevice/sepolicy/vendor/property_contexts # 格式:属性名 u:object_r:属性类型:s0 vendor.power.lpm.enable u:object_r:vendor_power_prop:s0 persist.vendor.power.lpm.enable u:object_r:vendor_power_prop:s0

这里我们创建了一个新的SELinux类型vendor_power_prop。接下来,我们需要为这个类型定义访问规则。

3.4 第四步:定义SELinux策略(*.te文件)

sepolicy/vendor目录下,创建或修改相关的.te(Type Enforcement)文件。

  1. 定义属性类型(如果上一步是新类型):在device/mycompany/mydevice/sepolicy/vendor/attributefile.te中,确保类型被正确声明。更常见的做法是在property.te中关联:

    # device/mycompany/mydevice/sepolicy/vendor/property.te type vendor_power_prop, property_type;

    这行代码声明vendor_power_prop是一个属性类型。

  2. 授予进程访问权限:我们需要允许特定的进程域(domain)来读写这个属性。

    • hal进程:假设你的硬件HAL运行在vendor.power-hal-service这个进程上下文中。
      # device/mycompany/mydevice/sepolicy/vendor/hal_power_default.te (或类似的hal te文件) # 允许hal进程读写(getattr, set)这个属性 allow hal_power_default vendor_power_prop:property_service { set get };
    • init进程init本身需要管理属性,通常已有通用权限。但如果你在init.rc脚本中直接设置该属性,可能需要确认。一般initproperty_type有完全权限。
    • system_app(Settings):Settings应用运行在system_app域。
      # device/mycompany/mydevice/sepolicy/vendor/system_app.te # 允许Settings应用读写这个属性 allow system_app vendor_power_prop:property_service { set get };
    • shell:为了方便调试,我们可能希望adb shell里的setprop命令也能修改它。
      # device/mycompany/mydevice/sepolicy/vendor/shell.te allow shell vendor_power_prop:property_service { set get };

实操心得:SELinux拒绝(avc: denied)是属性设置失败最常见的原因之一。务必使用adb logcat | grep avcdmesg | grep avc来查看拒绝日志,并根据日志提示精确添加allow规则。不要图省事直接设置setenforce 0(关闭SELinux)来绕过,这在生产版本中是严重的安全问题。

3.5 第五步:在代码中访问属性

属性定义好后,就可以在C++、C或Java代码中使用了。

1. C/C++/HAL层(Native Code)

#include <cutils/properties.h> // 头文件 char value[PROPERTY_VALUE_MAX] = {'\0'}; // 读取属性,第二个参数是默认值 property_get("persist.vendor.power.lpm.enable", value, "1"); int lpm_enable = atoi(value); // 将字符串转换为整数 // 设置属性 if (some_condition) { property_set("persist.vendor.power.lpm.enable", "0"); }

注意PROPERTY_VALUE_MAX通常是92(包括结尾的\0),属性值的长度不能超过这个限制。

2. Java层(System API)在Java中,通过android.os.SystemProperties类访问。注意,这个类是@hide的,普通SDK应用无法直接使用。系统应用(如Settings)或使用系统权限的应用可以调用。

import android.os.SystemProperties; // 读取 String value = SystemProperties.get("persist.vendor.power.lpm.enable", "1"); boolean isEnabled = "1".equals(value); // 设置 SystemProperties.set("persist.vendor.power.lpm.enable", "0");

对于非系统应用,如果想安全地暴露属性控制,通常的做法是创建一个系统服务(System Service)来代理属性的读写,并通过Binder接口提供API。

3. 在init.rc脚本中使用你可以在init.${ro.hardware}.rc或你的服务定义的.rc文件中,根据属性值来条件化执行命令或控制服务。

# device/mycompany/mydevice/init.mydevice.rc # 在on boot阶段,根据属性决定是否加载某个内核模块 on boot # 等待属性服务就绪 wait_for_property sys.boot_completed 1 # 读取属性,注意init语言中通过${prop.name}格式读取 setprop vendor.power.lpm.status "unknown" if [ "${persist.vendor.power.lpm.enable}" == "1" ] then insmod /vendor/lib/modules/my_lpm.ko setprop vendor.power.lpm.status "loaded" else setprop vendor.power.lpm.status "disabled" endif # 定义一个服务,其行为受属性控制 service my_lpm_service /vendor/bin/hw/my_lpm_daemon class hal user system group system # 只有当属性为1时才会自动启动 disabled oneshot on property:persist.vendor.power.lpm.enable=1 start my_lpm_service on property:persist.vendor.power.lpm.enable=0 stop my_lpm_service

3.6 第六步:编译与验证

  1. 编译:在AOSP根目录下执行source build/envsetup.shlunch选择你的设备,然后m编译整个系统,或者只编译bootimagesystemimagevendorimage(取决于你修改的文件属于哪个分区)。

    m vendorimage

    确保你的system.propproperty_contexts等文件被正确打包到了对应的镜像中。

  2. 刷机与验证

    • 刷入编译好的镜像。
    • 开机后,首先通过adb shell getprop | grep lpm检查你的属性是否存在,值是否正确。
    • 测试setprop persist.vendor.power.lpm.enable 0,然后再次getprop查看是否修改成功。
    • 重启设备,再次getprop,确认persist.属性值是否被保留。
    • 在你的HAL或应用代码中打日志,确认读写操作能正常执行。
    • 检查SELinux日志,确保没有avc: denied

4. 高级话题与疑难排查

4.1 属性覆盖与优先级问题

有时候你会发现属性值不是你设定的那样,这很可能是被覆盖了。Android属性加载有严格的顺序(大致如下):

  1. /default.prop(内核命令行androidboot.*属性转换而来)
  2. /system/build.prop
  3. /vendor/build.prop
  4. /product/build.prop,/odm/build.prop
  5. 持久化属性文件(/data/property/*)
  6. property_set的运行时设置

后加载的会覆盖先加载的。如果你的属性在vendor/build.prop中被定义为ro.vendor.xxx=1,但在system/build.prop的某个后期加载的片段里又被定义为ro.vendor.xxx=0,那么最终值就是0。排查时,可以逐一检查这些文件。

4.2 属性名长度与值长度限制

  • 属性名长度:理论上很长,但实践中建议保持简洁明了。
  • 属性值长度绝对不能超过PROPERTY_VALUE_MAX - 1个字符。这个宏在bionic中定义,通常是92 - 1 = 91个有效字符。property_set不会截断超长的字符串,而是静默失败!这是非常隐蔽的坑。在设置长字符串(比如JSON配置)时,务必先检查长度。

4.3 属性变更监听

某些场景下,进程需要监听属性的变化。在Native代码中,可以使用property_set_callback(已废弃)或更底层的__system_property_wait/__system_property_read_callbackAPI。在Java中,SystemProperties类提供了addChangeCallback方法(也是@hide的)。更常见的模式是,在init.rc中使用on property:触发器来执行脚本命令,或者在自己的守护进程中轮询属性值。

4.4 调试命令与技巧

  • adb shell getprop:列出所有属性。
  • adb shell getprop [key]:获取特定属性值。
  • adb shell setprop [key] [value]:设置属性值(需权限)。
  • adb shell watchprops:实时监视属性变化(需要系统支持)。
  • adb logcat -b events | grep property:查看属性变更的事件日志。
  • adb shell ls -lZ /data/property/:查看持久化属性文件及其SELinux上下文。
  • 检查SELinuxadb shell su root dmesg | grep avcadb logcat | grep avc

5. 常见问题与避坑指南实录

在实际操作中,我遇到了不少问题,这里总结几个典型的:

问题一:setprop成功了,但重启后值又变回默认值了。

  • 原因:你设置的属性名不是以persist.开头的。只有persist.属性才会被自动保存到/data/property/
  • 解决:确保你要持久化的属性名正确。如果你想修改一个ro.开头的属性,那是徒劳的,property_service会拒绝。

问题二:在Java应用里调用SystemProperties.set,毫无反应,也不报错。

  • 原因A:你的应用没有android.permission.WRITE_SECURE_SETTINGS权限(对于系统属性,这个权限有时是必要的,但并非对所有属性),或者更关键的是,SELinux策略不允许。
  • 原因B:你尝试设置的属性名是ro.开头的。
  • 排查
    1. 检查logcat是否有avc: denied日志。
    2. 检查应用是否被授予了正确的权限(在AndroidManifest.xml中声明,并且签名匹配)。
    3. 尝试在adb shell下用setprop命令(通常有shellroot权限)测试,如果命令可以但应用不行,基本就是权限或SELinux问题。

问题三:属性在getprop里能看到,但在我的C++代码里property_get返回空字符串或默认值。

  • 原因:极有可能是属性名拼写错误,或者前后有空格。属性名是大小写敏感的。
  • 排查:在代码里把尝试读取的属性名打印到日志里,和getprop列表里的名字仔细比对。我曾经因为把vendor.power.lpm.enable错写成vendor.power.lpm_enable(下划线 vs 点)而调试了半天。

问题四:编译时发现property_contexts*.te文件修改不生效。

  • 原因:AOSP的构建系统对sepolicy有缓存和继承机制。特别是如果你在device/目录下修改,但项目可能从另一个commonbasesepolicy继承。
  • 解决
    1. 确保你的修改在正确的sepolicy目录下(vendor还是system?Android 8.0以后推荐放在vendor)。
    2. 执行make cleanrm -rf out/target/product/设备名/obj/ETC/sepolicy_*.intermediates等清理操作,然后重新编译。
    3. 检查out/target/product/设备名/vendor/etc/selinux/vendor_sepolicy.cil等最终生成的策略文件,看你的规则是否被包含进去。

问题五:添加了新属性类型,编译报错“未声明的类型”。

  • 原因:在property.te中声明了type my_prop, property_type;,但可能没有在attributes文件中将其关联为attribute,或者在其他.te文件中引用时拼写错误。
  • 解决:确保类型声明语句语法正确,并且所有引用该类型的地方(allow规则)拼写一致。可以搜索AOSP中其他属性类型的定义作为参考。

添加系统属性是一个连接Android系统层与应用层的桥梁性工作,它要求你对Android的启动流程、权限管理和SELinux有基本的了解。整个过程像是一场精密的布线:定义源头(system.prop)、铺设管道并加锁(property_contextssepolicy)、最后在各个房间安装开关和指示灯(代码中读写)。只要理清了这个脉络,遵循规范的步骤,再结合logcatdmesg进行调试,就能稳稳地让这个全局“公告栏”为你所用。