recovery 找分区靠的不是分区表,是 /etc/fstab。而这份 fstab 里写着 PARTLABEL=oem,代码却用 strstr 找 LABEL=——一个是另一个的子串。这个巧合撑起了整条链路,也是它在 Debian/Ubuntu 上跑不起来的原因。
问题场景
上一篇里 recovery 拿到命令之后,要往 /userdata 擦数据、往 /dev/block/by-name/misc 写 BCB。但 recovery 是个跑在 ramdisk 里的独立程序,它没有在 SDK 里见过的分区表——parameter.txt 早就被 u-boot 解析成 GPT 了,recovery 手上什么都没有。
那它是怎么知道 /userdata 对应哪块设备的?/dev/block/by-name/ 这个目录又是谁建的?为什么官方文档 2.3 节专门讲"Debian 和 Ubuntu 系统的升级"要额外拷一个 udev 规则文件?
一、recovery 眼里的"分区表"
先说一个需要区分的地方:挂载分区和重建分区表走的是两条完全不同的路。
- recovery 二进制本身(roots.c、bootloader.c 这一套)不解析 parameter.txt,它只认 /etc/fstab 和 /dev/block/by-name/ 这些运行时视图
- 但 updateEngine 里有解析 RK 参数格式的代码——getParamFromString() 会解开 mtdparts= 那一行,然后在升级 parameter 分区时据此重建整个 GPT(这部分在 09 篇讲)
本节讲的是前者。分区表在启动链的更早阶段就已经被消费掉了:u-boot 读 parameter.txt 并据此写入 GPT,内核再根据 GPT 建立起设备节点。等 recovery 跑起来时,能看到的只有这些运行时视图:
| 来源 | 提供什么 | 谁在用 |
|---|---|---|
| /etc/fstab | 挂载点到设备的映射 | load_volume_table() |
| /dev/block/by-name/* | 按分区名寻址的符号链接 | bootloader.c、update_engine |
| /proc/cmdline | storagemedia=、androidboot.slot_suffix= 等 | isMtdDevice()、getCurrentSlot() |
| /sys/bus/mmc/devices/*/type | 哪个 mmcblkN 是 eMMC、哪个是 SD | init_sd_emmc_point() |
| /proc/mtd | MTD 分区名和大小 | mtd_scan_partitions() |
所以"找分区"实际上是在运行时视图里查表,而不是解析任何镜像格式。
二、17 行 fstab 变成一张表
roots.c 的 load_volume_table() 把 /etc/fstab 读进一张 Volume 数组。解析逻辑很短:
// external/recovery/roots.cwhile(fgets(buffer, sizeof(buffer) -1, fstab)) {i = sscanf(buffer,"%s%s%s%s%s%s", file_system,mount_point, fs_type, option,dump, pass);if(file_system[0] =='#')continue;if(i ==6) {/* 记录这一项到 device_volumes[] */}else{LOGE("skipping malformed recovery.fstab line:%sn", buffer);}}
要求一行必须有6 个字段(device、mountpoint、type、options、dump、pass),少一个就整行丢弃。注意 # 注释的判断是在 sscanf之后做的——不过因为格式串只吃6 个 token,完整的 6 字段行后面再跟 # 注释 属于第 7 个 token,会被直接丢弃,不影响解析。真正受影响的只有以 # 开头的整行注释,而且那样写本来就是合法用法。
这张表的 0 号位置是硬编码塞进去的:
// external/recovery/roots.c// Insert an entry for /tmp, which is the ramdisk and is always mounted.device_volumes[0].mount_point ="/tmp";device_volumes[0].fs_type ="ramdisk";device_volumes[0].device =NULL;/* device2 / option / dump / pass 在这里一并置空 */num_volumes =1;
/tmp 永远可用,不参与挂载。
rk3576 recovery 镜像里真实的 fstab
这个文件不是手写的,由 post-fstab.sh 在构建时生成。生成结果里的关键几行是:
# buildroot/output/rockchip_rk3576_recovery/target/etc/fstab/dev/root/ext4rw,noauto01tmpfs/tmptmpfsmode=177700.../dev/sda1/mnt/udiskautodefaults0 2/dev/mmcblk1p1/mnt/sdcardautodefaults0 2PARTLABEL=oem/oemext4defaults0 2PARTLABEL=userdata/userdataext4defaults0 2
看清楚最后两行的写法:PARTLABEL=userdata。记住这个,下一节就是围绕它的。
三、一个靠巧合工作的转换
fstab 里设备字段理论上可以直接写 /dev/mmcblk0p5,但 RK 的镜像想用分区名寻址。于是 roots.c 里有一段标着 HACK 的转换:
// external/recovery/roots.c/*HACK:Convert *LABEL to "by-name" symlink */if(strstr(file_system,"LABEL="))snprintf(device,sizeof(device),"/dev/block/by-name/%s",strstr(file_system,"LABEL=") +strlen("LABEL="));elsestrcpy(device, file_system);
作者的意图大概是处理 LABEL=xxx 这种写法。但代码用的是strstr 子串匹配,不是前缀匹配。
于是 PARTLABEL=userdata 里的 "PART" 后面正好跟着 "LABEL=",子串命中。
strstr 返回的是 "LABEL=userdata" 这段的起始位置,再 + strlen("LABEL=") 跳过 6 个字符,拿到的正是 userdata。拼出来的结果是:
PARTLABEL=userdata → /dev/block/by-name/userdata
结果是对的,但路径完全不是作者写这段代码时想的那条。如果哪天有人把 fstab 改成前缀判断(看起来是修 bug),或者换个不叫 PARTLABEL 的写法,这条链就断了。
顺带一个对照:官方文档 4.3.3 节给了另一种改法,让用户直接写完整路径——
/dev/block/by-name/userdata /userdata ext2 defaults 0 0
这种写法走 else 分支原样使用,反而更稳。
四、/dev/block/by-name/ 是谁建的
既然最后都落到 /dev/block/by-name/ <名字> ,那这个目录必须先存在。它由一条 udev 规则创建,在 recovery 的 rootfs 里是 external/rkscript/61-persistent-internal-storage.rules: 名字>
# persistent internal storage links(Android style): /dev/block/by-nameACTION=="remove", GOTO="persistent_storage_end"SUBSYSTEM!="block", GOTO="persistent_storage_end"# ignore removable devicesATTRS{removable}=="1", GOTO="persistent_storage_end"# ignore SD CardsATTRS{TYPE}=="SD", GOTO="persistent_storage_end"# honor various types of namesENV{ID_PART_ENTRY_NAME}=="?*",SYMLINK+="block/by-name/$env{ID_PART_ENTRY_NAME}", GOTO="persistent_storage_end"ENV{PARTNAME}=="?*", SYMLINK+="block/by-name/$env{PARTNAME}",GOTO="persistent_storage_end"ENV{ID_FS_LABEL}=="?*", SYMLINK+="block/by-name/$env{ID_FS_LABEL}",GOTO="persistent_storage_end"KERNEL=="mtdblock*", TEST=="device/name", SYMLINK+="block/by-name/$attr{device/name}"
三个名字来源按优先级排队:GPT 分区名 → 内核 uevent 的 PARTNAME → 文件系统 label。因为 GPT 是有分区名的(parameter.txt 里括号里那些),实际生效的基本都是第一个。
注意上面那两行"忽略":
- ATTRS{removable}=="1" 排除可移动设备
- ATTRS{TYPE}=="SD"明确排除 SD 卡
所以 SD 卡上不会有 /dev/block/by-name/*。这不是遗漏,是故意的——by-name 是给"内部固定存储"用的,插拔的介质不该占用这个名字空间。这也直接决定了下一节。
五、SD 卡走的是另一条路
因为 SD 卡被 udev 规则排除在外,它的设备节点就是普通的 /dev/mmcblk1p1——这正是 fstab 里那一行的来历:
/dev/mmcblk1p1/mnt/sdcardautodefaults0 2
但 roots.c 对 /mnt/sdcard 还埋了一个特例。挂载时它会绕开 fstab 里的设备名,改用环境变量:
// external/recovery/roots.cif(strcmp("/mnt/sdcard", v->mount_point) ==0) {blk_device =getenv(SD_POINT_NAME);if(blk_device ==NULL) {setFlashPoint();blk_device =getenv(SD_POINT_NAME);}result =mount(blk_device, v->mount_point, v->fs_type, ...);if(result ==0)return0;}
为什么要绕?因为 SD 卡插在哪个控制器上是不定的。板子设计不同、有没有 eMMC,都会让 SD 卡在 mmcblk0 和 mmcblk1 之间跳。写死 mmcblk1p1 只是碰巧对——真正靠谱的是运行时探测。
挂载失败时还会依次退回 v->device、v->device2 两个备选,算是一层保险。
六、运行时怎么认出哪块是 eMMC
setFlashPoint() 干的就是这件事(rktools.c)。它会遍历 /sys/bus/mmc/devices/,读每个设备的 type 文件,和一张表比对:
// external/recovery/rktools.hstaticconstchar*point_items[] = {"/dev/mmcblk0","/dev/mmcblk1","/dev/mmcblk2","/dev/mmcblk3"};staticconstchar*typeName[] = {"MMC","SD","SDIO","SDcombo"};
type 文件里写的是 MMC 或 SD 这样的字符串,匹配上哪个就把对应的 mmcblkN 记下来,然后导出成环境变量:
setenv(EMMC_POINT_NAME, result_point[MMC],1);if(access(result_point[SD], F_OK) ==0)setenv(SD_POINT_NAME_2, result_point[SD],1);// 再拼上 "p1" 试探第一个分区是否存在,存在才导出setenv(SD_POINT_NAME,name_t,1);
两个 SD 变量有区别:sd_point_name_2 存的是裸设备(/dev/mmcblk1),sd_point_name 存的是第一个分区(/dev/mmcblk1p1)。
但要注意:sd_point_name_2 在这份 SDK 里只被导出、没有被读取过(全仓库找不到 getenv("sd_point_name_2"))。真正被消费的是 emmc_point_name——update_engine/rktools.c 和 rkupdate 都靠它定位整块 flash。所以别把 sd_point_name_2 当成一条已经打通的链路。
这个函数会先等 /dev/block/by-name/misc 出现再开始探测——又回到了第四节那条 udev 规则:by-name 没建好,闪存探测就不会开始。
整条链路串起来是这样:
两个构建期的产物(parameter.txt、fstab)和一条 udev 规则,必须同时对齐,recovery 才能正常工作。少任何一环,表现都是"找不到设备"。
七、Debian/Ubuntu 上为什么跑不起来
官方文档 2.3 节专门讲了这个问题,原因是这一节开头那条链里,第四条断了——Debian/Ubuntu 系统里没有生成 /dev/block/by-name/ 的规则,设备节点是 /dev/mmcblk0p0、/dev/mmcblk0p1 这种数字名字。而 recovery 里到处都是写死的 by-name 路径:
// external/recovery/common.h
文档给的解法是把 buildroot 的 udev 规则拷过去。但这里有个坑:文档里写的文件名是 61-partition-init.rules,而这份 SDK 里实际的文件叫 61-persistent-internal-storage.rules——名字对不上,照着文档去找会找不到。
更麻烦的是,Ubuntu 那边已经有一条同类的规则了,但生成的是另一个路径。这份 SDK 里就有,在 ubuntu/binary/usr/lib/udev/rules.d/61-persistent-storage-android.rules:
ACTION!="remove", KERNEL=="mmcblk[0-9]p[0-9]", ENV{PARTNAME}=="?*",SYMLINK+="disk/by-partlabel/$env{PARTNAME}"
注意 SYMLINK+= 后面跟的是disk/by-partlabel/,不是block/by-name/。
两条规则同时看下来,Ubuntu 上会出现这样的局面:
| 系统 | 生成的符号链接 | recovery 需要的 | 结果 |
|---|---|---|---|
| Buildroot recovery | /dev/block/by-name/* | /dev/block/by-name/* | 正常 |
| Debian / Ubuntu | /dev/disk/by-partlabel/* | /dev/block/by-name/* | 找不到设备 |
所以光在 Ubuntu 里"有一条 android 风格的 udev 规则"是不够的,路径必须落到 block/by-name。稳妥的做法就是文档说的:把 buildroot 那条规则整个拷过去,放到 /etc/udev/rules.d/ 或 /lib/udev/rules.d/ 下。
如果串口里看到 /dev/block/ 里是一堆纯数字(179:0、179:1、7:0 这种),就说明规则没生效——官方 FAQ 4.3 节给的就是这个排查点。
八、格式化与扩容
找到设备之后,擦数据和扩容走的是同一张表。format_volume() 按 fs_type 分派到外部工具:
// external/recovery/roots.cext4 → make_ext4ext2 → make_ext2vfat → make_vfatntfs → make_ntfs
MTD 上的 yaffs2 / mtd / ubifs 走另一条路,用 mtd_erase_blocks() 直接擦(注意 mtd 也是合法值,别只记 yaffs2)。
resize_volume() 支持得窄一些,只有 ext4 和 ext2,调的是 rk_check_and_resizefs()。这也是为什么升级 userdata 之后能做自动扩容,而 vfat 不行——官方文档 4.3.3 节讲把 userdata 改成 vfat 时,通篇没提扩容,原因就在这里。
小结
| 环节 | 靠什么 | 常见故障 |
|---|---|---|
| 知道 /userdata 在哪 | /etc/fstab 的 PARTLABEL= 被当 LABEL= 匹配 | 改 fstab 写法就断 |
| 按名字找分区 | udev 规则生成 /dev/block/by-name/ | 规则没装 / 名字不对 |
| SD 卡寻址 | 被 udev 规则故意排除,走 sd_point_name | 误以为 SD 也该有 by-name |
| 分辨 eMMC/SD | 读 /sys/bus/mmc/devices/*/type | — |
| 计算 total sectors | getFlashSize() | MTD 要减掉 4 个擦除块 |
下一篇讲 UI:为什么官方文档说"不带屏的设备必须打开 No UI for recovery"——这个配置背后到底是哪一行代码会让 recovery 起不来。