欢迎访问云渡桥财经网

recovery是怎么找到各个分区的?——fstab、by-name和那个PARTLABEL巧合

频道:云渡桥资讯 日期: 浏览:4478

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="));else strcpy(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-name
ACTION=="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.h#defineEMMC_POINT_NAME "emmc_point_name"#defineSD_POINT_NAME "sd_point_name"#defineSD_POINT_NAME_2 "sd_point_name_2"
staticconstchar*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 没建好,闪存探测就不会开始。

整条链路串起来是这样:

wKgZO2rFgPaAFgB3AAB3hAitorM894.png

两个构建期的产物(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#defineMISC_PARTITION_NAME_BLOCK"/dev/block/by-name/misc"

文档给的解法是把 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 起不来。