本文要点
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)但设备不会消失:内核只是把它标记成「最后一个使用者退出后销毁」。正确顺序永远是先umount再losetup -d - ⚠️
losetup -a不是即时快照:detach 之后立刻查往往还能看到设备,等一拍(约 1 秒)才彻底消失,脚本里别用「立刻查一次」判断释放结果 - ⚠️ 设备号不要写死:以
losetup -f --show的返回值为准;另外本机 snap 的 squashfs 就挂在/dev/loop1~loop6上,所以losetup -D(拆掉全部)这种命令碰都不要碰
Linux 环回设备:从 losetup 挂载镜像文件到分区镜像与扩容
手上有 .iso 想看看里面有什么、拿到一块 .img 磁盘镜像想改两个文件、需要一块「临时硬盘」但又不想动分区表——这些需求指向同一个内核机制:环回设备(loop device)。它让一个普通文件在块设备层拥有一个化身,于是 mount、mkfs、fsck、dd 这些只认块设备的工具,全都愿意在文件上干活。
它不是冷门技巧。snap 的每个包就是一个 squashfs 镜像,开机时被 loop 设备挂到 /snap 下;cryptsetup 的加密容器、虚拟机磁盘的离线维护、各种「用文件冒充硬盘」的方案,底层用的都是同一套机制。本文在一台 Ubuntu 22.04 云服务器(KVM,1 核、内存 1.9G,内核 5.15.0-30-generic)上把这条链路完整跑了一遍:从造一个 64M 的空镜像开始,到挂载写入、扩容、再做成分区镜像。文中所有输出、容量数字都是真实执行结果。
环回设备到底解决什么问题
块设备和普通文件的差别,在于内核给它挂了哪套操作接口:块设备支持按扇区随机读写、能被分区表切分、能交给文件系统当「地基」;普通文件只支持 read/write/seek。环回设备做的事,就是在两者之间架一座桥——把一个文件注册成 /dev/loopN,让文件的内容冒充块设备的内容。
于是下面这些场景才有了轻量解法:
- 挂载 ISO:
mount -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
几个值得注意的细节:
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.txtFilesystem 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.txtdf 报的是 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
输出里的三个数字就是全部剧情:
- 第一次
blockdev --getsize64→67108864(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 -q 从标准输入读分区脚本:label: dos 声明分区表类型,,32M,L,* 表示「起始扇区交给工具决定、大小 32M、类型 Linux、可引导」。分区镜像建好后,用起来和真盘一模一样:
mkfs.ext4 -q /dev/loop0p1
mount /dev/loop0p1 /mnt/demo
df -h /mnt/demoFilesystem 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 创建的设备这列是 0,umount 之后设备依然挂着,必须手动释放(这也解释了为什么 losetup -a 里会攒下一堆没人用的设备)。
差别二,mount -o loop 会复用已有关联。 如果这个文件已经被 losetup 显式关联过,mount -o loop 不会新建一个设备,而是直接用那个。此时设备没带 AUTOCLEAR,umount 之后它就会残留下来:
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回收顺序错了会留下悬挂设备。 正确顺序是 umount → losetup -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(文件系统),三步缺一不可; - 释放:
umount→losetup -d <设备>,顺序不能反。
真正需要记住的其实只有两句话:loop 设备是文件的化身,所以文件层面的每一次变化(长大、删除、权限)都要同步想到设备层面;以及内核不会替你猜任何事——文件长大了要 losetup -c 告诉它,设备还挂着就不肯真的 detach。把这两点记住,剩下都是查手册的事。
验证文件系统一致性、抢救损坏的镜像,用的是 e2fsck 那一套,见 Linux 文件系统自检:从 e2fsck 只读体检到备份超级块抢救;如果镜像本身就是从真盘 dd 出来的,克隆与备份的注意事项见 dd 命令:从磁盘克隆到数据备份的完整指南。
评论 (0)
暂无评论,快来抢沙发吧!