暗色模式

Linux 环回设备:从 losetup 挂载镜像文件到分区镜像与扩容

技术教程
2026-09-24
5
0
本文要点
  • losetup 干的事是把一个普通文件注册成块设备:truncate 造文件 → mkfs.ext4 写文件系统 → losetup -f --show 关联 → mount /dev/loop0。走完这条链,一个文件就获得了分区的一切待遇
  • 镜像长大了内核不会自己知道:实测 truncate -s 96M 之后 blockdev --getsize64 /dev/loop0 仍然报 67108864,必须先 losetup -c 让内核重读容量,再 resize2fs 把文件系统撑满
  • losetup -P 会自动扫描镜像里的分区表并生成 /dev/loop0p1 这样的子设备——ISO、虚拟机磁盘、树莓派 img 都靠它挂载
  • ⚠️ 对已挂载的 loop 设备执行 losetup -d,命令返回成功(rc=0)但设备不会消失:内核只是把它标记成「最后一个使用者退出后销毁」。正确顺序永远是先 umountlosetup -d
  • ⚠️ losetup -a 不是即时快照:detach 之后立刻查往往还能看到设备,等一拍(约 1 秒)才彻底消失,脚本里别用「立刻查一次」判断释放结果
  • ⚠️ 设备号不要写死:以 losetup -f --show 的返回值为准;另外本机 snap 的 squashfs 就挂在 /dev/loop1~loop6 上,所以 losetup -D(拆掉全部)这种命令碰都不要碰

Linux 环回设备:从 losetup 挂载镜像文件到分区镜像与扩容

手上有 .iso 想看看里面有什么、拿到一块 .img 磁盘镜像想改两个文件、需要一块「临时硬盘」但又不想动分区表——这些需求指向同一个内核机制:环回设备(loop device)。它让一个普通文件在块设备层拥有一个化身,于是 mountmkfsfsckdd 这些只认块设备的工具,全都愿意在文件上干活。

它不是冷门技巧。snap 的每个包就是一个 squashfs 镜像,开机时被 loop 设备挂到 /snap 下;cryptsetup 的加密容器、虚拟机磁盘的离线维护、各种「用文件冒充硬盘」的方案,底层用的都是同一套机制。本文在一台 Ubuntu 22.04 云服务器(KVM,1 核、内存 1.9G,内核 5.15.0-30-generic)上把这条链路完整跑了一遍:从造一个 64M 的空镜像开始,到挂载写入、扩容、再做成分区镜像。文中所有输出、容量数字都是真实执行结果。

环回设备到底解决什么问题

块设备和普通文件的差别,在于内核给它挂了哪套操作接口:块设备支持按扇区随机读写、能被分区表切分、能交给文件系统当「地基」;普通文件只支持 read/write/seek。环回设备做的事,就是在两者之间架一座桥——把一个文件注册成 /dev/loopN,让文件的内容冒充块设备的内容

于是下面这些场景才有了轻量解法:

  • 挂载 ISOmount -o loop xxx.iso /mnt 是大多数人第一次接触 loop 的地方;
  • 虚拟机磁盘:KVM 的 qcow2/raw 镜像要离线改配置,先 losetup -P 再挂分区;
  • 磁盘镜像取证/维修:拿到一块盘的 dd 镜像,用 loop 挂起来只读分析,不必还原到真盘;
  • 没有空闲分区时先顶上:云服务器只有一块盘、也没做 LVM,临时需要一块 ext4 就用镜像文件代替;
  • 配合加密cryptsetup 的容器底层也是文件,见 Linux 磁盘加密:从 LUKS 原理到 cryptsetup 加密与解锁挂载

要说明的是,它替代不了真正的磁盘管理。镜像文件的安全性、权限、备份策略全都落在那个文件上,而分区、扩容这些操作在镜像里依然要按老规矩来(Linux 磁盘挂载:从 fdisk 分区到 fstab 开机自动挂载LVM 逻辑卷管理:从物理卷到在线扩容与快照回滚 里的规则同样适用)。

第一步:造镜像、格式化、关联设备

三件事:先用 truncate 造一个 64M 的文件(truncate 只是把文件长度标到 64M,不实际写 64M 数据,所以瞬间完成);再直接对文件 mkfs.ext4;最后用 losetup -f --show 找到空闲设备并关联,它会把设备名打印出来。

truncate -s 64M /tmp/demo.img
mkfs.ext4 -q -L DEMODISK /tmp/demo.img
losetup -f --show /tmp/demo.img
losetup -l

truncate 造 64M 镜像、mkfs.ext4 格式化、losetup -f --show 返回 /dev/loop0,losetup -l 显示本机 loop 设备表

几个值得注意的细节:

  • mkfs.ext4 直接作用在文件上完全合法(mke2fs 接受普通文件),但这不代表文件已经是块设备了——要挂载还是得有关联好的 loop 设备-L DEMODISK 顺手打了个卷标,方便以后用 LABEL= 定位。
  • losetup -f 是「找一个空闲的 loop 设备」,--show 让它在关联完成后把设备名打印出来。本次拿到的是 /dev/loop0,但这个号码不保证,永远以命令输出为准。
  • losetup -l 列出的表格里,BACK-FILE 是后端文件;AUTOCLEAR 那一列是关键(后面会用到),RO 表示只读挂载。
  • 表格里 /dev/loop1~/dev/loop6 是 snap 的 squashfs 镜像——这台机器上的 loop 设备不只属于你,这就是不能用 losetup -D 的原因。

第二步:挂载、写入、卸载

关联好之后,/dev/loop0 就是一个货真价实的块设备了,挂载方式和真分区没有区别:

mkdir -p /mnt/demo
mount /dev/loop0 /mnt/demo
df -h /mnt/demo
echo 'hello loop' > /mnt/demo/note.txt
ls -l /mnt/demo/note.txt
Filesystem      Size  Used Avail Use% Mounted on
/dev/loop0       56M   24K   52M   1% /mnt/demo
-rw-r--r-- 1 root root 11 Sep 18 21:01 /mnt/demo/note.txt

df 报的是 56M 而不是 64M:ext4 自己的元数据(inode 表、位图、日志区)和默认 5% 的保留块都不算进可用空间,这是正常现象,不是镜像缩水了。

写进去的 note.txt 实际躺在 /tmp/demo.img 这个文件里。卸载并释放设备:

umount /mnt/demo
losetup -d /dev/loop0

这里有两个坑,都是实测踩出来的:

第一,losetup -d 对已挂载的设备会「假装成功」。 对着一台还挂着的 loop 设备执行 detach,命令返回 0,但设备并不会消失:

losetup -d /dev/loop0; echo "detach rc=$?"     # 设备其实还挂着
losetup -a | grep demo.img                     # 依然能查到
detach rc=0
/dev/loop0: [64513]:1534 (/tmp/demo.img)

内核的语义是「把它标记为最后一个使用者退出后销毁」,所以 umount 必须在前。指望用 losetup -d 去「强制拔出」一个正在使用的文件系统,只会得到一条成功的假消息。

第二,detach 不是瞬时的。 无论 umount 触发的自动释放,还是手动的 losetup -d,紧接着执行 losetup -a 往往仍能看到那个设备,约 1 秒后才彻底消失。判断「是否真的释放干净」,要么等一拍再查,要么用 lsblk 复核设备是否还在。

第三步:镜像长大了,得手动告诉内核

这是环回设备最容易被忽略的一环。把镜像文件扩到 96M,然后看内核眼中的设备容量:

blockdev --getsize64 /dev/loop0
truncate -s 96M /tmp/demo.img
blockdev --getsize64 /dev/loop0
losetup -c /dev/loop0
blockdev --getsize64 /dev/loop0
e2fsck -f -p /dev/loop0
resize2fs /dev/loop0 2>&1
mount /dev/loop0 /mnt/demo && df -h /mnt/demo

truncate 扩容后 blockdev 仍报 67108864,losetup -c 之后变为 100663296,再 resize2fs 把 ext4 撑到 88M

输出里的三个数字就是全部剧情:

  • 第一次 blockdev --getsize6467108864(64M),这是扩容前的容量;
  • truncate -s 96M 之后再查还是 67108864——设备容量是关联那一刻记下来的,文件变大了内核并不知道;
  • losetup -c--set-capacity)让内核重新读一次后端文件的长度 → 100663296(96M)。

容量对上之后才轮到文件系统:e2fsck -f -p 先做一致性检查(-f 强制全检、-p 自动修复并只输出一行摘要),resize2fs 把 ext4 撑满新容量。df 从 56M 变成 88M,而先前写进去的 note.txt 原封不动——扩容是无损的

顺带说明:e2fsck 不是 resize2fs 的硬性前置条件,在干净的文件系统上直接 resize2fs 也会成功(实测 rc=0),但一旦文件系统有隐患,跳过检查的代价可能是元数据损坏。这一步别省。

第四步:带分区表的镜像,用 losetup -P

前面造的是「裸文件系统」镜像(文件开头就是 ext4 超级块)。但真实世界的镜像通常是整盘镜像:开头是分区表,分区在偏移量若干 MB 之后才开始。这时直接 losetup + mount 会失败——因为文件开头不是文件系统。

losetup -P--partscan)解决的正是这个问题:它让内核扫描镜像里的分区表,并自动生成 /dev/loop0p1/dev/loop0p2 这样的子设备。

truncate -s 64M /tmp/part.img
printf 'label: dos\n,32M,L,*\n' | sfdisk -q /tmp/part.img
losetup -P -f --show /tmp/part.img
fdisk -l /dev/loop0

sfdisk 给镜像写 DOS 分区表,losetup -P 关联后 fdisk -l 显示 /dev/loop0 下有 32M 的 loop0p1 分区

sfdisk -q 从标准输入读分区脚本:label: dos 声明分区表类型,,32M,L,* 表示「起始扇区交给工具决定、大小 32M、类型 Linux、可引导」。分区镜像建好后,用起来和真盘一模一样:

mkfs.ext4 -q /dev/loop0p1
mount /dev/loop0p1 /mnt/demo
df -h /mnt/demo
Filesystem      Size  Used Avail Use% Mounted on
/dev/loop0p1     26M   24K   24M   1% /mnt/demo

如果 lsblk 里只看到 loop0 却看不到 loop0p1,说明设备是在扫描分区表之前关联的:要么重新用 -P 关联一次,要么对已关联的设备补一条 partprobe /dev/loop0(或 partx -u /dev/loop0)让它重新读表。这个现象在「先 losetup、后用 fdisk 在镜像里分区」的顺序下必然出现。

mount -o loop 与显式 losetup 的区别

日常最省事的写法是让 mount 自己搞定 loop:

mount -o loop /tmp/demo.img /mnt/demo

它确实会替你找一个空闲 loop 设备关联上,但和显式 losetup 有两个实质差别,混用时会咬人:

差别一,自动创建的设备带 AUTOCLEAR 标记。 losetup -l 能看到 AUTOCLEAR 那列是 1,含义是「最后一个使用者退出后自动销毁」——所以 umount 一执行,设备自己就释放了,不用再 losetup -d。而显式 losetup 创建的设备这列是 0umount 之后设备依然挂着,必须手动释放(这也解释了为什么 losetup -a 里会攒下一堆没人用的设备)。

差别二,mount -o loop 会复用已有关联。 如果这个文件已经被 losetup 显式关联过,mount -o loop 不会新建一个设备,而是直接用那个。此时设备没带 AUTOCLEARumount 之后它就会残留下来:

losetup -f --show /tmp/demo.img     # 显式关联, 拿到 /dev/loop7
mount -o loop /tmp/demo.img /mnt/demo   # 复用 loop7, 不新建
umount /mnt/demo                        # 不会自动释放
losetup -a | grep demo.img              # loop7 仍在

结论很简单:要么全程用显式 losetup + mount /dev/loopN,要么全程用 mount -o loop,别在同一文件上两种混着来。 需要精细控制(只读、指定设备号、分区扫描)时用前者;图省事、一次性挂载 ISO 时用后者。

几个必须知道的坑

别用 losetup -D -D 是「拆掉所有 loop 设备」,而这台机器上 snap 的 squashfs 就挂在 /dev/loop1~/dev/loop6。一条 losetup -D 会把它们一起摘掉,轻则 snap 应用异常,重则连 snapd 都要重来。释放永远用 losetup -d <具体设备>

设备号不保证稳定。 演示里一直是 /dev/loop0,但那是「当时它空闲」的结果;只要前面的设备 detach 得晚一点,losetup -f --show 就可能返回 /dev/loop7。脚本里请用变量接住:

DEV=$(losetup -f --show /tmp/demo.img)
mount "$DEV" /tmp/demo-mnt

回收顺序错了会留下悬挂设备。 正确顺序是 umountlosetup -d。反过来执行,losetup -d 会返回成功却不生效,之后你再 rm 掉镜像文件,就会得到一个「后端文件已删除但仍占用设备号」的僵尸 loop 设备(losetup -a 里显示为 (/tmp/demo.img (deleted))),只能靠 umount 之后再 detach 来收拾。

镜像文件是普通文件,权限就是它的安全边界。 谁能读这个文件,谁就能拿到里面的全部数据——chmod 600 是底线;要防的是「拿到文件也读不懂」,才需要 LUKS 这类加密层。另外删镜像前务必确认设备已释放,否则 df 上会挂着一个指向已删除文件的文件系统(Linux 磁盘空间排查与清理:从 df 到 journald 的完整救火流程 里那类「空间凭空消失」的案例,一部分就是这么来的)。

用完记得连镜像一起清理。 一个 64M 镜像不算什么,但测试脚本反复跑、每个都忘删,在 7.6G 磁盘的小机器上很快会见底。

小结

把整条链路和对应命令收在一张表里:

  • 造镜像truncate -s 64M /tmp/demo.img(瞬间完成,不写实际数据)、mkfs.ext4 -L DEMODISK /tmp/demo.img
  • 关联设备losetup -f --show <文件> 拿设备名、losetup -l 看全表、分区镜像加 -P
  • 挂载使用mount /dev/loop0 /mnt/demo,用法与真分区完全一致;
  • 扩容truncate -s 96M(文件)→ losetup -c(设备)→ e2fsck -f -p + resize2fs(文件系统),三步缺一不可;
  • 释放umountlosetup -d <设备>,顺序不能反。

真正需要记住的其实只有两句话:loop 设备是文件的化身,所以文件层面的每一次变化(长大、删除、权限)都要同步想到设备层面;以及内核不会替你猜任何事——文件长大了要 losetup -c 告诉它,设备还挂着就不肯真的 detach。把这两点记住,剩下都是查手册的事。

验证文件系统一致性、抢救损坏的镜像,用的是 e2fsck 那一套,见 Linux 文件系统自检:从 e2fsck 只读体检到备份超级块抢救;如果镜像本身就是从真盘 dd 出来的,克隆与备份的注意事项见 dd 命令:从磁盘克隆到数据备份的完整指南

发表评论

暂无评论,快来抢沙发吧!