收起左侧

将Arm飞牛从eMMC/TF卡无损迁移至外部存储(NVMe/USB/SATA/TF)的完整方案

1
回复
754
查看
[ 复制链接 ]

4

主题

7

回帖

0

牛值

江湖小虾

2026-3-22 18:25:13 显示全部楼层 阅读模式

将 fnOS 从 eMMC 无损迁移至 NVMe SSD

**日常用 ARM 设备,总习惯把固件刷进 eMMC 或者 TF 卡。eMMC 读写慢,寿命有限,用久了总觉得差口气。我手头有块 NanoPC-T4,给它刷了 Arm 飞牛固件简单体验了一下,就琢磨:要是能把系统挪到 NVMe 上,应该会爽很多。NanoPC-T4 正好有个 NVMe M.2 插槽,把系统装到外部存储上,就能绕过 eMMC 这个瓶颈。我去翻了友善官方的 **eflasher-multiple-os 固件说明,它确实可以把根文件系统写到 NVMe 或 USB 设备里,但我不确定它能不能兼容飞牛的 rootfs。而且官方固件内核太老了(4.19),新特性和 Docker 完整支持都享受不到。好在 ARM 设备的引导思路是相通的,自己研究了一下,把飞牛系统迁移到了 NVMe 硬盘上。下面就是完整的折腾记录。

416b1551fe0532c502766cd8d65da302.png

先搞清楚 ARM 板子是怎么启动的

**以 RK3399 为例,芯片内部固化了一段不可更改的启动 ROM(BootROM)。上电后,BootROM 会按顺序扫描可启动设备(一般是 SD 卡 → eMMC → SPI Flash),找到有效的 U-Boot 就加载执行。U-Boot 负责初始化内存、时钟等,然后从某个设备(通常是 eMMC 或 SD 卡的分区)加载内核(kernel)和设备树(dtb)。内核跑起来之后,再根据 **root= 参数去挂载根文件系统。

关键点:RK3399 的 BootROM 不认识 NVMe。U‑Boot 和内核镜像必须放在 eMMC 或 SD 卡上,但根文件系统可以放在任何内核能驱动的地方——当然也包括 NVMe。顺着这个思路,整个方案可以拆成两步:

  1. 准备 rootfs:在 NVMe 上建好分区,把 eMMC 上的根文件系统完整复制过去,并处理好 UUID 冲突。
  2. 修改 PARTUUID:修改 eMMC 上的内核引导参数,让系统从 NVMe 的 PARTUUID 启动。

**基于这个原理,我们决定用 **“eMMC 引导 + NVMe 系统” 的分离架构:

  • eMMC:只放引导文件(U‑Boot、内核、DTB),几乎只读,寿命无限
  • NVMe:放完整的根文件系统,所有读写都在这儿,享受高速低延迟

**这个方案既能绕过 RK3399 的引导限制,又能把 eMMC 的寿命省下来,同时把 NVMe 的性能用满。而且,这种“引导介质与 rootfs 分离”的思路并不局限于 NVMe——如果 eMMC 损坏了,或者板子根本没有 eMMC,甚至你想把系统挪到 USB 3.0 硬盘、SATA 盘、另一张 TF 卡上,本质都是一样的:只要 BootROM 能从某个介质(比如 TF 卡)把 U‑Boot 和内核拉起来,内核就能从任何它认得到的设备上挂载 rootfs。后面你会看到,我们只改了一个 **root=PARTUUID= 参数,这个参数指向哪里,系统就从哪里启动。

我手里的东西

  • 开发板:NanoPC-T4(RK3399)
  • 引导盘:板载 eMMC,14.6 GB
  • 系统盘:Intel Optane M10 16GB NVMe,实际可用只有 13.4 GB
  • 系统:fnOS 1.1.24
  • 内核:Linux 6.12.41
  • 文件系统:Btrfs
  • 引导方式:U-Boot 脚本(boot.scr + fnEnv.txt

注意:本文所有操作都假设 NVMe 设备名为 nvme0n1。你的可能是 nvme1n1 或别的,请用 lsblk 确认后替换。

一上来就碰了个硬钉子

**量了一下,傲腾 M10 虽然标称 16GB,实际可用只有 13.4 GB,而 eMMC 上的根分区占了 14.1 GB。用 **dd 直接克隆肯定不行,目标盘比源盘还小,得换个思路——不能用块设备克隆,只能用文件级复制,挑着有用的文件搬过去。

一步步开干

1. 先看看设备

lsblk

我的输出:

root@NanoPC-T4:/# lsblk
NAME                                              MAJ:MIN RM   SIZE RO TYPE  MOUNTPOINTS
mmcblk2                                           179:0    0  14.6G  0 disk  
**─mmcblk2p1                                       179:1    0   285M  0 part  /boot
**─mmcblk2p2                                       179:2    0  14.1G  0 part  /
mmcblk2boot0                                      179:32   0     4M  1 disk  
mmcblk2boot1                                      179:64   0     4M  1 disk  
zram0                                             252:0    0   1.9G  0 disk  [SWAP]
nvme0n1                                           259:0    0  13.4G  0 disk  
**─nvme0n1p1                                       259:1    0  13.4G  0 part  
  **─md0                                             9:0    0  13.4G  0 raid1 
    **─trim_08a0334e_a652_40cb_a25e_151bd2290a7e-0 253:0    0  13.4G  0 lvm   

注意,我的傲腾 M10 之前在飞牛上挂载过,残留了 LVM 和 RAID 的配置,得先清理干净。

2. 清理 NVMe 上的旧配置

**如果不清理干净,后面分区格式化会出各种奇怪问题——比如明明已经分区了,但 **lsblk 还是能看到旧的分区结构。

# 强制卸载所有可能相关的挂载点(根据你的实际情况改)
umount -l /vol1 2>/dev/null || true
umount -l /dev/md0 2>/dev/null || true
umount -l /dev/nvme0n1p1 2>/dev/null || true
​
# 停止 LVM 和 RAID 服务(替换成你实际的 LVM 名称)
lvchange -an -ff trim_08a0334e_a652_40cb_a25e_151bd2290a7e-0 2>/dev/null || true
mdadm --stop /dev/md0 2>/dev/null || true
dmsetup remove trim_08a0334e_a652_40cb_a25e_151bd2290a7e-0 2>/dev/null || true
​
# 擦除分区表头扇区,让内核认为这盘是空的
dd if=/dev/zero of=/dev/nvme0n1 bs=512 count=100 conv=fsync
​
# 通知内核重新读取分区表
partprobe /dev/nvme0n1

执行完后,lsblk 应该只看到一个光秃秃的 nvme0n1

root@NanoPC-T4:/# lsblk
NAME         MAJ:MIN RM   SIZE RO TYPE MOUNTPOINTS
mmcblk2      179:0    0  14.6G  0 disk 
**─mmcblk2p1  179:1    0   285M  0 part /boot
**─mmcblk2p2  179:2    0  14.1G  0 part /
mmcblk2boot0 179:32   0     4M  1 disk 
mmcblk2boot1 179:64   0     4M  1 disk 
zram0        252:0    0   1.9G  0 disk [SWAP]
nvme0n1      259:0    0  13.4G  0 disk 

3. 确认 eMMC 上的文件系统类型

blkid /dev/mmcblk2p2

我得到:

root@NanoPC-T4:/# blkid /dev/mmcblk2p2
/dev/mmcblk2p2: LABEL="rootfs" UUID="48b0a7cb-68bc-4337-b9ad-fc605dbb31bf" UUID_SUB="f29b6a03-37ca-4e05-987e-a2b5d5e05154" BLOCK_SIZE="4096" TYPE="btrfs" PARTUUID="afe747ae-13c5-4540-8cd0-94fb975662e5"

是 Btrfs,记下这个 UUID,后面要用。

4. 给 NVMe 分区(第一步:准备 rootfs 的开始)

# 彻底清一下残留元数据
wipefs -a /dev/nvme0n1
partprobe /dev/nvme0n1
​
# 创建 GPT 分区表
parted /dev/nvme0n1 mklabel gpt
# 输入 yes 确认
​
# 创建 Boot 分区,285MB,和 eMMC 保持一致
parted /dev/nvme0n1 mkpart primary ext4 1MiB 286MiB
​
# 创建 Root 分区,占满剩余空间(此时建立了一个ext4分区,后面会格式化成btrfs)
parted /dev/nvme0n1 mkpart primary ext4 286MiB 100%    
​
# 刷新分区表
partprobe /dev/nvme0n1
​
# 检查分区结果
lsblk /dev/nvme0n1

5. 格式化

# Boot 分区用 ext4,兼容性最好
mkfs.ext4 /dev/nvme0n1p1
​
# Root 分区用 btrfs,和源系统一致
mkfs.btrfs -f -L rootfs_nvme /dev/nvme0n1p2

格式化完,顺手记一下新分区的 UUID 和 PARTUUID:

blkid /dev/nvme0n1p2

6. 挂载并复制数据(继续准备 rootfs)

# 创建临时挂载点
mkdir -p /mnt/dst_boot /mnt/dst_root
​
# 挂载目标分区
mount /dev/nvme0n1p1 /mnt/dst_boot
mount /dev/nvme0n1p2 /mnt/dst_root
​
# 检查空间够不够
df -h /          # 源已用空间
df -h /mnt/dst_root  # 目标可用空间
# 必须确保 源已用 < 目标可用

**确认空间足够后,开始复制。因为目标盘比源盘小,不能用 **dd,只能用 rsync 做文件级复制。

# 复制 Boot 分区
rsync -avHAX /boot/ /mnt/dst_boot/
​
# 复制根文件系统(排除虚拟文件系统和外部挂载点)
rsync -avHAX \
  --exclude='/proc/*' \
  --exclude='/sys/*' \
  --exclude='/dev/*' \
  --exclude='/run/*' \
  --exclude='/mnt/*' \
  --exclude='/tmp/*' \
  --exclude='/lost+found/*' \
  --exclude='/vol*/' \
  --exclude='/media/*' \
  / /mnt/dst_root/

等几分钟,看到类似这样的输出就成功了:

sent 5,244,244,907 bytes  received 1,572,046 bytes  62,080,674.00 bytes/sec
total size is 5,241,423,994  speedup is 1.00

7. 修改 NVMe 分区的 UUID(关键步骤,属于准备 rootfs 的收尾)

**现在遇到一个新问题:我们用 **rsync 把文件原样复制过去了,但 Btrfs 文件系统的 UUID 也被原样复制了。也就是说,NVMe 上的新 rootfs 和 eMMC 上的旧 rootfs 有完全相同的 UUID。内核在挂载时看到两个相同 UUID 的 Btrfs 卷,就会混乱——它不知道该用哪一个。所以必须给 NVMe 上的 rootfs 生成一个新的 UUID。

先卸载 root 分区(btrfstune 要求分区不能处于挂载状态):

umount /mnt/dst_root

然后生成新 UUID:

btrfstune -u /dev/nvme0n1p2

**工具会问你是否确认,输入 **y

New fsid: ff743428-987f-4649-b87e-3a94a65c94c6
Set superblock flag CHANGING_FSID
Change fsid in extent tree
Change fsid in chunk tree
Clear superblock flag CHANGING_FSID
Fsid change finished

记录下新旧 UUID 和 PARTUUID:

OLD_UUID=$(blkid -s UUID -o value /dev/mmcblk2p2)
NEW_UUID=$(blkid -s UUID -o value /dev/nvme0n1p2)
NEW_PARTUUID=$(blkid -s PARTUUID -o value /dev/nvme0n1p2)
​
echo "旧 UUID (eMMC): $OLD_UUID"
echo "新 UUID (NVMe): $NEW_UUID"
echo "新 PARTUUID (NVMe): $NEW_PARTUUID"

我的输出:

旧 UUID (EMMC): 48b0a7cb-68bc-4337-b9ad-fc605dbb31bf
新 UUID (NVMe): ff743428-987f-4649-b87e-3a94a65c94c6
新 PARTUUID (NVMe): 871c986b-5385-45ae-a241-2e875a3ecc43

8. 修改 NVMe 里的 /etc/fstab(让新 rootfs 引用自己的 UUID)

**NVMe 上的 rootfs 里还有一个 **/etc/fstab 文件,它里面写的是旧的 UUID(指向 eMMC)。如果不改,即使内核从 NVMe 启动了,后续挂载 / 时又会因为 UUID 不匹配而出错。所以得把它也换成新的 UUID。

重新挂载 NVMe 的 root 分区:

mount /dev/nvme0n1p2 /mnt/dst_root

备份并替换 UUID:

cp /mnt/dst_root/etc/fstab /mnt/dst_root/etc/fstab.bak
sed -i "s/$OLD_UUID/$NEW_UUID/g" /mnt/dst_root/etc/fstab

验证一下:

cat /mnt/dst_root/etc/fstab

**应该看到 **/ 分区已经指向新的 UUID:

root@NanoPC-T4:/# cat /mnt/dst_root/etc/fstab
.......
UUID=ff743428-987f-4649-b87e-3a94a65c94c6       /       btrfs   defaults,noatime,errors=remount-ro      0       1
UUID=cf2ecdac-946c-4790-a894-19c10b526a1a       /boot   ext4    defaults,noatime,errors=remount-ro      0       2
tmpfs                                           /tmp    tmpfs   defaults,nosuid                         0       0

确认无误后,卸载:

umount /mnt/dst_root
umount /mnt/dst_boot

**至此,**第一步“准备 rootfs” 完成:NVMe 上已经有了一个独立、可用的根文件系统,UUID 也改好了。

9. 修改 eMMC 上的引导配置(第二步:修改 PARTUUID)

现在,NVMe 上的系统已经准备好了,但内核还不知道要去 NVMe 上找 root。因为 RK3399 的 BootROM 只认识 eMMC/SD 卡,所以我们还是从 eMMC 启动内核,但要让内核把根文件系统挂载到 NVMe 上。这就需要修改内核启动参数。

另外,如果直接使用文件系统的 UUID 来指定 root,可能会遇到问题:在早期引导阶段,内核不一定能正确解析 Btrfs 的 UUID(尤其当有多个相同类型的文件系统时)。更可靠的办法是用 PARTUUID——这是分区表的属性,在分区创建时就固定了,不会因为文件系统格式化而改变,而且内核在很早期的阶段就能识别它。

**编辑 eMMC 上的 **/boot/fnEnv.txt

vim /boot/fnEnv.txt

原来的内容大概是这样:

verbosity=1
bootlogo=false
console=both
extraargs=cma=256M
fdtfile=rockchip/rk3399-nanopc-t4.dtb
kernelfile=vmlinuz-6.12.41-trim

我们需要在 extraargs=cma=256M 后面追加 root=PARTUUID=xxxx-xxxx-xxxx-xxxx。注意 PARTUUID= 后面不要加双引号cma=256Mroot=... 之间必须有一个空格

修改后:

verbosity=1
bootlogo=false
console=both
extraargs=cma=256M root=PARTUUID=871c986b-5385-45ae-a241-2e875a3ecc43
fdtfile=rockchip/rk3399-nanopc-t4.dtb
kernelfile=vmlinuz-6.12.41-trim

为什么用 PARTUUID 而不是 UUID? ** **PARTUUID 是分区表的属性,不会因为文件系统重新格式化而改变,在早期引导阶段更可靠。而且,这个 root=PARTUUID= 参数是通用的——如果你的 eMMC 坏了,你可以把同样的参数指向另一张 TF 卡上的根分区;如果你想用 USB 3.0 硬盘启动,只要内核能识别该硬盘,改一下 PARTUUID 就行。整个方案的精髓就在这一行。

10. 重启验证

**建议再检查一遍 **/boot/fnEnv.txt 里的 PARTUUID 是否和 blkid /dev/nvme0n1p2 输出一致。

sync
reboot

验证一下是否真的跑在 NVMe 上

重启后,登录系统,执行:

df -hT /
lsblk
mount | grep " / "

**可以看到 **/ 已经挂载在 /dev/nvme0n1p2 上了,eMMC 的 mmcblk2p2 没有被使用。

d3e505c095da7d8fcc8bdcc818ecb575.png


性能测试:傲腾到底有多快

Intel Optane M10 是一块很有意思的小硬盘。它的容量不大(16GB),但用的是 3D XPoint 介质,延迟极低,读写磨损均衡做得特别好。普通 TLC 固态的 4K 随机读写通常只有几万 IOPS,而傲腾可以轻松跑到六位数。在 NanoPC-T4 上,虽然 PCIe 2.0 x4 的带宽限制了顺序读写(理论上限约 1.6 GB/s,实际更少),但傲腾真正的**锏——****4K 随机读写超低延迟——几乎不受影响。跑数据库、Docker 容器、频繁读写小文件的场景,这块小傲腾能带来脱胎换骨的体验。

**先装 **fio

sudo apt update && sudo apt install fio -y

然后跑几个测试:

# 4K 随机读
fio --name=randread --ioengine=libaio --iodepth=1 --rw=randread --bs=4k --direct=1 --size=256M --num**s=1 --runtime=60 --group_reporting --filename=/tmp/fio_test_read
​
# 4K 随机写
fio --name=randwrite --ioengine=libaio --iodepth=1 --rw=randwrite --bs=4k --direct=1 --size=256M --num**s=1 --runtime=60 --group_reporting --filename=/tmp/fio_test_write
​
# 混合读写(70% 读 30% 写,典型服务器负载)
fio --name=mixed --ioengine=libaio --iodepth=4 --rw=randrw --rwmixread=70 --bs=4k --direct=1 --size=256M --num**s=2 --runtime=60 --group_reporting --filename=/tmp/fio_mixed
​
# 清理
rm /tmp/fio_test_* /tmp/fio_mixed_* 2>/dev/null

我的测试结果:

测试项目 速度/性能 延迟 评价
顺序写入 611 MB/s - 吃满 PCIe 2.0 带宽
顺序读取 800 MB/s - 含内存缓存加速
4K 随机读 137,000 IOPS 6.16 μs 企业级性能
4K 随机写 135,000 IOPS 6.13 μs 读写几乎一致
混合读写 读 181k / 写 77.8k IOPS 29 μs 低负载依然极低延迟

傲腾在 RK3399 上跑出了 13 万 IOPS,延迟只有 6 微秒——系统响应飞快,Docker 容器启动也明显流畅了。

这个方案能走多远

**回过头看,这次迁移的核心其实就两步:先在目标盘上准备好一个独立的根文件系统,再改一下 eMMC 上 **fnEnv.txt 里的 root=PARTUUID= 参数。这个思路并不绑定 NVMe,也不依赖 eMMC 必须完好。

**引导介质可以是 eMMC,也可以是 TF 卡。万一哪天 eMMC 彻底写坏了,只要把同样的引导文件(U‑Boot、内核、dtb)和 **fnEnv.txt 拷到一张 TF 卡上,插上去就能照样启动。而 root=PARTUUID= 后面那个参数,你想指向哪里就指向哪里——NVMe 可以,USB 3.0 硬盘可以,SATA 盘可以,甚至另一张 TF 卡也可以。只要内核能驱动那个设备,系统就能从那儿跑起来。

所以这套方法不仅救活了我这块小容量傲腾,也适用于任何想把瑞芯微 ARM 设备的系统从慢速存储迁到高速外部介质的场景。如果你的 eMMC 还没坏,它可以作为提速方案;如果已经坏了,它更是一条复活路径。

LECREATE

2026 年 3 月 22 日

收藏
送赞 4
分享

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x

0

主题

5

回帖

0

牛值

系统先锋体验团🛩️

2026-3-23 17:04:12 显示全部楼层

赞,大佬能否出个oes系统迁移到ssd的教程,可以ota升级的那种

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则