Android开机启动shell脚本踩坑总结,这些错误别再犯
在Android系统定制开发中,让自定义shell脚本随系统开机自动运行是常见需求——比如初始化硬件参数、配置网络环境、启动后台守护进程等。但看似简单的“写个脚本+加到init.rc”流程,实际落地时却布满隐蔽陷阱:脚本不执行、权限被拒、SELinux拦截、路径失效、执行时机错乱……轻则功能缺失,重则导致系统卡在开机动画甚至无法进入桌面。
本文不是照搬官方文档的理论复述,而是基于真实项目(MTK平台Android 8.0)反复调试、抓取logcat和dmesg、逐行比对sepolicy规则后沉淀出的实操避坑指南。所有内容均经过真机验证,覆盖从脚本编写、SELinux适配、init.rc集成到问题定位的完整链路。你将看到的不是“应该怎么做”,而是“为什么这里会挂”“log里哪一行暴露了真相”“改完这行代码后adb shell里立刻能验证”。
如果你曾遇到过“脚本手动执行OK,一放进开机就静默失败”“setprop没生效”“init.rc语法没错但服务根本没起来”这类问题——这篇文章就是为你写的。
1. 脚本本身:第一道关,90%的人栽在这里
很多人以为shell脚本只要语法正确就能跑,但在Android init上下文中,解释器路径、执行权限、环境变量、输出重定向全部是雷区。下面这些细节,错一个,脚本就彻底静默。
1.1 解释器路径必须精确匹配系统实际路径
Android的init进程调用脚本时,不会读取shebang(#!)后面的路径并自动查找解释器。它只认你写死在service声明里的路径,而脚本内部的#!/xxx/sh只是给开发者看的提示,init根本不解析它。
- 正确做法:脚本第一行必须是
#!/system/bin/sh或#!/system/xbin/sh(取决于你的系统是否启用busybox),且这个路径必须与init.rc中service命令指定的路径完全一致。 - ❌ 常见错误:
- 写成
#!/bin/sh(Linux标准路径,Android里不存在) - 写成
#!/system/bin/bash(Android默认不带bash,即使有也未被SELinux策略允许) init.rc里写/system/bin/init.test.sh,但脚本里写#!/system/xbin/sh(路径不一致导致init找不到解释器)
- 写成
验证方法:在adb shell中执行
ls -l /system/bin/sh /system/xbin/sh,确认哪个真实存在且可执行;再用readelf -a /system/bin/sh | grep interpreter确认其动态链接器路径,避免因ABI不兼容导致崩溃。
1.2 执行权限和文件位置有硬性约束
init进程以root身份运行,但它只信任特定目录下的可执行文件,且对文件权限极其敏感:
推荐存放路径:
/system/bin/或/vendor/bin/(需对应添加file_contexts规则)❌ 绝对禁止路径:
/data/local/tmp/(init启动时/data可能还未挂载或处于加密状态)/sdcard/(FUSE挂载,init无权限访问)/system/etc/(通常只有读权限,且init不从此处加载可执行文件)
权限必须为
-rwxr-xr-x(755),且属主属组为root:root❌ 常见错误:
chmod 777 init.test.sh(过于宽松,SELinux可能拒绝执行)或chown system:system init.test.sh(init以root运行,非root属主会被拒绝)
快速验证:
adb shell ls -l /system/bin/init.test.sh,确保输出形如-rwxr-xr-x 1 root root 234 ...
1.3 脚本内避免依赖外部命令和复杂逻辑
init进程启动阶段,系统服务尚未就绪,很多常用命令不可用或行为异常:
❌ 禁止使用:
curl,wget,ping,netstat,ps,top(busybox未完全初始化)❌ 避免长耗时操作:
sleep 10会导致init阻塞,影响其他服务启动安全操作:
setprop,getprop,log,echo > /dev/kmsg,mkdir -p,touch关键提醒:不要在脚本里创建新文件或写入日志到
/data/。此时/data分区可能处于encrypting或mounted中间态,写入会失败且无错误提示。建议统一用log -t TEST "message"输出到kernel log,再用dmesg | grep TEST查看。
2. SELinux策略:看不见的墙,80%的失败根源
Android 8.0起强制启用SELinux,且init相关服务默认处于enforcing模式。即使你临时setenforce 0,脚本仍可能因缺少file_contexts规则而无法被init识别为合法可执行文件。
2.1 file_contexts规则必须精准匹配路径和正则
这是最容易被忽略却最致命的一环。file_contexts文件定义了每个文件路径对应的SELinux类型,init在加载脚本前会严格校验。
- 正确规则(以
/system/bin/init.test.sh为例):
/system/bin/init\.test\.sh u:object_r:test_service_exec:s0- ❌ 致命错误:
- 写成
/system/bin/init.test.sh(缺少转义符.,正则匹配失败) - 写成
/system/bin/.*\.sh(过于宽泛,违反最小权限原则,编译可能报错) - 路径与
init.rc中service路径不一致(如rc里写/system/bin/init.test.sh,但file_contexts写/vendor/bin/init.test.sh)
- 写成
验证方法:编译后检查
out/target/product/xxx/root/file_contexts是否包含你添加的行;启动后执行adb shell ls -Z /system/bin/init.test.sh,确认输出中u:object_r:test_service_exec:s0已生效。
2.2 te策略要覆盖完整的domain transition链
仅仅定义test_service_exec类型还不够。init进程需要从initdomain切换到你的test_servicedomain才能执行脚本,这需要三重策略:
声明domain和type(
test_service.te):type test_service, domain; type test_service_exec, exec_type, vendor_file_type;允许init启动该domain(关键!):
# 必须添加,否则init无法fork出test_service进程 allow init test_service_exec:file { read execute open getattr }; # 允许init进行domain transition allow init test_service:process { transition };定义domain能力(
test_service.te):# 让test_service能设置属性(否则setprop失败) allow test_service system_file:file { read open getattr }; allow test_service system_prop:property_service { set }; # 允许写log(调试必需) allow test_service kernel_debug:file { write };
注意:
init_daemon_domain(test_service)宏仅适用于init启动的daemon服务,不适用于oneshot脚本。直接使用该宏会导致策略缺失,必须手写上述三条核心规则。
3. init.rc集成:语法正确 ≠ 功能正常
init.rc是Android的启动蓝图,但它的语法和执行逻辑与普通shell差异巨大。一个空格、一个缩进、一个单词拼错,都会让服务消失于无形。
3.1 service声明的字段顺序和值有严格要求
以下是最小可用模板,任何字段缺失或顺序错乱都会导致服务不被加载:
service test_service /system/bin/init.test.sh class main user root group root oneshot seclabel u:object_r:test_service_exec:s0必填字段(缺一不可):
class:必须与on property:触发条件或start <class>命令匹配,main类在早期启动user/group:必须为root,否则无权执行系统级操作oneshot:表示执行完即退出(适合初始化脚本);若省略,init会持续监控进程,脚本退出后立即重启,造成循环seclabel:必须与file_contexts中定义的type完全一致❌ 常见错误:
user system(权限不足,无法setprop)- 缺少
oneshot(脚本退出后init不断重启,log刷屏) seclabel写成u:object_r:test_service:s0(类型名错误,应为test_service_exec)
3.2 启动时机必须显式控制
Android 8.0引入on property:触发机制,比简单放在class main里更可靠:
on property:sys.boot_completed=1 start test_service- 优势:确保系统基本服务(如property service)已就绪,
setprop命令可用 - ❌ 风险:若脚本依赖
/data分区,需改用on property:ro.crypto.state=unencrypted(解密完成后)
验证服务状态:
adb shell getprop | grep test(检查属性是否设置成功);adb shell ps | grep test_service(确认进程是否存在)。
4. 问题定位:不靠猜,靠log精准打击
当脚本不执行时,90%的人第一反应是“改代码”,但真正的问题往往藏在log里。以下是高效排障路径:
4.1 三步锁定问题层级
| 步骤 | 命令 | 判定依据 | 问题层级 |
|---|---|---|---|
| 1. 检查init是否加载服务 | adb shell cat /proc/1/cmdline | 输出含init.rc路径 | init解析层 |
| 2. 检查服务是否在initctl列表 | adb shell su -c 'initctl list' | grep test_service | 有输出且状态为stopped | init服务注册层 |
| 3. 检查SELinux拒绝日志 | adb shell dmesg | grep avc | 出现avc: denied { execute } for path=/system/bin/init.test.sh | SELinux策略层 |
4.2 关键log解读速查表
init: cannot find '/system/bin/init.test.sh'→ file_contexts未生效或路径错误init: starting service 'test_service'...+ 无后续日志 → 脚本执行瞬间崩溃(检查shebang路径或语法错误)avc: denied { execute } for comm="init" name="init.test.sh"→ file_contexts规则缺失或类型名错误avc: denied { set } for property="test.prop"→ te策略缺少allow test_service system_prop:property_service { set };test_service: not found→initctl list无此服务,init.rc语法错误或未编译进镜像
实用技巧:在脚本开头加入
log -t TEST "STARTED at $(date)",结尾加log -t TEST "ENDED"。若只看到STARTED,说明脚本在中间某行崩溃,结合dmesg定位具体错误。
5. 工程化建议:让开机脚本真正可靠
最后分享几个经项目验证的工程实践,帮你把“能跑”升级为“稳跑”:
5.1 使用on property:替代class main启动
# 在init.rc中 on property:sys.boot_completed=1 start test_service # 脚本内增加超时保护 #!/system/bin/sh timeout=30 while [ $timeout -gt 0 ]; do if getprop sys.boot_completed | grep -q 1; then break fi sleep 1 timeout=$((timeout - 1)) done setprop test.prop 1115.2 用logwrapper包装脚本,捕获stderr
service test_service /system/bin/logwrapper /system/bin/sh /system/bin/init.test.sh这样脚本内所有echo和错误都会输出到logcat,无需手动log -t。
5.3 构建时自动校验,防低级错误
在Android.mk中添加检查规则:
$(warning Checking init.test.sh permissions...) $(shell chmod 755 $(LOCAL_PATH)/init.test.sh) $(shell chown root:root $(LOCAL_PATH)/init.test.sh)获取更多AI镜像
想探索更多AI镜像和应用场景?访问 CSDN星图镜像广场,提供丰富的预置镜像,覆盖大模型推理、图像生成、视频生成、模型微调等多个领域,支持一键部署。