尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

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持久化的时区设置等。当你需要实现一个功能其配置需要被内核、init、Zygote、SystemServer以及上层的多个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_setJava层是SystemProperties.get/set这些函数都封装在bionicAndroid的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查看到的呢这个过程涉及多个阶段编译时定义在AOSP源码树的各个*.prop文件如system/core/rootdir/etc/prop.default、device/厂商/设备/system.prop中定义属性的默认值。构建阶段合并在编译系统镜像system.img、vendor.img时构建系统会将所有*.prop文件合并并生成最终的/system/build.prop和/vendor/build.prop等文件。init进程加载在开机启动的init阶段property_service会按顺序加载这些*.prop文件将属性初始化到共享内存中。加载顺序决定了优先级后加载的文件中的属性值会覆盖先加载的。运行时修改通过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_appSettings读写。3.2 第二步在设备配置中声明属性默认值最规范的做法是在设备专属的system.prop文件中定义。这个文件通常位于device/mycompany/mydevice/system.prop。如果不存在可以创建它。# device/mycompany/mydevice/system.prop # 定义只读的默认值用于初始化。注意这里用了ro.确保它只是一个初始默认值源。 ro.vendor.power.lpm.enable1 # 如果需要也可以直接定义可写的persist属性初始值但通常通过ro.来初始化更清晰。 # persist.vendor.power.lpm.enable1关键点为什么这里用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.开头的属性。我们需要在sepolicySELinux策略中配置但更直接的是在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目录下创建或修改相关的.teType Enforcement文件。定义属性类型如果上一步是新类型在device/mycompany/mydevice/sepolicy/vendor/attribute或file.te中确保类型被正确声明。更常见的做法是在property.te中关联# device/mycompany/mydevice/sepolicy/vendor/property.te type vendor_power_prop, property_type;这行代码声明vendor_power_prop是一个属性类型。授予进程访问权限我们需要允许特定的进程域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脚本中直接设置该属性可能需要确认。一般init对property_type有完全权限。system_appSettingsSettings应用运行在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 avc或dmesg | 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.enable1 start my_lpm_service on property:persist.vendor.power.lpm.enable0 stop my_lpm_service3.6 第六步编译与验证编译在AOSP根目录下执行source build/envsetup.sh、lunch选择你的设备然后m编译整个系统或者只编译bootimage、systemimage、vendorimage取决于你修改的文件属于哪个分区。m vendorimage确保你的system.prop和property_contexts等文件被正确打包到了对应的镜像中。刷机与验证刷入编译好的镜像。开机后首先通过adb shell getprop | grep lpm检查你的属性是否存在值是否正确。测试setprop persist.vendor.power.lpm.enable 0然后再次getprop查看是否修改成功。重启设备再次getprop确认persist.属性值是否被保留。在你的HAL或应用代码中打日志确认读写操作能正常执行。检查SELinux日志确保没有avc: denied。4. 高级话题与疑难排查4.1 属性覆盖与优先级问题有时候你会发现属性值不是你设定的那样这很可能是被覆盖了。Android属性加载有严格的顺序大致如下/default.prop(内核命令行androidboot.*属性转换而来)/system/build.prop/vendor/build.prop/product/build.prop,/odm/build.prop等持久化属性文件(/data/property/*)property_set的运行时设置后加载的会覆盖先加载的。如果你的属性在vendor/build.prop中被定义为ro.vendor.xxx1但在system/build.prop的某个后期加载的片段里又被定义为ro.vendor.xxx0那么最终值就是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 avc或adb logcat | grep avc。5. 常见问题与避坑指南实录在实际操作中我遇到了不少问题这里总结几个典型的问题一setprop成功了但重启后值又变回默认值了。原因你设置的属性名不是以persist.开头的。只有persist.属性才会被自动保存到/data/property/。解决确保你要持久化的属性名正确。如果你想修改一个ro.开头的属性那是徒劳的property_service会拒绝。问题二在Java应用里调用SystemProperties.set毫无反应也不报错。原因A你的应用没有android.permission.WRITE_SECURE_SETTINGS权限对于系统属性这个权限有时是必要的但并非对所有属性或者更关键的是SELinux策略不允许。原因B你尝试设置的属性名是ro.开头的。排查检查logcat是否有avc: denied日志。检查应用是否被授予了正确的权限在AndroidManifest.xml中声明并且签名匹配。尝试在adb shell下用setprop命令通常有shell或root权限测试如果命令可以但应用不行基本就是权限或SELinux问题。问题三属性在getprop里能看到但在我的C代码里property_get返回空字符串或默认值。原因极有可能是属性名拼写错误或者前后有空格。属性名是大小写敏感的。排查在代码里把尝试读取的属性名打印到日志里和getprop列表里的名字仔细比对。我曾经因为把vendor.power.lpm.enable错写成vendor.power.lpm_enable下划线 vs 点而调试了半天。问题四编译时发现property_contexts或*.te文件修改不生效。原因AOSP的构建系统对sepolicy有缓存和继承机制。特别是如果你在device/目录下修改但项目可能从另一个common或base的sepolicy继承。解决确保你的修改在正确的sepolicy目录下vendor还是systemAndroid 8.0以后推荐放在vendor。执行make clean或rm -rf out/target/product/设备名/obj/ETC/sepolicy_*.intermediates等清理操作然后重新编译。检查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_contexts和sepolicy、最后在各个房间安装开关和指示灯代码中读写。只要理清了这个脉络遵循规范的步骤再结合logcat和dmesg进行调试就能稳稳地让这个全局“公告栏”为你所用。
返回列表