收起左侧

NanoPC-T4实战:将fnOS从eMMC无损迁移至NVMe SSD的完整方案

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

2

主题

4

回帖

0

牛值

江湖小虾

NanoPC-T4 实战:将 fnOS 从 eMMC 无损迁移至 NVMe SSD 的完整方案(含 Btrfs 与小容量盘适配)

一、背景与目标

1.1 理解 ARM 开发板的引导过程

**在开始之前,有必要先简单梳理一下 ARM 开发板的引导流程。以 RK3399 为例,芯片内部固化了一段不可更改的启动 ROM(BootROM)。上电后,BootROM 会按照预设顺序扫描可启动设备(通常是 SD 卡 → eMMC → SPI Flash),找到有效的引导程序(U-Boot)后加载执行。U-Boot 负责初始化硬件(DDR、时钟等),然后从指定设备(通常是 eMMC 或 SD 卡的分区)加载内核(kernel)和设备树(dtb)。内核启动后,再根据 **root= 参数挂载根文件系统。

关键点在于:RK3399 的 BootROM 无法直接识别 NVMe 硬盘。引导程序(U-Boot)和内核镜像必须存放在 eMMC 或 SD 卡上。但根文件系统可以放在任何内核能识别的地方——包括 NVMe。

1.2 硬件限制

RK3399 芯片有一个众所周知的限制:不支持直接从 NVMe/M.2 硬盘启动。引导程序必须位于 eMMC 或 SD 卡上。友善官方给出的 eflasher-multiple-os 固件虽然可以将根文件系统写入 NVMe 或 USB 设备,但我无法确定它对 Arm 飞牛的兼容性。而且官方固件内核过老(4.19),无法享受新特性和 Docker 完整支持。eMMC 读写慢且寿命有限,不适合作为长期运行的系统盘。

1.3 解决方案

**采用 **“eMMC 引导 + NVMe 系统” 分离架构:

  • eMMC:仅存放引导文件(U-Boot、Kernel、DTB),几乎只读,寿命无限
  • NVMe:存放完整根文件系统,承担所有读写,享受高速低延迟

该方案既能突破 RK3399 的引导限制,又能充分发挥 NVMe 的性能优势,同时延长 eMMC 的使用寿命。这套方案基本适用于常见的 Linux 固件。

1.4 本次案例的特殊性

项目 源盘 (eMMC) 目标盘 (NVMe)
总容量 14.6 GB 13.4 GB
根分区大小 14.1 GB 13.1 GB
文件系统 Btrfs Btrfs
关键问题 目标盘容量 < 源分区容量,无法使用 dd克隆

**这是一个很尴尬的局面——傲腾 M10 16GB 的实际可用容量比 eMMC 还小。用 **dd 直接克隆会失败,必须换一种思路。


二、硬件与软件环境

2.1 硬件

  • 开发板:NanoPC-T4(RK3399)
  • 引导盘:板载 eMMC(14.6 GB)
  • 系统盘:Intel Optane M10 16GB NVMe(实际可用 13.4 GB)

本文****所有操作nvme0n1 作为 NVMe 设备名。实际操作中,请根据你的硬件环境替换为正确的设备名(例如 nvme1n1nvme2n1 等)。可以使用 lsblk 命令确认。

2.2 软件

  • 系统:fnOS 1.1.24
  • 内核:Linux 6.12.41
  • 文件系统:Btrfs
  • 引导方式:U-Boot 脚本(boot.scr + fnEnv.txt

三、核心挑战与解决方案

挑战 解决方案
NVMe 容量小于 eMMC 分区 放弃 dd,改用 rsync文件级复制
rootfs 克隆后 Btrfs 文件系统 UUID 冲突 使用 btrfstune -u生成新 UUID
RK3399 无法从 NVMe 启动 eMMC 保留引导,通过内核参数指定 NVMe 为 root
引导脚本动态获取分区 UUID 修改 fnEnv.txt,用 extraargs覆盖 root 参数
双盘同时存在导致挂载混乱 使用 PARTUUID 而非 UUID 指定启动分区

四、详细操作步骤

4.1 准备工作

# 确认NVME设备名称(请根据实际输出替换教程中的nvme0n1)
lsblk
# 确认 eMMC 为 mmcblk2,NVMe 为 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  
**─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 配置,需要先清理干净。

4.2 清理 NVMe 硬盘上的旧配置

这一步很关键。如果 NVMe 上还有飞牛残留的 LVM 或 RAID 元数据,后续操作会出问题。

# 1. 强制卸载所有相关挂载点(替换为你的实际挂载点)
umount -l /vol1 2>/dev/null || true
umount -l /dev/md0 2>/dev/null || true
umount -l /dev/nvme0n1p1 2>/dev/null || true
​
# 2. 停止 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
​
# 3. 清除分区表头扇区(让内核认为磁盘是空的)
dd if=/dev/zero of=/dev/nvme0n1 bs=512 count=100 conv=fsync
​
# 4. 通知内核重新读取分区表
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 

4.3 确认源系统文件系统类型

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.4 分区 NVMe 硬盘

注意:以下所有 nvme0n1 请替换为你的实际 NVMe 设备名。

# 彻底清除可能残留的元数据
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 分区(占用剩余所有空间)
parted /dev/nvme0n1 mkpart primary ext4 286MiB 100%
​
# 刷新分区表
partprobe /dev/nvme0n1
​
# 验证分区结构
lsblk /dev/nvme0n1

4.5 格式化分区

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

格式化完成后,记录下新分区的 UUID 和 PARTUUID:

blkid /dev/nvme0n1p2

4.6 挂载并复制数据

# 创建挂载点
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  # 目标可用空间
# 确保 源已用 < 目标可用

**确认空间足够后,开始复制。这里用 **rsync 而不是 dd,因为目标盘比源盘小,文件级复制才能绕过这个限制。

# 复制 Boot 分区
rsync -avHAX /boot/ /mnt/dst_boot/
​
# 复制 Root 根文件系统
# 关键:排除虚拟文件系统和外部挂载点
rsync -avHAX \
  --exclude='/proc/*' \
  --exclude='/sys/*' \
  --exclude='/dev/*' \
  --exclude='/run/*' \
  --exclude='/mnt/*' \
  --exclude='/tmp/*' \
  --exclude='/lost+found/*' \
  --exclude='/vol00/*' \
  --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

4.7 修改 NVMe 分区的 UUID(关键步骤)

Btrfs 文件系统用 UUID 来标识。如果两个分区有相同的 UUID,系统会分不清该挂载哪一个。所以必须给 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

4.8 修改 NVMe 内部的 /etc/fstab

重新挂载 NVMe 的 root 分区:

mount /dev/nvme0n1p2 /mnt/dst_root

备份并修改 fstab,将原来的 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

验证修改结果:

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

可以看到,/ 分区已经正确指向了 NVMe 的新 UUID。

清理挂载:

umount /mnt/dst_root
umount /mnt/dst_boot

4.9 修改 eMMC 引导配置(最关键的一步)

现在需要告诉内核:从 NVMe 启动,而不是从 eMMC。

编辑 eMMC 上的引导配置文件:追加 root=PARTUUID=xxxx

PARTUUID 在分区表层面唯一标识分区,不会因为文件系统重新格式化而改变,因此比 UUID 更稳定。

vim /boot/fnEnv.txt

**修改 **extraargs 这一行,在原有参数后追加 root=PARTUUID=xxxx-xxxx-xxxx-xxxx

修改前:

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

修改后:

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= 后面不要加双引号
  • cma=256Mroot=... 之间必须有空格
  • **确保 PARTUUID 是 NVMe 根分区的值(用 **blkid /dev/nvme0n1p2 确认)

4.10 重启验证

**建议检查 **/boot/fnEnv.txt 中的 PARTUUID 是否与 blkid /dev/nvme0n1p2 输出一致,确保万无一失

sync
reboot

五、验证与性能测试

5.1 确认系统运行在 NVMe 上

# 查看根目录挂载点
df -hT /
# 应显示 /dev/nvme0n1p2
​
# 查看块设备挂载情况
lsblk
# 应显示 nvme0n1p2 挂载在 /,eMMC 的 mmcblk2p2 不应挂载在 /
​
# 查看文件系统类型
mount | grep " / "
# 应显示 btrfs

d3e505c095da7d8fcc8bdcc818ecb575.png

5.2 性能测试

**让我们立刻见证这块 **Intel Optane M10 16GB 在 RK3399 上的真实实力。

**Intel Optane M10 的强项不是顺序读写(受限于 PCIe 2.0 带宽),而是 **4K 随机读写超低延迟。在运行数据库、Docker 容器和系统响应上,体验远超普通 NVMe SSD。

先安装测试工具(如果还没装):

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

5.3 Intel Optane M10 16GB @ NanoPC-T4 测速结果

测试项目 速度/性能 延迟 评价
顺序写入 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 容器的启动和运行也会明显流畅。

六、总结

本方案成功实现了:

  1. 突破容量限制:目标盘小于源盘,用 rsync 完成迁移
  2. 解决引导限制:RK3399 无法 NVMe 启动,采用 eMMC 引导 + 内核参数覆盖
  3. 处理 Btrfs 特性:用 btrfstune 修改 UUID,避免双盘冲突
  4. 最大化性能:Optane M10 跑出 13 万 IOPS,延迟仅 6μs
  5. 延长硬件寿命:eMMC 仅做引导,几乎零磨损

适用场景

  • NanoPC-T4 / Rock Pi 4 等 RK3399 开发板
  • 需要高性能、低延迟的边缘计算节点
  • 希望延长 eMMC 寿命的长期运行设备
  • 手头有小容量 Optane 硬盘闲置的用户

测试日期:2026 年 3 月 22 日** ** 测试者:LECREATE** ** 系统:fnOS 1.1.24

收藏
送赞 1
分享

本帖子中包含更多资源

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

x

0

主题

4

回帖

0

牛值

系统先锋体验团🛩️

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

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

本版积分规则