news 2026/10/17 6:50:43

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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 data,ramdisk 里的脚本都不会被清掉,除非重新刷 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_completed=1。每个触发器对应一批命令,执行完就往下走。如果你只是简单地把脚本挂在 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 毕竟是厂商深度定制 ROM,boot 分区结构虽然沿用 Android 标准,但具体细节不能想当然。第一个要注意的是分区命名。A/B 设备上 boot 分区对应 boot_a/boot_b,甚至还要看当前活跃 slot;单分区设备则简单一些,直接是 boot。建议先执行ls -l /dev/block/by-name/看清楚当前环境,别把两个 slot 搞混。

第二个关键是启动校验。Android 8 之后的设备普遍启用 Verified Boot,boot 分区和 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 bs=4096" 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_completed=1 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" 2>&1 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_completed=1代替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、看分区布局,不要盲目重复刷写。备份做得足够完整,你就有足够的试错空间。希望这篇分享能让你少走点弯路,顺利把开机脚本跑起来。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/17 6:50:35

千问 LeetCode 343. 整数拆分 Java实现

Java 实现 class Solution { public int integerBreak(int n) { if (n < 3) { return n - 1; }int quotient n / 3;int remainder n % 3;if (remainder 0) {return (int) Math.pow(3, quotient);} else if (remainder 1) {return (int) Math.pow(3, quotient - 1) * 4;}…

作者头像 李华
网站建设 2026/10/17 6:49:12

Rancher + Helm Chart + 私有仓库:打造标准化Kubernetes应用交付体系

搞Kubernetes平台运维这些年&#xff0c;我最大的体会就是&#xff1a;应用交付方式的标准化&#xff0c;决定了整个交付链条的上限。早期我们团队每次上线都是复制一堆YAML改改名字&#xff0c;碰到几十个服务轮流发布的时候&#xff0c;改错一个缩进或者复制漏掉一个配置项&a…

作者头像 李华
网站建设 2026/10/17 6:49:09

C#类成员全解析:字段、属性到构造函数与事件的面试要点

1. 先说清楚&#xff1a;面试官问"类成员"到底想问什么"C#每日面试题"这个系列我看了不少&#xff0c;说实话大部分人的回答都停留在"字段、属性、方法、事件"这种背诵式的枚举上&#xff0c;能说到点子上、把成员之间的边界和设计意图讲明白的人…

作者头像 李华
网站建设 2026/10/17 6:48:32

开放式代码评审:一套从流程设计到工具落地的完整实践

代码评审这事&#xff0c;十个人有九个说重要&#xff0c;但真正做得好的团队屈指可数。我前前后后待过好几个研发团队&#xff0c;从十几个人的创业公司到上百人的业务线&#xff0c;见过太多次评审会变成“走过场”&#xff1a;二十页的diff没人细看、Committer机械点个Appro…

作者头像 李华
网站建设 2026/10/17 6:48:28

如何选择降 AI 率工具:从选型到避坑的完整思路

深夜十一点&#xff0c;宿舍楼里只剩键盘声。论文改到第三稿&#xff0c;查重过了&#xff0c;AI 检测却亮起红灯。导师的批注只有一句&#xff1a;内容需要重写。你盯着满屏的修改建议&#xff0c;不知道该信哪一款工具&#xff0c;更怕上传的文稿被拿去喂了模型。2026年&…

作者头像 李华
网站建设 2026/10/17 6:48:27

小白程序员必备:收藏这份金融业大模型落地指南,轻松开启AI转型之路!

本文深入剖析金融业AI应用现状&#xff0c;揭示头部机构与中小机构在智能体落地上的差距与机遇。核心聚焦信息采集、文档生成、数据整理等低风险高价值场景&#xff0c;提供合规先行、数据其次、自动化最后的三步落地策略&#xff0c;并针对投研、信贷、核保等细分领域提出定制…

作者头像 李华