EMUI8开机执行sh脚本:修改ramdisk与init.rc的完整指南

发布时间:2026/10/17 6:50:58
EMUI8开机执行sh脚本:修改ramdisk与init.rc的完整指南 做 Android 系统定制的人应该都碰到过这类需求手上的设备跑的是 EMUI8某厂商基于 Android 8.x 开源系统深度定制的 ROM希望在设备每次开机时自动执行一段 sh 脚本。可能是要调整内核参数、挂载额外分区、拉一个本地守护进程也可能是想统一做一次环境初始化。这篇文章我就围绕“EMUI8 开机执行 sh 脚本”这个真实场景把完整设计思路、底层原理、实操步骤和排错经验一次讲清楚适合正在做 ROM 定制、系统集成和底层自动化验证的开发者参考。内容会尽量说人话能直接照着操作也会把我踩过的坑一并交代。1. 需求拆解与整体思路1.1 为什么要在开机阶段执行脚本开机执行脚本听起来是个很朴素的诉求但实际能覆盖不少场景。我在实际项目里遇到比较多的是这么几类一是内核参数调节比如修改 TCP 拥塞控制算法、IO 调度器、某些功耗相关的节点这类操作用普通 App 根本碰不到必须要有高权限且时机要早二是挂载与目录绑定比如外置存储分区挂载、把某个目录 bind mount 到指定位置开机阶段做最合适三是环境清理和初始化像删除上次异常退出留下的临时文件、拉起的 socket、锁文件或者在系统服务启动前准备好配置文件。为什么不能用普通 App 来做原因很简单Android 的应用层启动顺序不可控而且普通 App 没有 CAP_SYS_ADMIN 这类内核能力很多系统节点即使给了 root 权限也未必能稳定操作。更直接的一点是有些设备根本没有屏幕或者设备不需要进入完整桌面环境App 方案天然失效。而一个 sh 脚本配合 init 进程来拉起属于用户空间最早执行的一批逻辑时机早、权限高、行为直白出了问题也容易定位。1.2 主流的几种实现路径对比围绕“开机跑脚本”这件事业内常见的做法大致能分成四类我先把它们放在一起对比再解释为什么我最终推荐第四种实现路径。实现路径生效阶段优点缺点适用场景修改 ramdisk 中的 init.rc 并添加服务内核启动后、init 解析阶段时机最早、可控性最强、权限高需要解包和重打包 boot 分区操作稍复杂绝大多数系统级初始化需求修改 system 分区内的服务或脚本system 挂载完成后不需要动 boot 分区改动简单依赖签名校验OTA 和恢复出厂易丢失时机偏晚应用层配置、弱权限初始化用户态 root 管理方案init 后期通过框架动态加载不用解包 boot 分区增删灵活兼容性受限于 ROM重启后可能失效维护成本高快速验证、个人设备临时使用外部控制器或上位机触发任意阶段完全不依赖目标系统需要额外硬件配合部署复杂量产产线、特殊嵌入式场景先解释一下为什么很多人第一反应会想到“改 system 分区”。因为很多早期 Android 版本可以靠 init.d 机制或直接把脚本丢进 system/etc但到了 Android 8.x 时代官方早已不默认执行 init.d厂商又普遍开启 system 分区签名校验直接改 system 镜像很容易触发启动校验失败。因此如果你希望脚本长期稳定存在并且不随恢复出厂被清理掉ramdisk 这条路径是最稳妥的。1.3 推荐方案改造 ramdisk 中的 init 服务EMUI8 虽然厂商定制内容多但底层没有脱离 Android 8.x 体系开机流程依然是 bootloader - kernel - init。init 会解析 ramdisk 里的 rc 文件并按照触发器执行对应动作。把 sh 脚本放进 ramdisk再通过 rc 文件定义成服务到了指定阶段由 init 拉起这个做法最直接、最可靠也是我做这类需求时的首选。选择 ramdisk 还因为一个很实际的好处ramdisk 位于 boot 分区内和 system 分区相互独立。你恢复出厂设置、双清、Wipe dataramdisk 里的脚本都不会被清掉除非重新刷 boot 分区或 OTA 覆盖。这就让脚本的“持久性”有了基本保障。当然这也意味着你必须在 boot 分区上做操作所以设备是否允许自定义引导程序、是否能处理引导校验就成了前置条件。这一点我在后面会单独强调。2. 核心机制解析与前置准备2.1 init 进程和 rc 文件的加载顺序要改好开机脚本至少得先理解 Android 8.x 的 init 是怎么转起来的。内核态工作结束后用户空间第一个进程就是 /init它的任务包括挂载基础文件系统、解析 rc 文件、启动属性服务、拉起核心服务。EMUI8 的 init 和通用 Android 8.x 没有本质区别只是厂商会在 rc 文件里追加不少自定义逻辑比如专属硬件初始化、日志服务等。rc 文件里最核心的是触发器和服务。几个常见触发器按时间顺序大概是 early-init、init、late-init、boot还有基于属性变化的触发器如 property:sys.boot_completed1。每个触发器对应一批命令执行完就往下走。如果你只是简单地把脚本挂在 on boot 后面很多时候会发现脚本跑得太早比如 /data 还没挂好、属性服务还没就绪进而出现写文件失败、目录不存在、mount 不成功这类问题。服务和触发器是配合使用的。一个 service 可以指定 class比如 class late_start然后 init 在 late-init 阶段执行 class_start late_start 时统一启动该 class 下的所有服务。用 class 而不是直接挂在 on boot 里的好处是时机更可控、启动顺序更清晰也方便你通过 disabled 加手动 start 的组合实现“注册但不立即启动”。这套机制理解透了后面写配置就不会像无头苍蝇一样乱试。2.2 脚本编写规范与存放位置脚本本身写起来不难难在让它符合 Android 运行环境。第一件事是解释器路径脚本头必须写#!/system/bin/sh不要想当然写成#!/bin/sh。Android 上/bin/sh的符号链接在某些版本里存在但很多定制 ROM 并不保证直接用/system/bin/sh最稳。第二件事是语法克制不要用 bash 特有语法比如数组、${var^^}这类Android 自带的 sh 是精简版 mksh 或 toybox sh兼容性有限。第三件事是文件格式我吃过一次亏脚本在 Windows 上编辑保存后带 CRLF 换行结果刷进去后 init 一直报“not found”看起来每个字符都对但执行器就是拒绝最后用 dos2unix 转一下立马正常。存放位置建议分三步走先在 ramdisk 根目录放/init.custom.sh因为 ramdisk 一解包就能看到不依赖其它分区验证通过后再根据需求迁移到/vendor/bin或/data/local/tmp。放/vendor/bin需要 vendor 分区已经挂载放/data/local/tmp则依赖数据分区解密完成时间更晚但好处是后续更新脚本不用重打包 boot。这个取舍没有标准答案完全取决于你的脚本内容对文件系统和时机的依赖程度。2.3 EMUI8 定制系统环境的关键点EMUI8 毕竟是厂商深度定制 ROMboot 分区结构虽然沿用 Android 标准但具体细节不能想当然。第一个要注意的是分区命名。A/B 设备上 boot 分区对应 boot_a/boot_b甚至还要看当前活跃 slot单分区设备则简单一些直接是 boot。建议先执行ls -l /dev/block/by-name/看清楚当前环境别把两个 slot 搞混。第二个关键是启动校验。Android 8 之后的设备普遍启用 Verified Bootboot 分区和 vbmeta 分区存在强校验关系。如果你只改了 ramdisk 里的文件刷回去之后很可能直接提示校验失败、卡在开机警告页。常规处理方式是在确认设备允许解锁引导程序的前提下先完成引导程序解锁再处理 vbmeta 状态。具体解锁步骤每家厂商公开的说明都不一样我不在这里展开只想强调操作前务必备份原始 boot 分区和 vbmeta 分区否则一旦校验不通过你连回退的余地都没有。3. 实操过程与关键步骤3.1 准备解锁与提取 boot 分区正式开始前先把环境准备好。一台 Linux 机器安装好cpio、gzip、mkbootimg这类常用工具即可不需要图形界面。然后把设备进入可调试状态确认 adb 连接正常。这里我默认你已经处理好了 bootloader 解锁以及 vbmeta 校验的问题如果没有请先按厂商公开流程完成操作不要在未解锁状态下强刷。提取 boot 分区可以用 adb 配合 dd 完成命令大概长这样adb root adb shell ls -l /dev/block/by-name/ | grep -E boot|vbmeta adb shell dd if/dev/block/by-name/boot of/data/local/tmp/boot.img bs4096 adb pull /data/local/tmp/boot.img ./我在这一步的习惯是顺便把vbmeta.img也备份一份再把当前 slot、分区布局、kernel cmdline 全部记录到文本里。后续出问题想要恢复这些信息比网上搜任何资料都有用。提取完 boot.img 后立刻在电脑上另外复制一份原始文件命名为boot-stock.img并放到安全目录避免后续操作把唯一备份弄坏。3.2 解包 ramdisk 并加入自定义脚本拿到了 boot.img接着解包。boot.img 本身由 kernel、ramdisk、dtb 等组成直接解包可以用现成脚本或开源工具如果你更愿意手动思路是先跳过 kernel 头部取出 ramdisk.cpio.gz再用 cpio 解开。这里以解包 ramdisk 为例mkdir boot cd boot # 假设你已经用工具从 boot.img 中提取出 kernel 和 ramdisk.cpio.gz mkdir ramdisk cd ramdisk gunzip -c ../ramdisk.cpio.gz | cpio -i解包后就会看到init、init.rc、selinux_version、prop等文件目录结构和 Android 系统根目录很像。接着把脚本放进去cp /path/to/init.custom.sh ./init.custom.sh chmod 0755 init.custom.sh这一步有个关键细节不要试图改动解包出来的其它文件结构尤其是selinux_version、file_contexts这类文件它们和打包格式、SELinux 标签绑定得很紧随意修改很容易造成 init 阶段解析异常。脚本放进去后可以先在宿主机上做一次轻量语法检查比如sh -n init.custom.sh确保没有基础语法错误再继续。3.3 编写 init 服务配置脚本放进 ramdisk 后还需要让 init 知道在什么时候把它拉起来。我习惯新建一个独立的 rc 文件比如init.custom.rc然后在init.rc里用import引入而不是直接在原有 rc 文件末尾追加。这样逻辑清晰回滚也方便。一个最小可用的配置可以写成这样on boot start custom_init service custom_init /init.custom.sh class late_start user root group root system seclabel u:r:init:s0 oneshot disabled解释一下每一行的作用。on boot是触发器表示 boot 阶段会执行下面的命令start custom_init表示启动名为 custom_init 的服务。服务本体用service custom_init /init.custom.sh定义执行路径是 ramdisk 根目录下的脚本。class late_start让服务归入 late_start 类这样 init 在 late-init 阶段就会处理它user root和group root system指定运行身份seclabel u:r:init:s0是给脚本指定 init 域的 SELinux 标签oneshot表示服务只执行一次不会常驻disabled表示初始不启动等待触发器手动拉起。如果你希望脚本再晚一些执行比如必须等系统完全启动完可以把触发器替换成属性监听on property:sys.boot_completed1 start custom_init这种情况下服务依赖属性触发脚本执行时机基本在所有开机阶段之后适合做后置初始化。3.4 重新打包 boot.img 并刷回脚本和 rc 文件都改好之后重新打包 ramdisk 并合成 boot.img。打包 ramdisk 的命令cd ramdisk find . | cpio -o -H newc | gzip ../new-ramdisk.cpio.gz然后合成 boot 镜像。最好的方式是用能够从原 boot.img 自动读取 cmdline、base、pagesize 等参数的工具而不是手敲 mkbootimg 参数。手敲参数很容易漏掉某个关键字段导致刷进去无法启动。合成完成后先确认新 boot.img 大小和原包差别不大再进入刷写阶段adb reboot bootloader fastboot flash boot boot-new.img fastboot reboot如果你使用的是 A/B 设备注意先fastboot getvar current-slot确认当前 slot并把镜像刷到对应分区。刷写完成后如果设备提示校验失败那就要回到 vbeta 处理流程确保引导程序处于解锁状态。整个过程中原版boot-stock.img始终保留这是你最后的救命稻草。3.5 首次开机验证刷回系统后第一件事不是确认功能而是确认脚本到底有没有被执行。我强烈建议脚本最开始就加上日志重定向比如#!/system/bin/sh LOG/data/local/tmp/custom_boot.log exec $LOG 21 echo custom script start $(date) 然后在每个关键步骤后面加 echo 输出。重启后进入系统直接adb shell cat /data/local/tmp/custom_boot.log看日志。如果日志文件存在且内容完整说明服务已经被成功拉起了如果文件不存在再去查 init 日志和 dmesg。不要一上来基于“我觉得它应该执行了”去做下一步日志先行能帮你节省大量时间。4. 常见问题与排查技巧实录4.1 服务没被拉起先看 init 有没有报错最典型的现象是脚本没执行日志文件根本没出现。这时候先去查 init 进程日志adb shell dmesg | grep -i init adb logcat -b all -d | grep -i init重点看有没有 rc 文件解析错误。rc 文件的语法容错很低一个词写错、缩进异常、缺了冒号都可能导致后续内容被忽略。我踩过一次非常无语的坑在某行命令后面多打了一个空格init 直接把整段配置判为非法服务怎么都不启动。后来逐行对比才找到问题从那以后我改完 rc 文件都会先做一次最小化验证比如把服务名改成在 on boot 下直接 echo至少能确认配置能被解析。另外检查一下服务名和触发器的拼接。disabled的服务不会自己启动必须由start命令触发如果你在 on boot 里写了start custom_init但服务名写成了custom_init_service那当然拉不起来。这类低级错误在情绪紧张时很容易犯排查时不妨先冷静扫一遍配置。4.2 脚本执行太早等待分区和属性脚本执行了但里面的操作失败这是第二种常见情况。比如脚本里要往/data/local/tmp写日志但执行时/data还没完成挂载和加密流程目录都不存在写日志自然失败。解决办法有两个层面。第一个是在脚本开头加等待循环while [ $(getprop sys.boot_completed) ! 1 ]; do sleep 2 done第二个是在 rc 层面把触发时机往后期挪使用on property:sys.boot_completed1代替on boot。两者可以结合rc 控制启动阶段的粗粒度脚本内的循环控制细粒度。还有一点要注意如果你的脚本会写入/data而设备开启了用户数据加密那么必须等用户空间解锁完成后才能访问。盲目在早期写入有时会把未解密的目录结构看得一团糟甚至影响后续解密流程。4.3 SELinux 拦截从 dmesg 找 avc 日志第三种情况更隐蔽服务确实启动了进程也运行了但脚本里的操作被 SELinux 拦截了一部分导致功能不完整。此时日志文件和 dmesg 里都会出现avc: denied相关内容但默认日志等级下可能被过滤需要你主动去查adb shell dmesg | grep avc看到avc: denied { execute }这类记录就说明脚本的执行域和目标文件之间的策略不允许。临时验证的手段是把 SELinux 打成 permissive 模式比如setenforce 0然后重新执行一次脚本确认问题是否真的出在策略上。确认后不要图省事一直开着 permissive正确做法是补充 sepolicy 规则给对应域放行必要的文件访问和执行权限。这里补充一个细节如果你在 rc 里给脚本指定了seclabel u:r:init:s0那么脚本运行在 init 域权限范围比默认域宽不少很多简单操作不会触发拦截可一旦脚本去访问带特殊标签的文件依然会被拒。所以关键是搞清楚脚本到底需要访问什么再去匹配对应的 sepolicy 规则。4.4 刷入后 bootloop快速恢复与备份习惯刷完 boot 后设备不断重启这种情况谁遇到都会慌但处理流程其实很固定。第一步设备如果能进 bootloader直接用 fastboot 把原版boot-stock.img刷回去fastboot flash boot boot-stock.img fastboot reboot多数情况下纯 ramdisk 改动导致的 bootloop 都能靠恢复原版 boot 解决。如果连 bootloader 都进不去那就需要考虑是不是操作过程中把分区表或 vbmeta 弄坏了这种时候只能依靠厂商提供的底层恢复工具操作复杂度会高一个数量级。所以我特别强调开工前把原版 boot 和 vbmeta 备份到电脑和外部存储各一份这条习惯能救命。排查 bootloop 原因时常见的有三个方向ramdisk 打包格式错误、脚本本身卡死比如脚本里出现了阻塞挂载、SELinux 标签异常导致 init 无法执行后续服务。逐一排除即可。4.5 问题速查表我在项目里把常见问题和解决方案整理成了一张速查表贴出来直接做参考故障现象可能原因处理建议脚本完全没有执行rc 文件解析异常、服务名不匹配、触发器没命中检查 dmesg 和 init 日志核对服务名与触发器脚本执行但日志写不了执行时机太早/data 还没挂载在脚本内等待属性或改用 boot_completed 触发器操作被系统拒绝SELinux 策略拦截查 dmesg avc 日志补充 sepolicy 规则刷入后无法开机ramdisk 打包错误、设备校验失败恢复原版 boot重新检查打包参数脚本执行了但功能不生效依赖的模块/服务还没准备好增加依赖检查用循环等待或属性触发5. 扩展玩法与个人心得5.1 触发时机与幂等控制开机脚本在实际项目中往往不是跑一次就完各种触发路径可能导致重复执行比如异常重启、用户手动重启等。我的习惯是在脚本里加一个幂等判断保证同一轮开机只执行一次完整逻辑if [ -f /data/local/tmp/custom_executed ]; then exit 0 fi touch /data/local/tmp/custom_executed要注意的是这个标记文件依赖 /data 分区所以脚本必须在数据分区可用之后才执行不然判断逻辑起不到作用。如果你希望每次开机都重新执行全部逻辑就不要用幂等文件改成记录时间戳即可。5.2 用开机属性做条件分支脚本里可以借助系统属性来决定执行哪些逻辑。比如某个设备在系统属性中标识为研发模式那么脚本可以走完整调试流程如果是普通用户版本则只执行核心初始化。判断逻辑并不复杂if [ $(getprop ro.boot.mode) dev ]; then # 开发模式额外初始化 echo dev mode init $LOG fi这类分支非常适合同一个 boot 镜像适配多种场景避免为不同模式维护多份 ramdisk。它也能让你在联调时临时关闭某些高风险动作比如在脚本开头判断某个自定义属性是不是1如果是则提前退出方便快速回滚逻辑。5.3 日志记录与自删除技巧开机脚本如果写得比较随意日志文件可能会无限增长。建议每次执行时先清理旧日志或者只保留最近几条。比如if [ -f $LOG ]; then tail -n 100 $LOG $LOG.tmp mv $LOG.tmp $LOG fi另外如果你希望在完成一键初始化后不再让脚本继续存在可以在脚本末尾加一条自删除逻辑从 ramdisk 里把自身删掉。不过这里要提醒ramdisk 是只读挂载的能否删除取决于你的挂载方式一般更实际的做法是让脚本保留但靠标记文件阻止重复执行。5.4 与普通 App 初始化搭配开机脚本更适合做系统层面的准备而不是承担所有业务逻辑。很多项目里脚本负责准备配置文件、设置目录权限、拉起底层服务然后一个普通 App 在启动后去读取这些产物继续做上层业务。这种职责划分可以避免把脚本写得过于庞大也方便后续维护。我曾经遇到一个项目脚本里塞了一大段网络探测逻辑结果 ROM 版本升级后网络栈行为变化脚本直接卡在等待回包那一步把后续服务全部拖住了。后来把所有业务判断移到 App 层脚本只做环境准备问题立刻消失。这个经验让我明白脚本层越薄越好宁可多写几个独立步骤也不要在 init 阶段搞复杂状态机。5.5 一点个人体会我自己做这类需求时习惯先在模拟器或一台不重要的设备上做最小验证确认 rc 语法、打包参数、SELinux 标签这些基础要素没问题再上真机。真机上第一次刷写后一定先看日志再判断效果。EMUI8 这种定制 ROM 最容易出问题的地方往往是厂商的额外校验和分区差异遇到问题先看 dmesg、看分区布局不要盲目重复刷写。备份做得足够完整你就有足够的试错空间。希望这篇分享能让你少走点弯路顺利把开机脚本跑起来。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询