返回全部文章
KERNEL TRACE / 02Linux 内核 · I/O 一致性

fs freeze 完整行为分析:能否保证数据落到后端 Host 文件

沿上游 Linux 6.6 与开源 Cloud Hypervisor 源码,追踪 FIFREEZE、异步 I/O 等待、virtio FLUSH 到 Host fsync 的完整链路。

分析对象:上游 Linux 6.6 与开源 Cloud Hypervisor

核心问题:Guest 内执行 fsfreeze -f /mnt(即 ioctl(FIFREEZE))返回后,能否保证已写入 fs 的数据都已经落到 Host 上的后端镜像文件?

拆解成三个子问题:

  1. fs freeze 具体做了什么?
  2. 是否会等所有 in-flight 异步 IO(AIO / io_uring / DIO)返回?
  3. 是否会发指令强制后端 VMM 刷盘?

结论(一句话):--disk direct=off + writeback cache + Guest 未挂 -o nobarrier 的标准配置下,是。三个子问题答案全部是"会",且路径可完整对应到 Cloud Hypervisor 源码。


一、fs freeze 到底做了什么

ioctl(fd, FIFREEZE) 的入口在 fs/ioctl.c:387 的 ioctl_fsfreeze(),只是一层薄壳,实际工作全部在 fs/super.c:1960 的 freeze_super() 里完成。

freeze_super() 是一个五态状态机,逐级阻塞不同粒度的写者,每级都必须完成才能推进到下一级:

CODE
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 的设计目标:

c
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:

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():

c
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():

c
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:

CODE
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 处理该命令。核心链路:

CODE
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-932update_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:397writethrough 短路径:write 后立即 disk.flush()
block/src/lib.rs:508异步路径:RequestType::Flush => disk_image.fsync(Some(user_data))
block/src/raw_async.rs:132io_uring opcode::Fsync 提交
block/src/raw_async_aio.rs:115libaio IOCB_CMD_FSYNC 提交
block/src/raw_sync.rs:121同步 libc::fsync(fd)
block/src/qcow_sync.rs:96qcow2 后端的 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 本身是否可靠。


四、完整时序图

SEQUENCE / MERMAID
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 内:

bash
# 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 进程所在主机):

bash
# 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:

  1. sync_filesystem → sync_fs(wait=1) 里 ext4_sync_fs 走 jbd2 事务提交,needs_barrier 时调 blkdev_issue_flush;
  2. 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 可能未刷)。