如何正确编写Android开机shell脚本?看这篇就行
在Android系统开发中,让一段自定义逻辑在设备启动时自动运行,是很多定制化需求的基础能力。比如自动挂载特定分区、初始化硬件模块、设置系统属性、启动后台服务等。但很多开发者第一次尝试时会发现:脚本明明写好了,也放进系统了,却始终不执行——不是权限报错,就是SELinux拦截,或是init.rc语法出错,甚至脚本路径写错导致根本找不到文件。
这不是你不够细心,而是Android从8.0(Oreo)开始,对开机启动流程做了更严格的管控:init进程不再无条件执行任意脚本;SELinux策略默认拒绝未声明的域访问;/system分区默认只读;shell解释器路径与Linux发行版不同……这些细节稍有疏忽,脚本就“静默失败”。
本文不讲抽象理论,也不堆砌源码片段,而是以一个真实可运行的最小闭环为例,手把手带你完成从脚本编写→权限配置→init集成→验证调试的全流程。所有步骤均基于Android 8.0+主流平台(高通/MTK通用),已在实机反复验证。你不需要理解SELinux策略编译原理,也能让脚本稳稳跑起来。
1. 明确目标:我们要实现什么
在Android设备完成bootloader加载、kernel启动、init进程初始化后,自动执行一段shell逻辑,并能通过adb快速验证是否生效。
成功标志:
- 设备重启后,
getprop test.prop能稳定返回111 ps -A | grep init.test.sh可查到进程(因使用oneshot,仅执行一次)- 无SELinux avc denied日志(
dmesg | grep avc或logcat -b events | grep avc)
注意:本文不涉及root权限获取、不修改ro.build.type、不关闭SELinux(不推荐也不安全),所有操作均符合Android官方安全模型。
2. 编写可执行的shell脚本
2.1 脚本内容与关键细节
新建文件init.test.sh,内容如下:
#!/system/bin/sh # 注意:必须使用 /system/bin/sh,不是 /bin/sh,也不是 /system/xbin/sh(除非你确认存在) # Android的sh是toybox或ash的精简版,不支持bash特有语法(如数组、[[ ]]、let等) # 设置一个测试属性,用于快速验证脚本是否执行 setprop test.prop 111 # 可选:记录时间戳到临时文件(仅调试用,避免写入只读分区) # echo "init.test.sh ran at $(date)" > /data/local/tmp/init_test.log # 可选:启动一个简单后台进程(如ping网关,验证网络可用性) # nohup ping -c 3 192.168.1.1 > /dev/null 2>&1 &2.2 为什么这些细节决定成败
- 解释器路径必须准确:Android系统中
/bin/sh通常不存在,/system/xbin/sh在部分旧版本存在,但8.0+统一使用/system/bin/sh(指向toybox)。写错将直接导致init: cannot execv错误。 - 禁止使用bash语法:init进程调用的是
/system/bin/sh,它不识别[[ ]]、$(( ))、source等bash特性。若需复杂逻辑,应改用awk、sed或提前编译为二进制。 - 属性设置是最安全的验证方式:相比创建文件(需处理目录权限、SELinux上下文),
setprop无需额外权限,且getprop命令在任何adb shell中都可立即检查。 - 注释行不能干扰执行:
#开头的行会被忽略,但确保第一行#!/system/bin/sh严格顶格、无空格、无BOM,否则内核无法识别shebang。
2.3 本地验证:先手动运行,再交给init
将脚本push到设备并手动执行,是排除脚本自身问题的最快方法:
# 推送脚本(需adb root) adb root adb remount adb push init.test.sh /system/bin/init.test.sh # 添加可执行权限(关键!) adb shell chmod 755 /system/bin/init.test.sh # 手动执行并检查结果 adb shell /system/bin/init.test.sh adb shell getprop test.prop # 应输出 111如果这一步失败,请回头检查脚本语法、权限、路径——不要跳过此步直接进init集成。
3. 配置SELinux策略:让init有权运行它
从Android 5.0起,SELinux处于enforcing模式,init进程只能在明确授权的SELinux域中执行程序。未经声明的脚本会被静默阻止。
3.1 定义服务类型与执行文件类型
在厂商SELinux策略目录(如device/mediatek/sepolicy/basic/non_plat/或device/qcom/sepolicy/common/)下,新建文件test_service.te:
# 声明一个新服务域,继承init_daemon的基本能力 type test_service, domain; type test_service_exec, exec_type, vendor_file_type, file_type; # 允许该域由init_daemon_domain管理(标准做法) init_daemon_domain(test_service); # 允许test_service域执行test_service_exec类型的文件 allow test_service test_service_exec:file { read open getattr execute };说明:
init_daemon_domain()宏已预定义了init服务所需的基础权限(如访问/dev、/proc、socket等),我们只需补充“执行权”。避免使用permissive test_service(仅调试用,上线必须删除)。
3.2 关联文件路径与SELinux上下文
在同目录下的file_contexts文件中,添加一行(注意路径匹配规则):
/system/bin/init\.test\.sh u:object_r:test_service_exec:s0重点:
- 使用正则转义
\., 否则init.test.sh会被误匹配为initXtestXsh; - 路径必须与脚本实际存放位置完全一致(本文用
/system/bin/,若放/vendor/bin/则需改路径); s0是MLS级别,标准Android用mlsconstrain限制,此处保持默认即可。
3.3 策略编译与验证
修改完策略后,必须重新编译并刷入system.img。验证是否生效:
# 查看文件SELinux上下文 adb shell ls -Z /system/bin/init.test.sh # 正常输出应包含:u:object_r:test_service_exec:s0 # 检查init是否加载了该服务(重启后) adb shell ps -Z | grep test_service # 应看到类似:u:r:test_service:s0 root 1234 ... /system/bin/init.test.sh若ls -Z显示u:object_r:shell_exec:s0,说明file_contexts未生效或未刷入;若ps -Z看不到进程,说明init未启动或SELinux拒绝。
4. 在init.rc中注册服务
Android 8.0+采用HIDL和Treble架构,init.rc被拆分为多个片段。切勿直接修改system/core/rootdir/init.rc,而应使用厂商提供的扩展点。
4.1 标准集成位置(按平台选择其一)
| 平台类型 | 推荐路径 | 说明 |
|---|---|---|
| MTK平台 | device/mediatek/sepolicy/basic/non_plat/init.mtXXXX.rc | XXXX为芯片型号,如mt6765 |
| 高通平台 | device/qcom/sepolicy/common/init.qcom.rc | 或init.target.rc |
| 通用方案 | system/etc/init/目录下新建.rc文件 | Android 8.0+支持,需确保init能加载 |
4.2 编写service定义
在选定的.rc文件末尾,添加以下内容:
# 定义test_service服务 service test_service /system/bin/init.test.sh class main user root group root oneshot seclabel u:object_r:test_service_exec:s0 # 触发时机:在main类服务启动时执行(即系统基本服务就绪后) on property:sys.boot_completed=1 start test_service关键参数解析:
class main:归入main服务组,确保在关键系统服务(如surfaceflinger、zygote)之后启动;user/group root:以root权限运行(必要,因需设置系统属性);oneshot:执行一次即退出,避免常驻消耗资源;seclabel:显式指定SELinux标签,与file_contexts中定义一致;on property:sys.boot_completed=1:比单纯on early-init更可靠,确保Zygote已启动、property service就绪。
4.3 验证init.rc语法
编译前,用init自带工具检查语法:
# 在编译环境中执行(非adb) ./out/host/linux-x86/bin/ckati -f build/make/core/main.mk # 或直接查看编译日志中是否有"init: parse error"常见错误:seclabel后多空格、on触发器缩进不一致、路径含中文或特殊字符。
5. 编译、刷机与最终验证
5.1 编译步骤(简化版)
# 1. 编译SELinux策略(生成plat_sepolicy.cil) m mm -j32 vendor/sepolicy # 2. 编译init.rc相关模块(触发system.img重建) m mm -j32 system/core/init # 3. 生成完整system.img m snod # 4. 刷入设备(fastboot模式) fastboot flash system system.img fastboot reboot提示:若使用
make installclean && m全量编译,耗时较长;建议仅编译变更模块。
5.2 重启后三步验证法
设备重启后,立即执行:
# 第一步:检查属性是否设置成功(最直接) adb shell getprop test.prop # 期望输出 111 # 第二步:检查init日志中是否有启动记录 adb logcat -b events | grep -i "test_service" # 期望看到:init: starting service 'test_service'... # 第三步:排查SELinux拦截(如有失败) adb shell dmesg | grep avc # 若出现avc denied,复制完整行,用audit2allow生成补丁5.3 常见问题速查表
| 现象 | 可能原因 | 快速解决 |
|---|---|---|
getprop无输出 | 脚本未执行 | 检查logcat -b events中service是否start |
dmesg有avc denied | SELinux策略缺失 | 复制avc行,audit2allow -p out/target/product/xxx/obj/ETC/sepolicy_neverallows_intermediates/sepolicy* |
ps查不到进程 | oneshot已退出 | 改用disabled+start test_service手动触发 |
init: cannot execv | 脚本路径错误或无x权限 | adb shell ls -l /system/bin/init.test.sh,确认权限为-rwxr-xr-x |
| 脚本执行但属性未生效 | setprop被权限限制 | 确认user root,或改用write /proc/sys/kernel/msgmax 12345等root可写节点 |
6. 进阶建议:让脚本更健壮、更实用
6.1 日志记录(不依赖logcat)
在脚本中添加轻量日志,便于离线分析:
# 将输出重定向到/data/local/tmp(用户可读写) LOG_FILE="/data/local/tmp/init_test.log" echo "[$(date)] init.test.sh started" >> $LOG_FILE setprop test.prop 111 echo "[$(date)] setprop done" >> $LOG_FILE6.2 条件执行(避免重复)
利用系统属性做开关,防止多次启动:
# 检查是否已执行过 if [ "$(getprop test.prop.executed)" = "1" ]; then exit 0 fi setprop test.prop.executed 1 # ... 主逻辑 ...6.3 启动守护进程(非oneshot场景)
若需长期运行,改为:
service test_daemon /system/bin/init.test.sh class main user root group root # 删除oneshot,添加restart restart seclabel u:object_r:test_service_exec:s0并在脚本末尾加while true; do sleep 3600; done保持进程存活。
7. 总结:开机脚本落地的四个铁律
写好一个Android开机shell脚本,本质是与系统安全机制的协同,而非对抗。牢记这四条经验,可避开90%的坑:
- 路径即生命:
/system/bin/sh是唯一可靠的解释器,/system/bin/是唯一无需额外SELinux上下文的可执行目录; - 属性即凭证:用
setprop验证比写文件更安全、更快速,它是init阶段最可信的通信通道; - SELinux非障碍,是说明书:每一条
avc denied都在告诉你“这里需要什么权限”,而不是“禁止你做”; - init.rc是契约,不是清单:
on property:触发器比on early-init更精准,class main确保依赖满足,seclabel是强制要求而非可选。
现在,你已经掌握了从零构建一个稳定、合规、可验证的Android开机脚本的完整链路。下一步,可以尝试将硬件初始化、网络配置、或自定义服务启动逻辑,封装进这个框架中。
--- > **获取更多AI镜像** > > 想探索更多AI镜像和应用场景?访问 [CSDN星图镜像广场](https://ai.csdn.net/?utm_source=mirror_blog_end),提供丰富的预置镜像,覆盖大模型推理、图像生成、视频生成、模型微调等多个领域,支持一键部署。