分析对象:上游 Linux 6.6 与开源 Cloud Hypervisor
核心问题:Guest 内执行
fsfreeze -f /mnt(即ioctl(FIFREEZE))返回后,能否保证已写入 fs 的数据都已经落到 Host 上的后端镜像文件?拆解成三个子问题:
fs freeze具体做了什么?- 是否会等所有 in-flight 异步 IO(AIO / io_uring / DIO)返回?
- 是否会发指令强制后端 VMM 刷盘?
结论(一句话):
--disk direct=off+writebackcache + Guest 未挂-o nobarrier的标准配置下,是。三个子问题答案全部是"会",且路径可完整对应到 Cloud Hypervisor 源码。
一、fs freeze 到底做了什么
ioctl(fd, FIFREEZE) 的入口在 fs/ioctl.c:387 的 ioctl_fsfreeze(),只是一层薄壳,实际工作全部在 fs/super.c:1960 的 freeze_super() 里完成。
freeze_super() 是一个五态状态机,逐级阻塞不同粒度的写者,每级都必须完成才能推进到下一级:
frozen 状态 (sb->s_writers.frozen):
SB_UNFROZEN (0)
│
▼ 拿 sb->s_umount 写锁
SB_FREEZE_WRITE (1)
│ ① percpu_down_write(rw_sem[0]) ← 阻塞新的 write/mkdir/rename/ioctl
│ 等所有 in-flight 写者释放读锁(含 AIO/io_uring/DIO)
│
▼
SB_FREEZE_PAGEFAULT (2)
│ ② percpu_down_write(rw_sem[1]) ← 阻塞新的写 page fault
│ 等所有正在处理的 pagefault 完成
│
│ ③ sync_filesystem(sb) ← 关键:刷脏 + 等 IO + 下 FLUSH
│
▼
SB_FREEZE_FS (3)
│ ④ percpu_down_write(rw_sem[2]) ← 阻塞 FS 内部事务
│ ⑤ sb->s_op->freeze_fs(sb) ← FS 私有钩子(再一次强制刷盘)
│
▼
SB_FREEZE_COMPLETE (4)
│ ⑥ freeze_holders |= who
│ ⑦ wake_up_var + lockdep 释放
▼
FIFREEZE ioctl 返回给用户态
这三级 percpu_rwsem 分别对应数据面这三个屏障:
sb_start_write()/kiocb_start_write()→ 对应rw_sem[0]sb_start_pagefault()→ 对应rw_sem[1]sb_start_intwrite()→ 对应rw_sem[2](FS 内部事务,比如 ext4 的 jbd2 事务)
任何写者都必须先握对应级别的读锁才能往下走;freeze 路径握写锁,percpu_down_write 的语义就是"等所有读端全部释放,才能拿到写锁"。所以每一级的推进本质上就是等。
二、是否会等异步 IO 返回?—— 会
2.1 异步 IO 也持有 freeze 保护
ioctl(FIFREEZE) 返回前,所有 in-flight 的 AIO / io_uring / DIO 写请求必须完成。这是 Linux 6.6 里 include/linux/fs.h:2792 kiocb_start_write 的设计目标:
static inline void kiocb_start_write(struct kiocb *iocb)
{
struct inode *inode = file_inode(iocb->ki_filp);
sb_start_write(inode->i_sb); // ← 真的持有 rw_sem[0] 读锁
/*
* Fool lockdep by telling it the lock got released so that it
* doesn't complain about the held lock when we return to userspace.
*/
__sb_writers_release(inode->i_sb, SB_FREEZE_WRITE); // ← 只骗 lockdep
}
对应的 AIO 提交与完成路径在 fs/aio.c:
/* fs/aio.c:1585 aio_write() —— io_submit 时抓锁 */
if (!ret) {
if (S_ISREG(file_inode(file)->i_mode))
kiocb_start_write(req); // ① 提交时抓 rw_sem[0] 读锁
req->ki_flags |= IOCB_WRITE;
aio_rw_done(req, call_write_iter(file, req, &iter)); // ② 异步下发,立即返回
}
/* fs/aio.c:1459 aio_complete_rw() —— IO 完成时才放锁 */
if (kiocb->ki_flags & IOCB_WRITE) {
if (S_ISREG(inode->i_mode))
kiocb_end_write(kiocb); // ③ AIO 完成回调才释放读锁
}
关键设计:__sb_writers_release 只是骗 lockdep(否则 io_submit 返回用户态时 lockdep 会抱怨"离开内核态还握着锁"),锁本身一直持有到 aio_complete_rw → kiocb_end_write 才释放。
io_uring 写路径同理,见 io_uring/rw.c:892-900 的 io_kiocb_start_write。
2.2 三重等待
freeze_super() 会通过三重机制保证等到所有 IO 返回:
| 等待手段 | 位置 | 作用 |
|---|---|---|
percpu_down_write(rw_sem[0]) | freeze_super SB_FREEZE_WRITE 阶段 | 等所有 in-flight 同步 write + AIO/io_uring 写的 kiocb_end_write |
percpu_down_write(rw_sem[1]) | freeze_super SB_FREEZE_PAGEFAULT 阶段 | 等所有 write page fault 完成 |
sync_filesystem → sync_blockdev → filemap_fdatawait | 中间步骤 | 兜底等所有 dirty page 写回的 bio 完成 |
只要 AIO 完成回调没走完,percpu_down_write 就会永远阻塞(不响应信号,除了 kill)。这就是为什么 FIFREEZE 有时候会看起来"卡住"—— 它在等某个慢盘的 IO 返回。
2.3 DIO / O_DIRECT 也一样
DIO 走的是同一个 file->f_op->write_iter → 具体 FS 的 ext4_file_write_iter / xfs_file_write_iter → iomap_dio_rw / __blockdev_direct_IO。kiocb_start_write 是在 aio_write() 进入 write_iter 之前就抓的锁,所以 buffered / direct / DAX 三种模式全部被覆盖。
三、是否会强制后端 VMM 刷盘?—— 会
3.1 Guest 侧:两次下 FLUSH
freeze_super() 里有两次明确的 FLUSH 触发点,二者互相兜底:
第一次:sync_filesystem → sync_fs(wait=1) → blkdev_issue_flush
以 ext4 为例,fs/ext4/super.c:6356 ext4_sync_fs():
static int ext4_sync_fs(struct super_block *sb, int wait)
{
...
if (sbi->s_journal) {
target = jbd2_get_latest_transaction(sbi->s_journal);
if (wait && sbi->s_journal->j_flags & JBD2_BARRIER &&
!jbd2_trans_will_send_data_barrier(sbi->s_journal, target))
needs_barrier = true;
if (jbd2_journal_start_commit(sbi->s_journal, &target)) {
if (wait)
ret = jbd2_log_wait_commit(sbi->s_journal, target);
}
} else if (wait && test_opt(sb, BARRIER))
needs_barrier = true;
if (needs_barrier) {
err = blkdev_issue_flush(sb->s_bdev); // ← 显式 FLUSH
}
return ret;
}
第二次:s_op->freeze_fs → ext4_freeze → jbd2_journal_flush
fs/ext4/super.c:6409 ext4_freeze():
static int ext4_freeze(struct super_block *sb)
{
if (journal) {
jbd2_journal_lock_updates(journal);
error = jbd2_journal_flush(journal, 0); // ← 完整 checkpoint + FLUSH
...
}
}
xfs 侧对应 fs/xfs/xfs_super.c:928 xfs_fs_freeze() → xfs_log_quiesce(mp),其内部 xlog_sync 提交的日志 IO 直接带 REQ_PREFLUSH|REQ_FUA flag,语义等价于"写前 flush + 写后强制到盘",比单独 blkdev_issue_flush 更强。
3.2 Guest block 层:FLUSH 转 virtio 命令
blkdev_issue_flush 走到 block/blk-flush.c:477:
blkdev_issue_flush(bdev)
└─ submit_bio_wait(REQ_OP_WRITE | REQ_PREFLUSH) ← 同步阻塞等
└─ blk_mq_submit_bio
└─ blk_insert_flush(rq) block/blk-flush.c:404
│
│ 判断 blk_queue_write_cache 的 writeback 位:
│ - writeback=true → 生成独立的 flush 请求,转发给驱动
│ - writeback=false → 直接完成,不下发(写透模式无需 flush)
▼
virtio_blk 驱动的 queue_rq
▼
virtblk_prep_rq() drivers/block/virtio_blk.c:263
case REQ_OP_FLUSH:
type = VIRTIO_BLK_T_FLUSH; ← 命令类型翻译
▼
virtqueue_add_sgs + virtqueue_kick 触发 eventfd 通知 Cloud Hypervisor
Guest 侧 cache 模式由 virtio_blk.c:1146 virtblk_update_cache_mode 从 Host 通告的 feature 里读取,writeback=true 才会真的下发 FLUSH 到后端。
3.3 Cloud Hypervisor 侧:FLUSH → fsync
Guest kick eventfd 后,Cloud Hypervisor 的 BlockEpollHandler 处理该命令。核心链路:
virtqueue kick eventfd
▼
BlockEpollHandler::handle_event(QUEUE_AVAIL_EVENT) virtio-devices/src/block.rs
▼
process_queue() 遍历 avail ring
▼
Request::parse() block/src/lib.rs
parse_request_type: VIRTIO_BLK_T_FLUSH → RequestType::Flush
▼
Request::execute_async() block/src/lib.rs:508
match request_type {
RequestType::Flush => {
disk_image.fsync(Some(user_data))?; ← 后端 fsync 入口
}
...
}
▼
AsyncIo::fsync (trait) block/src/async_io.rs:188
│
├── raw_async.rs:132 io_uring opcode::Fsync → 内核 fsync(2)
├── raw_async_aio.rs:115 libaio IOCB_CMD_FSYNC → 内核 fsync(2)
├── raw_sync.rs:121 libc::fsync(fd) → 内核 fsync(2)
├── qcow_sync.rs:96 qcow2 元数据 flush + libc::fsync
└── nvme_tcp.rs / vhdx / fixed_vhd 等其它后端
▼
Host 内核 fsync(2):
→ 写脏页 + 提交块层 FLUSH
→ 物理盘 SCSI SYNCHRONIZE CACHE / NVMe FLUSH
▼
fsync 返回 → Cloud Hypervisor 收到 CQE / eventfd
▼
process_queue_complete():写 used ring + queue.signal_used()
▼
Guest virtio_blk 收到 MSI-X 中断
▼
blk_mq_complete_request → bio_endio → submit_bio_wait 唤醒
▼
blkdev_issue_flush 返回 → ext4_sync_fs 返回
▼
freeze_super 继续推进 → SB_FREEZE_COMPLETE
▼
ioctl(FIFREEZE) 返回用户态
Cloud Hypervisor 关键代码定位:
| 位置 | 作用 |
|---|---|
| virtio-devices/src/block.rs:183 | 未协商 F_FLUSH 时拒绝 Flush 请求 |
| virtio-devices/src/block.rs:785-793 | 特性协商:direct=off 时通告 F_FLUSH + F_CONFIG_WCE |
| virtio-devices/src/block.rs:915-932 | update_writeback():根据 config.wce / feature ack 决定 writeback |
| virtio-devices/src/block.rs:286 | 每次处理请求前 request.set_writeback(...) 传递给 block 层 |
| block/src/lib.rs:400 | 同步路径:RequestType::Flush => disk.flush() |
| block/src/lib.rs:397 | writethrough 短路径:write 后立即 disk.flush() |
| block/src/lib.rs:508 | 异步路径:RequestType::Flush => disk_image.fsync(Some(user_data)) |
| block/src/raw_async.rs:132 | io_uring opcode::Fsync 提交 |
| block/src/raw_async_aio.rs:115 | libaio IOCB_CMD_FSYNC 提交 |
| block/src/raw_sync.rs:121 | 同步 libc::fsync(fd) |
| block/src/qcow_sync.rs:96 | qcow2 后端的 fsync |
3.4 Cloud Hypervisor 没有"丢弃 FLUSH"的开关
与 QEMU 不同,Cloud Hypervisor 没有 cache=unsafe / cache.no-flush=on 这类公开的忽略 FLUSH 选项。只要 direct=off(默认)且 Guest 协商了 F_FLUSH,RequestType::Flush 就一定会调 disk_image.fsync(),最终一定会调到 Host 内核 fsync(2)。这意味着落盘保证只取决于 Host 内核 fsync 本身是否可靠。
四、完整时序图
sequenceDiagram
autonumber
participant App as Guest App
participant Freeze as freeze_super
participant Sync as sync_filesystem
participant Ext4 as ext4_sync_fs<br/>+ ext4_freeze
participant JBD as jbd2
participant Blk as Guest block 层
participant Vblk as virtio_blk 前端
participant Ring as virtqueue
participant VMM as Cloud Hypervisor<br/>BlockEpollHandler
participant Async as AsyncIo 后端<br/>(io_uring/aio/sync)
participant HK as Host 内核 fsync
participant Disk as Host 物理盘
App->>Freeze: ioctl(FIFREEZE)
Note over Freeze: ① SB_FREEZE_WRITE<br/>percpu_down_write[0]
Freeze-->>Freeze: 等 in-flight 同步/AIO/io_uring 写<br/>直到 kiocb_end_write 释放读锁
Note over Freeze: ② SB_FREEZE_PAGEFAULT<br/>percpu_down_write[1]
Freeze-->>Freeze: 等所有写 page fault 完成
Freeze->>Sync: ③ sync_filesystem(sb)
Sync->>Ext4: sync_fs(wait=1)
Ext4->>JBD: jbd2_journal_start_commit<br/>+ jbd2_log_wait_commit
JBD->>Blk: 日志 IO<br/>(REQ_PREFLUSH|REQ_FUA)
Blk->>Vblk: REQ_OP_WRITE + FLUSH
Vblk->>Ring: 提交描述符
Ring->>VMM: kick eventfd
VMM->>Async: execute_async<br/>disk_image.fsync(user_data)
Async->>HK: io_uring Fsync / IOCB_CMD_FSYNC / libc::fsync
HK->>Disk: 写日志 + 物理盘 FLUSH
Disk-->>HK: 完成
HK-->>Async: fsync 返回
Async-->>VMM: CQE / eventfd
VMM-->>Vblk: 写 used ring + MSI-X
Vblk-->>JBD: 完成
Ext4->>Blk: needs_barrier → blkdev_issue_flush
Blk->>Vblk: REQ_OP_FLUSH
Vblk->>Ring: VIRTIO_BLK_T_FLUSH
Ring->>VMM: kick
VMM->>Async: disk_image.fsync
Async->>HK: fsync(host_fd)
HK->>Disk: FLUSH
Disk-->>HK: 完成
HK-->>VMM: 完成
VMM-->>Vblk: MSI-X
Sync->>Blk: sync_blockdev(sb->s_bdev)<br/>filemap_fdatawait 兜底等
Sync-->>Freeze: 返回
Note over Freeze: ④ SB_FREEZE_FS<br/>percpu_down_write[2]
Freeze->>Ext4: ⑤ s_op->freeze_fs(sb)
Ext4->>JBD: jbd2_journal_lock_updates<br/>+ jbd2_journal_flush(0)
Note over JBD: 完整 checkpoint<br/>再次下 FLUSH
JBD->>Blk: 写元数据到最终位置 + FLUSH
Blk->>Vblk: REQ_OP_WRITE + REQ_OP_FLUSH
Vblk->>Ring: 命令
Ring->>VMM: kick
VMM->>Async: 写 + fsync
Async->>HK: pwritev + fsync
HK->>Disk: 写 + FLUSH
Disk-->>HK: 完成
HK-->>VMM: 完成
VMM-->>Vblk: MSI-X
Ext4-->>Freeze: freeze_fs 返回
Note over Freeze: SB_FREEZE_COMPLETE
Freeze-->>App: FIFREEZE 返回<br/>此时数据已落到 Host 物理盘
五、能否保证落盘 —— 场景矩阵
| 场景 | 是否落到 Host 物理盘 | 原因 |
|---|---|---|
--disk direct=off + Guest 协商 F_FLUSH + Guest 无 nobarrier | ✅ 是 | 标准配置,路径如上,两次 FLUSH → Cloud Hypervisor fsync() → Host 内核块层 FLUSH → 物理盘 |
--disk direct=on(O_DIRECT) | ✅ 是(每次写已到盘) | Cloud Hypervisor 不通告 F_FLUSH,Guest 侧 blk_insert_flush 短路;但写路径本身已 O_DIRECT,绕过 Host page cache,每次写就已下发到盘 |
Guest 挂载 -o nobarrier | ❌ 否 | ext4 test_opt(sb, BARRIER)=false,needs_barrier=false;Guest 侧根本不下 FLUSH。数据可能仍在 Host page cache 中 |
Guest 通过 sysfs 把 cache_type 改为 write through | ✅ 是(每次写就落盘) | Cloud Hypervisor 支持 F_CONFIG_WCE,Guest 写 config.wce=0 触发 update_writeback() 关闭 writeback,写路径每次都 disk.flush()(block/src/lib.rs:397) |
virtio-blk 未协商 F_FLUSH(旧 Guest 内核) | ❌ 否 | blk_queue_write_cache(q, false, ...),Guest block 层短路所有 flush 请求 |
| 后端是 tmpfs 上的 raw 镜像 | ❌ 不适用 | tmpfs 无持久化,fsync 也无意义 |
后端是 nvme_tcp.rs(NVMe/TCP) | ✅ 是(依赖对端) | fsync 语义翻译为 NVMe FLUSH 命令发到远端 target |
| Guest FS 是 tmpfs / ramfs | ❌ 不适用 | 不下 block IO |
六、自检命令
Guest 内:
# 1. 确认 cache 模式:只有 write back 才会下发 FLUSH
cat /sys/block/vda/queue/write_cache
# 期望:write back
# 2. 确认挂载选项没有 nobarrier
mount | grep -E "on / |on /data" | grep -v nobarrier
# 若有 nobarrier / barrier=0 → Guest 侧根本不下 FLUSH
# 3. 触发 freeze 并追踪 flush 次数
sudo bpftrace -e '
kprobe:blkdev_issue_flush { @[comm] = count(); }
interval:s:10 { print(@); clear(@); }' &
sudo fsfreeze -f /mnt/data
sudo fsfreeze -u /mnt/data
# 期望能看到至少一次 blkdev_issue_flush
Host 内(Cloud Hypervisor 进程所在主机):
# 1. 追踪 Cloud Hypervisor 的 fsync 系统调用
sudo bpftrace -e '
tracepoint:syscalls:sys_enter_fsync /comm=="cloud-hypervisor"/ {
printf("fsync fd=%d pid=%d ts=%lu\n", args->fd, pid, nsecs);
}
tracepoint:syscalls:sys_exit_fsync /comm=="cloud-hypervisor"/ {
printf("fsync return=%d ts=%lu\n", args->ret, nsecs);
}'
# 在 Guest 内执行 fsfreeze -f 时应能看到 fsync 的入口/退出对
# 2. 观察 io_uring 提交(如果用 io_uring 后端)
sudo strace -f -e trace=io_uring_enter -p <cloud-hypervisor-pid> 2>&1 | head
# 3. 确认镜像文件的 dirty page 已回落
sudo grep -E "^(Dirty|Writeback):" /proc/meminfo # freeze 后 Dirty 应降至底部平台
七、最终结论
回到最初的三个子问题:
7.1 fs freeze 做了什么
freeze_super() 通过五态状态机 + 三级 percpu_rwsem 逐级阻塞写者(普通写 → 写 page fault → FS 内部事务),中间 sync_filesystem(sb) 兜底刷脏,最后调 s_op->freeze_fs(sb) 让 FS 做私有的完整 checkpoint(ext4:jbd2_journal_flush;xfs:xfs_log_quiesce)。冻结完成后新写者全部挂在读锁上,直到 FITHAW 才释放。
7.2 是否等异步 IO 返回
会。异步写路径通过 kiocb_start_write 在 io_submit / io_uring 提交时抓 SB_FREEZE_WRITE 级读锁,只是用 __sb_writers_release 骗过 lockdep(让 io_submit 能安全返回用户态),锁实际持有到 aio_complete_rw → kiocb_end_write 才释放。freeze_super() 的 percpu_down_write(rw_sem[0]) 语义就是"等所有读端释放",因此在最后一个 in-flight AIO 完成回调走完之前,FIFREEZE 会一直阻塞。DIO / O_DIRECT 同理,因为 kiocb_start_write 在 write_iter 之前就抓锁了。
7.3 是否强制后端 VMM 刷盘
会,且下发两次 FLUSH:
sync_filesystem → sync_fs(wait=1)里ext4_sync_fs走jbd2事务提交,needs_barrier时调blkdev_issue_flush;s_op->freeze_fs → ext4_freeze里jbd2_journal_flush做完整 checkpoint,再一次下 FLUSH。
FLUSH 经 Guest block 层 blk_insert_flush 转成独立 flush 请求,virtio_blk 驱动 virtblk_prep_rq 把 REQ_OP_FLUSH 翻译为 VIRTIO_BLK_T_FLUSH,通过 virtqueue kick eventfd 到 Cloud Hypervisor。Cloud Hypervisor 的 BlockEpollHandler::process_queue → Request::execute_async 命中 RequestType::Flush 分支,调 disk_image.fsync(Some(user_data)),最终由 io_uring opcode::Fsync / libaio IOCB_CMD_FSYNC / libc::fsync 调 Host 内核 fsync(2)。Host 内核提交块层 FLUSH(SCSI SYNCHRONIZE CACHE / NVMe FLUSH)到物理盘。Cloud Hypervisor 没有 cache=unsafe 这类可忽略 FLUSH 的开关,一旦协商了 F_FLUSH,RequestType::Flush 一定会调到 Host fsync(2)。
7.4 能否保证落到 Host 后端文件
在下面所有条件同时满足时可以保证:
- Cloud Hypervisor
--disk direct=off(会通告F_FLUSH + F_CONFIG_WCE); - Guest kernel 支持并 ack 了
VIRTIO_BLK_F_FLUSH(上游 Linux 6.6 已支持); - Guest cache 处于 writeback 模式(
cat /sys/block/vda/queue/write_cache显示write back); - Guest 未挂载
-o nobarrier; - Host 内核
fsync(2)本身实现正确(标准 kernel 均可靠); - Host 底层存储真实响应 FLUSH(正常 SATA/NVMe 盘、cephfs/xfs 等文件系统均可靠)。
只要有任一条件不满足(尤其是 nobarrier 或 Guest 侧未协商 FLUSH),FIFREEZE 返回不代表数据一定在 Host 物理盘上,可能还停留在某一层缓存(Guest page cache 通常已刷,但 Host page cache 或存储控制器 volatile cache 可能未刷)。