Skip to content

Latest commit

 

History

History
2623 lines (2100 loc) · 88 KB

File metadata and controls

2623 lines (2100 loc) · 88 KB

Linux 内核 virtiofs 与 9P 文件系统深度分析

分析基于 Linux 内核主线代码(commit 8a30aeb0d),重点覆盖 fs/fuse/virtio_fs.cfs/fuse/fuse_i.hnet/9p/client.cinclude/net/9p/9p.hfs/9p/vfs_inode.cnet/9p/trans_virtio.c 等文件。


目录

  1. virtiofs 总体架构
  2. 核心数据结构
  3. FUSE 协议在 virtio ring 上的传输
  4. DAX 模式与零拷贝映射
  5. FUSE_SETUPMAPPING 与 FUSE_REMOVEMAPPING
  6. 9P 协议基础
  7. 9P 核心数据结构
  8. 9P virtio 传输层
  9. 9P VFS 集成
  10. virtiofs 与 9P 对比
  11. 挂载流程分析
  12. 性能特性与调优
  13. virtiofs 队列深度设计
  14. DAX window 内存管理机制
  15. 9P TCP 传输层与 fd 传输层
  16. virtiofs 安全模型与 namespace 隔离
  17. xattr 与 ACL 支持
  18. virtiofs 卸载与热拔插流程
  19. fuse_conn 能力位协商详解
  20. 9P 协议编解码机制
  21. DAX 与非 DAX 路径性能对比
  22. Kata Containers 场景下的应用
  23. sysfs 接口与可观测性
  24. 附录:关键文件路径索引

1. virtiofs 总体架构

1.1 系统层次

virtiofs(VIRTIO_ID_FS,设备 ID 26)是专为虚拟机环境设计的文件系统,将 FUSE 协议与 virtio 传输层结合,使 guest 内核能够访问 host 用户态守护进程(virtiofsd)所暴露的文件系统。

+----------------------------------------------------------------------+
|                          Guest Kernel                                |
|                                                                      |
|  应用进程                                                             |
|     |  syscall (read/write/open...)                                  |
|     v                                                                |
|  VFS 层 (fs/fuse/)                                                   |
|     |  fuse_request                                                  |
|     v                                                                |
|  FUSE 连接层 (fuse_conn / fuse_iqueue)                               |
|     |  virtio_fs_fiq_ops.send_req()                                  |
|     v                                                                |
|  virtio-fs 驱动 (fs/fuse/virtio_fs.c)                               |
|     |  virtio_fs_enqueue_req()                                       |
|     v                                                                |
|  virtio ring (virtqueue_add_sgs / virtqueue_kick)                   |
|     |  共享内存 + 通知机制                                            |
+-----|----------------------------------------------------------------+
      |  PCI/MMIO (virtio 设备)
+-----|----------------------------------------------------------------+
|     v                          Host Kernel                           |
|  vhost-user / vhost (内核态 vhost 或用户态 socket)                  |
+-----|----------------------------------------------------------------+
      |
+-----|----------------------------------------------------------------+
|     v                          Host Userspace                        |
|  virtiofsd (FUSE 服务端守护进程)                                     |
|     |  passthrough 到 host 文件系统                                  |
|     v                                                                |
|  Host 文件系统 (ext4 / xfs / overlayfs ...)                         |
+----------------------------------------------------------------------+

1.2 设备配置结构

include/uapi/linux/virtio_fs.h(第 11-17 行)定义了设备配置空间:

struct virtio_fs_config {
    /* Filesystem name (UTF-8, not NUL-terminated, padded with NULs) */
    __u8 tag[36];

    /* Number of request queues */
    __le32 num_request_queues;
} __attribute__((packed));
  • tag:文件系统标签,用于 mount 时的 source 匹配(最长 36 字节 UTF-8)
  • num_request_queues:设备支持的请求队列数,驱动会截断为 nr_cpu_ids

设备 ID 定义在 include/uapi/linux/virtio_ids.h

#define VIRTIO_ID_FS  26  /* virtio filesystem */

DAX 共享内存 cap ID(include/uapi/linux/virtio_fs.h:20):

#define VIRTIO_FS_SHMCAP_ID_CACHE 0

1.3 Virtqueue 布局

fs/fuse/virtio_fs.c 第 40-43 行定义了队列编号枚举:

enum {
    VQ_HIPRIO,    /* 索引 0: 高优先级队列(FORGET 请求)*/
    VQ_REQUEST    /* 索引 1+: 普通请求队列,支持多队列 */
};

多队列映射通过 virtio_fs_map_queues()(第 859 行)建立,将 CPU 映射到请求队列:

CPU 0 --> requests.0 (VQ 索引 1)
CPU 1 --> requests.1 (VQ 索引 2)
...
CPU N --> requests.K (VQ 索引 1+K)

映射表 fs->mq_map[cpu_id] = vq_index 在发送请求时通过 fs->mq_map[raw_smp_processor_id()] 查找。


2. 核心数据结构

2.1 virtio_fs 结构

fs/fuse/virtio_fs.c 第 64-80 行:

struct virtio_fs {
    struct kobject kobj;
    struct kobject *mqs_kobj;
    struct list_head list;        /* 挂到全局 virtio_fs_instances 链表 */
    char *tag;                    /* 文件系统标签名 */
    struct virtio_fs_vq *vqs;     /* 所有 virtqueue 的状态数组 */
    unsigned int nvqs;            /* virtqueue 总数 */
    unsigned int num_request_queues; /* 请求队列数量 */
    struct dax_device *dax_dev;   /* DAX 设备(共享内存窗口) */

    unsigned int *mq_map;         /* CPU -> 请求队列 ID 的映射表 */

    /* DAX 内存窗口,文件内容在此映射 */
    void *window_kaddr;           /* 内核虚拟地址 */
    phys_addr_t window_phys_addr; /* 物理地址 */
    size_t window_len;            /* 窗口大小 */
};

全局实例链表和互斥锁(第 34-35 行):

static DEFINE_MUTEX(virtio_fs_mutex);
static LIST_HEAD(virtio_fs_instances);

kset 用于 sysfs 目录管理(第 38 行):

static struct kset *virtio_fs_kset;  /* /sys/fs/virtio_fs/ */

2.2 virtio_fs_vq 结构

fs/fuse/virtio_fs.c 第 48-61 行,____cacheline_aligned_in_smp 修饰保证 cache line 对齐:

struct virtio_fs_vq {
    spinlock_t lock;
    struct virtqueue *vq;          /* 底层 virtio 队列 */
    struct work_struct done_work;  /* 完成处理工作队列 */
    struct list_head queued_reqs;  /* 因队列满而等待的请求 */
    struct list_head end_reqs;     /* 需要结束的请求列表 */
    struct work_struct dispatch_work;
    struct fuse_dev *fud;          /* 关联的 FUSE 设备实例 */
    bool connected;               /* 队列是否连接 */
    long in_flight;               /* 飞行中的请求计数 */
    struct completion in_flight_zero;
    struct kobject *kobj;
    char name[VQ_NAME_LEN];
} ____cacheline_aligned_in_smp;

VQ_NAME_LEN = 24(第 45 行),队列名形如 "hiprio""requests.0"

2.3 fuse_conn 结构

fs/fuse/fuse_i.h 第 644-998 行,FUSE 连接的核心控制结构:

struct fuse_conn {
    spinlock_t lock;
    refcount_t count;
    atomic_t dev_count;          /* fuse_dev 数量 */
    atomic_t epoch;

    kuid_t user_id;
    kgid_t group_id;

    unsigned max_read;
    unsigned max_write;
    unsigned int max_pages;
    unsigned int max_pages_limit;

    struct fuse_iqueue iq;       /* 输入队列(待处理请求) */

    /* 背景请求控制 */
    unsigned max_background;
    unsigned congestion_threshold;
    unsigned num_background;
    struct list_head bg_queue;
    spinlock_t bg_lock;

    unsigned connected;          /* 连接是否建立 */
    unsigned conn_init:1;        /* INIT 握手是否完成 */
    unsigned writeback_cache:1;  /* 是否启用回写缓存 */

#ifdef CONFIG_FUSE_DAX
    enum fuse_dax_mode dax_mode;
    struct fuse_conn_dax *dax;   /* DAX 连接数据 */
#endif
    struct list_head mounts;     /* 所有挂载点列表 */
    struct list_head devices;    /* fuse_dev 列表 */
};

fuse_mount 结构(fuse_i.h:1007)将 fuse_conn 与超级块关联:

struct fuse_mount {
    struct fuse_conn *fc;
    struct super_block *sb;
    struct list_head fc_entry;
    struct rcu_head rcu;
};

2.4 fuse_iqueue 与 fuse_iqueue_ops

fs/fuse/fuse_i.h 第 497-517 行:

struct fuse_iqueue_ops {
    void (*send_forget)(struct fuse_iqueue *fiq,
                        struct fuse_forget_link *link);
    void (*send_interrupt)(struct fuse_iqueue *fiq,
                           struct fuse_req *req);
    void (*send_req)(struct fuse_iqueue *fiq, struct fuse_req *req);
    void (*release)(struct fuse_iqueue *fiq);
};

virtiofs 实现(fs/fuse/virtio_fs.c 第 1521-1526 行):

static const struct fuse_iqueue_ops virtio_fs_fiq_ops = {
    .send_forget   = virtio_fs_send_forget,
    .send_interrupt = virtio_fs_send_interrupt,
    .send_req      = virtio_fs_send_req,
    .release       = virtio_fs_fiq_release,
};

2.5 fuse_req 结构

fs/fuse/fuse_i.h 第 441-485 行:

struct fuse_req {
    struct list_head list;
    struct list_head intr_entry;

    struct fuse_args *args;      /* 请求参数(输入/输出) */
    refcount_t count;
    unsigned long flags;          /* FR_ISREPLY / FR_SENT / FR_FINISHED... */

    struct {
        struct fuse_in_header h;  /* 请求头(opcode, nodeid, unique...) */
    } in;
    struct {
        struct fuse_out_header h; /* 响应头(error, len) */
    } out;

    wait_queue_head_t waitq;

#if IS_ENABLED(CONFIG_VIRTIO_FS)
    void *argbuf;                 /* virtiofs 用的物理连续 bounce buffer */
#endif

    struct fuse_mount *fm;
    unsigned long create_time;   /* 请求创建时刻(jiffies) */
};

argbuf 是 virtiofs 专用字段(第 471-474 行):由于 virtio ring 需要物理连续内存, stack 上的参数必须通过 copy_args_to_argbuf() 拷贝到 kmalloc 分配的缓冲区。

2.6 FUSE 协议消息头

include/uapi/linux/fuse.h 第 1034-1050 行:

struct fuse_in_header {
    uint32_t  len;           /* 整个请求消息总长度 */
    uint32_t  opcode;        /* 操作码(FUSE_READ/WRITE/OPEN...) */
    uint64_t  unique;        /* 唯一 ID,用于匹配响应 */
    uint64_t  nodeid;        /* 文件系统节点 ID */
    uint32_t  uid;           /* 调用者 UID */
    uint32_t  gid;           /* 调用者 GID */
    uint32_t  pid;           /* 调用者 PID */
    uint16_t  total_extlen;  /* 扩展区域长度(8字节单位) */
    uint16_t  padding;
};

struct fuse_out_header {
    uint32_t  len;    /* 响应总长度 */
    int32_t   error;  /* 错误码(0 表示成功) */
    uint64_t  unique; /* 对应请求的 unique ID */
};

2.7 virtio_fs_forget 结构

fs/fuse/virtio_fs.c 第 82-91 行,FORGET 请求使用独立缓冲区(不复用 fuse_req):

struct virtio_fs_forget_req {
    struct fuse_in_header ih;
    struct fuse_forget_in arg;
};

struct virtio_fs_forget {
    struct list_head list;      /* 临时排队用 */
    struct virtio_fs_forget_req req;
};

2.8 virtio_fs_req_work 结构

fs/fuse/virtio_fs.c 第 93-97 行,用于将阻塞型请求的完成处理推迟到工作队列:

struct virtio_fs_req_work {
    struct fuse_req *req;
    struct virtio_fs_vq *fsvq;
    struct work_struct done_work;
};

req->args->may_block 为真时(第 839 行),不能在软中断上下文直接调用 virtio_fs_request_complete(),因此封装为 virtio_fs_req_work 并通过 schedule_work() 推迟执行。


3. FUSE 协议在 virtio ring 上的传输

3.1 请求发送路径总览

VFS 系统调用
    |
    v
fuse_simple_request() / fuse_request_send()
    |
    v
fuse_iqueue.ops->send_req()
    |
    v
virtio_fs_send_req()          [fs/fuse/virtio_fs.c:1475]
    |
    +-- mq_map[raw_smp_processor_id()] 选队列
    |
    v
virtio_fs_enqueue_req()       [fs/fuse/virtio_fs.c:1370]
    |
    +-- copy_args_to_argbuf()   构建 bounce buffer
    +-- sg_init_fuse_args()     构建 scatter-gather 列表
    +-- virtqueue_add_sgs()     加入 virtio ring
    +-- virtqueue_kick()        通知 host
    |
    v
(等待响应)
    |
    v
virtio_fs_vq_done()           [中断回调, fs/fuse/virtio_fs.c:898]
    |
    v
schedule_work(&fsvq->done_work)
    |
    v
virtio_fs_requests_done_work() [fs/fuse/virtio_fs.c:810]
    |
    +-- virtqueue_get_buf()      从 virtqueue 取回完成的请求
    +-- virtio_fs_request_complete()
    +-- copy_args_from_argbuf()  将结果拷贝回 args
    +-- fuse_request_end()       唤醒等待进程

3.2 virtio_fs_enqueue_req 详解

fs/fuse/virtio_fs.c 第 1380-1473 行是请求入队的核心函数。其关键操作分为以下几步:

第一步:决定 SG 列表是否在栈上分配

/* Does the sglist fit on the stack? */
total_sgs = sg_count_fuse_req(req);   /* 计算所需 SG 数量 */
if (total_sgs > ARRAY_SIZE(stack_sgs)) {
    sgs = kmalloc_objs(sgs[0], total_sgs, gfp);
    sg = kmalloc_objs(sg[0], total_sgs, gfp);
}

第二步:将参数拷贝到 bounce buffer

ret = copy_args_to_argbuf(req, gfp);  /* 1401 行:stack 变量不可被 DMA */

第三步:构建 out(host 读取)方向和 in(host 写入)方向的 SG 列表

/* Request elements */
sg_init_one(&sg[out_sgs++], &req->in.h, sizeof(req->in.h));  /* 1407 行 */
out_sgs += sg_init_fuse_args(&sg[out_sgs], req,
                              (struct fuse_arg *)args->in_args,
                              args->in_numargs, args->in_pages,
                              req->argbuf, &argbuf_used);

/* Reply elements */
if (test_bit(FR_ISREPLY, &req->flags)) {
    sg_init_one(&sg[out_sgs + in_sgs++],
                &req->out.h, sizeof(req->out.h));
    in_sgs += sg_init_fuse_args(...);
}

第四步:提交到 virtio ring 并通知 host

ret = virtqueue_add_sgs(vq, sgs, out_sgs, in_sgs, req, GFP_ATOMIC);  /* 1437 行 */
/* ...将请求加入 fpq->processing 哈希表... */
set_bit(FR_SENT, &req->flags);
smp_mb__after_atomic();   /* 与 request_wait_answer() 中的 barrier 配对 */

notify = virtqueue_kick_prepare(vq);
spin_unlock(&fsvq->lock);
if (notify)
    virtqueue_notify(vq);  /* 1459 行 */

3.3 Scatter-Gather 内存布局

virtio ring 中每个描述符链表的结构:

out 方向(guest 写,host 读):
+------------------+
| fuse_in_header   |  40 bytes: len/opcode/unique/nodeid/uid/gid/pid
+------------------+
| argbuf (in_args) |  可变长,包含操作参数(如 fuse_read_in)
+------------------+
| page(s) [可选]   |  如果 args->in_pages=1(写请求数据页)
+------------------+

in 方向(host 写,guest 读):
+--------------------+
| fuse_out_header    |  16 bytes: len/error/unique
+--------------------+
| argbuf (out_args)  |  可变长,响应数据(如 fuse_attr)
+--------------------+
| page(s) [可选]     |  如果 args->out_pages=1(读请求数据页)
+--------------------+

FUSE_HEADER_OVERHEAD = 4(第 29 行):表示每个请求至少需要 4 个 SG 描述符( fuse_in_header + 1 个 in_args + fuse_out_header + 1 个 out_args)。

3.4 请求队列满时的处理

virtio_fs_send_req()(第 1497-1507 行):当 virtqueue_add_sgs() 返回 -ENOSPC 时, 请求被加入 fsvq->queued_reqs 链表,等待 dispatch_work 工作队列在队列有空间后重试:

if (ret == -ENOSPC) {
    spin_lock(&fsvq->lock);
    list_add_tail(&req->list, &fsvq->queued_reqs);
    inc_in_flight_req(fsvq);
    spin_unlock(&fsvq->lock);
    return;
}

virtio_fs_requests_done_work() 在处理完成请求后(第 852-856 行)会主动触发 dispatch_work:

/* Try to push previously queued requests, as the queue might no longer be full */
spin_lock(&fsvq->lock);
if (!list_empty(&fsvq->queued_reqs))
    schedule_work(&fsvq->dispatch_work);
spin_unlock(&fsvq->lock);

3.5 FORGET 请求的特殊处理

FUSE_FORGET 是单向请求(不需要响应),通过高优先级队列 VQ_HIPRIO 发送。

发送流程(fs/fuse/virtio_fs.c:1234):

static void virtio_fs_send_forget(struct fuse_iqueue *fiq,
                                   struct fuse_forget_link *link)
{
    struct virtio_fs_forget *forget;
    struct virtio_fs *fs = fiq->priv;
    struct virtio_fs_vq *fsvq = &fs->vqs[VQ_HIPRIO];
    u64 unique = fuse_get_unique(fiq);

    forget = kmalloc_obj(*forget, GFP_NOFS | __GFP_NOFAIL);
    req = &forget->req;

    req->ih.opcode  = FUSE_FORGET;
    req->ih.nodeid  = link->forget_one.nodeid;
    req->ih.unique  = unique;
    req->ih.len     = sizeof(*req);
    req->arg.nlookup = link->forget_one.nlookup;

    /* 只有 out 方向(不需要响应缓冲区) */
    virtqueue_add_outbuf(fsvq->vq, sg, 1, forget, GFP_ATOMIC);
    virtqueue_kick(fsvq->vq);
}

FORGET 请求完成后,host 直接 free 缓冲区,不回写任何数据。

3.6 中断处理与完成回调

virtqueue 中断触发后(fs/fuse/virtio_fs.c:898):

/* Virtqueue interrupt handler */
static void virtio_fs_vq_done(struct virtqueue *vq)
{
    struct virtio_fs_vq *fsvq = vq_to_fsvq(vq);

    dev_dbg(&vq->vdev->dev, "%s %s\n", __func__, fsvq->name);

    schedule_work(&fsvq->done_work);
}

实际的完成处理在 virtio_fs_requests_done_work() 中通过禁用/重启 callback 的方式 批量取出完成的请求(第 822-832 行),避免在高频 IO 场景中反复触发中断:

spin_lock(&fsvq->lock);
do {
    virtqueue_disable_cb(vq);    /* 禁止 virtqueue callback */
    while ((req = virtqueue_get_buf(vq, &len)) != NULL) {
        spin_lock(&fpq->lock);
        list_move_tail(&req->list, &reqs);
        spin_unlock(&fpq->lock);
    }
} while (!virtqueue_enable_cb(vq));  /* 重启 callback,若有新完成项则重试 */
spin_unlock(&fsvq->lock);

4. DAX 模式与零拷贝映射

4.1 DAX 架构概述

DAX(Direct Access)模式允许 guest 直接通过内存映射访问 host 文件内容, 完全绕过 guest 页缓存,实现零拷贝读写。

+---------------------------+        +---------------------------+
|       Guest Kernel        |        |       Host                |
|                           |        |                           |
|  应用程序 mmap(file)      |        |  virtiofsd                |
|       |                   |        |       |                   |
|  page fault               |        |  处理 FUSE_SETUPMAPPING   |
|       |                   |        |       |                   |
|  DAX iomap_begin()        |        |  将文件页映射到            |
|  --> FUSE_SETUPMAPPING ----|------->|  共享内存窗口             |
|       |                   |        |       |                   |
|  pgoff -> window_kaddr    |        |  VIRTIO_FS_SHMCAP_ID_CACHE|
|  直接访问 PCI BAR 内存     |<-------|  (PCI 共享内存区域)       |
|                           |        |                           |
+---------------------------+        +---------------------------+
         ^
         | 物理地址映射(无数据拷贝)
         |
   [PCI BAR: DAX Window]
   window_phys_addr ... window_phys_addr + window_len

4.2 DAX 初始化

fs/fuse/virtio_fs.c 第 1052-1116 行 virtio_fs_setup_dax()

static int virtio_fs_setup_dax(struct virtio_device *vdev,
                                struct virtio_fs *fs)
{
    struct virtio_shm_region cache_reg;

    /* 分配 DAX 设备 */
    dax_dev = alloc_dax(fs, &virtio_fs_dax_ops);

    /* 从 virtio 设备获取共享内存区域信息 */
    have_cache = virtio_get_shm_region(vdev, &cache_reg,
                                        (u8)VIRTIO_FS_SHMCAP_ID_CACHE);
    if (!have_cache) {
        dev_notice(&vdev->dev, "%s: No cache capability\n", __func__);
        return 0;
    }

    /* 设置 pgmap(用于 devm_memremap_pages 注册物理页) */
    pgmap->type = MEMORY_DEVICE_FS_DAX;
    pgmap->range = (struct range) {
        .start = (phys_addr_t) cache_reg.addr,
        .end   = (phys_addr_t) cache_reg.addr + cache_reg.len - 1,
    };
    pgmap->nr_range = 1;

    /* 将 PCI BAR 内存映射到内核虚拟地址空间 */
    fs->window_kaddr = devm_memremap_pages(&vdev->dev, pgmap);
    fs->window_phys_addr = (phys_addr_t) cache_reg.addr;
    fs->window_len       = (phys_addr_t) cache_reg.len;

    fs->dax_dev = no_free_ptr(dax_dev);
}

4.3 DAX 直接访问操作

fs/fuse/virtio_fs.c 第 1006-1019 行 virtio_fs_direct_access()

static long virtio_fs_direct_access(struct dax_device *dax_dev,
                                     pgoff_t pgoff,
                                     long nr_pages,
                                     enum dax_access_mode mode,
                                     void **kaddr,
                                     unsigned long *pfn)
{
    struct virtio_fs *fs = dax_get_private(dax_dev);
    phys_addr_t offset = PFN_PHYS(pgoff);
    size_t max_nr_pages = fs->window_len / PAGE_SIZE - pgoff;

    if (kaddr)
        *kaddr = fs->window_kaddr + offset;   /* 内核虚拟地址 */
    if (pfn)
        *pfn = PHYS_PFN(fs->window_phys_addr + offset); /* 物理帧号 */
    return nr_pages > max_nr_pages ? max_nr_pages : nr_pages;
}

4.4 DAX 模式配置

fuse_i.h 第 589-594 行定义了 DAX 模式枚举:

enum fuse_dax_mode {
    FUSE_DAX_INODE_DEFAULT,  /* 默认:由 inode 决定 */
    FUSE_DAX_ALWAYS,         /* mount -o dax=always */
    FUSE_DAX_NEVER,          /* mount -o dax=never  */
    FUSE_DAX_INODE_USER,     /* mount -o dax=inode  */
};

挂载参数解析(virtio_fs.c 第 103-143 行):

static const struct constant_table dax_param_enums[] = {
    {"always",  FUSE_DAX_ALWAYS },
    {"never",   FUSE_DAX_NEVER },
    {"inode",   FUSE_DAX_INODE_USER },
    {}
};

fuse_conn 中 DAX 相关字段(fuse_i.h 第 966-972 行):

#ifdef CONFIG_FUSE_DAX
    enum fuse_dax_mode dax_mode;
    struct fuse_conn_dax *dax;   /* 非 NULL 表示 DAX 已启用 */
#endif

fuse_inode 的 DAX 字段(fuse_i.h 第 208-213 行):

#ifdef CONFIG_FUSE_DAX
    struct fuse_inode_dax *dax;  /* per-inode DAX 状态 */
#endif

5. FUSE_SETUPMAPPING 与 FUSE_REMOVEMAPPING

5.1 SETUPMAPPING 消息格式

include/uapi/linux/fuse.h 第 1169-1182 行:

#define FUSE_SETUPMAPPING_FLAG_WRITE (1ull << 0)  /* 允许写操作 */
#define FUSE_SETUPMAPPING_FLAG_READ  (1ull << 1)  /* 允许读操作 */

struct fuse_setupmapping_in {
    uint64_t  fh;       /* 已打开的文件句柄 */
    uint64_t  foffset;  /* 文件内偏移 */
    uint64_t  len;      /* 映射长度 */
    uint64_t  flags;    /* FUSE_SETUPMAPPING_FLAG_* */
    uint64_t  moffset;  /* 在 DAX 内存窗口中的偏移 */
};

5.2 REMOVEMAPPING 消息格式

include/uapi/linux/fuse.h 第 1184-1197 行:

struct fuse_removemapping_in {
    uint32_t  count;  /* 后跟 count 个 fuse_removemapping_one */
};

struct fuse_removemapping_one {
    uint64_t  moffset;  /* DAX 窗口中的起始偏移 */
    uint64_t  len;      /* 取消映射的长度 */
};

#define FUSE_REMOVEMAPPING_MAX_ENTRY \
    (PAGE_SIZE / sizeof(struct fuse_removemapping_one))

5.3 FUSE 7.31 协议变更

include/uapi/linux/fuse.h 第 173-174 行注释:

*  7.31
*  - add FUSE_SETUPMAPPING and FUSE_REMOVEMAPPING
*  - add map_alignment to fuse_init_out, add FUSE_MAP_ALIGNMENT flag

map_alignment 字段在 fuse_init_out(第 924 行)中指定 DAX 窗口偏移对齐要求(log2 字节数)。 FUSE_MAP_ALIGNMENT(第 478 行)标志在 INIT 握手时协商此能力:

#define FUSE_MAP_ALIGNMENT  (1 << 26)

5.4 DAX 映射操作流程

应用程序 mmap(fd, ...)
         |
         v
fuse_file_mmap()  -->  iomap_ops
         |
         v
fuse_iomap_begin()
         |
         +-- 查找已存在的映射(fuse_inode_dax->extents)
         |
         +-- 若无映射,发送 FUSE_SETUPMAPPING 请求
         |         |
         |         v
         |   virtiofsd 将文件页映射到 DAX window[moffset]
         |         |
         |         v
         +-- 返回 iomap(addr = window_phys_addr + moffset)
         |
         v
page fault 处理
         |
         v
dax_iomap_fault() --> vmf_insert_mixed()
         |
         v
pfn = PHYS_PFN(window_phys_addr + moffset + page_offset)
直接建立 PTE(无 page 结构体,无拷贝)

5.5 inode_dax 缓存

每个文件的 DAX 映射信息缓存在 fuse_inode.daxfuse_inode_dax 结构)中, 维护一个区间树(extent tree)记录 foffset -> moffset 的映射关系, 避免对同一文件区域重复发送 FUSE_SETUPMAPPING 请求。

当文件大小变更或文件被删除时,内核通过 FUSE_REMOVEMAPPING 告知 virtiofsd 释放对应的 DAX 窗口区域。


6. 9P 协议基础

6.1 历史背景

9P 协议来自 Bell Labs 的 Plan 9 操作系统,是其跨进程、跨机器的统一文件系统协议。 Linux 内核实现了三个版本:

版本 常量 特点
9P2000 p9_proto_legacy 原始版本,字符串 UID/GID
9P2000.u p9_proto_2000u Unix 扩展:数字 UID/GID、设备文件
9P2000.L p9_proto_2000L Linux 扩展:stat/create 与 POSIX 对齐

net/9p/client.c 第 36-46 行:

inline int p9_is_proto_dotl(struct p9_client *clnt)
{
    return clnt->proto_version == p9_proto_2000L;
}

inline int p9_is_proto_dotu(struct p9_client *clnt)
{
    return clnt->proto_version == p9_proto_2000u;
}

6.2 消息格式

所有 9P 消息的最小头部(include/net/9p/9p.h 第 328-329 行):

size[4]  type[1]  tag[2]
  • size:消息总长度(含 size 字段本身),4 字节小端序
  • type:消息类型(见 p9_msg_t 枚举)
  • tag:事务标签,用于多路复用,P9_NOTAG(0xFFFF)用于 VERSION

消息头大小常量(第 329 行):#define P9_HDRSZ 7

6.3 消息类型枚举

include/net/9p/9p.h 第 115-184 行定义了完整的消息类型。 9P2000.L 新增的关键操作(T=请求,R=响应):

enum p9_msg_t {
    P9_TLERROR  = 6,   P9_RLERROR,   /* 错误响应(2000.L) */
    P9_TSTATFS  = 8,   P9_RSTATFS,   /* 文件系统统计 */
    P9_TLOPEN   = 12,  P9_RLOPEN,    /* 打开文件(2000.L) */
    P9_TLCREATE = 14,  P9_RLCREATE,  /* 创建文件(2000.L) */
    P9_TSYMLINK = 16,  P9_RSYMLINK,  /* 创建符号链接 */
    P9_TMKNOD   = 18,  P9_RMKNOD,    /* 创建设备文件 */
    P9_TGETATTR = 24,  P9_RGETATTR,  /* 获取属性(2000.L stat) */
    P9_TSETATTR = 26,  P9_RSETATTR,  /* 设置属性(2000.L setattr) */
    P9_TREADDIR = 40,  P9_RREADDIR,  /* 读取目录(2000.L) */
    P9_TVERSION = 100, P9_RVERSION,  /* 版本协商 */
    P9_TATTACH  = 104, P9_RATTACH,   /* 建立会话根 fid */
    P9_TWALK    = 110, P9_RWALK,     /* 路径遍历,获取新 fid */
    P9_TREAD    = 116, P9_RREAD,     /* 读文件 */
    P9_TWRITE   = 118, P9_RWRITE,    /* 写文件 */
    P9_TCLUNK   = 120, P9_RCLUNK,    /* 释放 fid */
    P9_TREMOVE  = 122, P9_RREMOVE,   /* 删除文件 */
};

6.4 Fid 概念

Fid(File Identifier)是 9P 协议的核心抽象,类似 Unix 的文件描述符, 但作用域是整个会话而非单个进程:

  • 每个 fid 是一个 32 位整数,在同一 9P 会话内唯一
  • TATTACH 获取根目录 fid
  • TWALK 在目录层次中遍历,可以克隆或移动 fid
  • TOPEN/TLOPEN 打开 fid 对应的文件
  • TCLUNK 释放 fid(类似 close)
  • TREMOVE 删除文件并同时释放 fid

Fid 状态机:

TATTACH
    |
    v
[已绑定到路径,未打开]
    |
    +-- TWALK (克隆并遍历)--> [新 fid,绑定到子路径]
    |
    +-- TOPEN/TLOPEN ---------> [已打开,可 READ/WRITE]
    |                               |
    |                               +-- TCLUNK --> [释放]
    |
    +-- TCLUNK -------------------> [释放]

7. 9P 核心数据结构

7.1 p9_client 结构

include/net/9p/client.h 第 108-133 行:

struct p9_client {
    spinlock_t lock;
    unsigned int msize;                /* 最大消息大小(默认 128K+)*/
    unsigned char proto_version;       /* p9_proto_legacy/2000u/2000L */
    struct p9_trans_module *trans_mod; /* 传输层模块 */
    enum p9_trans_status status;       /* Connected/Disconnected/Hung */
    void *trans;                       /* 传输层私有状态 */
    struct kmem_cache *fcall_cache;    /* fcall 对象缓存 */

    union {
        struct { int rfd; int wfd; } fd;
        struct { u16 port; bool privport; } tcp;
    } trans_opts;

    struct idr fids;   /* fid 编号 -> p9_fid 的 IDR */
    struct idr reqs;   /* tag -> p9_req_t 的 IDR   */

    char name[__NEW_UTS_LEN + 1]; /* 客户端标识(主机名) */
};

默认 msize 常量(第 23 行):

#define DEFAULT_MSIZE ((128 * 1024) + P9_IOHDRSZ)

即 128KB payload + 24 字节 IO 头 = ~131KB。

7.2 p9_fid 结构

include/net/9p/client.h 第 244-257 行:

struct p9_fid {
    struct p9_client *clnt;  /* 所属客户端 */
    u32 fid;                  /* fid 数值(协议层标识) */
    refcount_t count;         /* 引用计数 */
    int mode;                 /* 打开模式(-1 表示未打开) */
    struct p9_qid qid;        /* 服务端唯一标识符 */
    u32 iounit;               /* 服务端报告的最大 IO 单元 */
    kuid_t uid;               /* 持有此 fid 的用户 */

    void *rdir;               /* readdir 状态(按需分配) */

    struct hlist_node dlist;  /* dentry 上挂载的 fid 链表 */
    struct hlist_node ilist;  /* inode 上挂载的 fid 链表 */
};

7.3 p9_qid 结构(服务端唯一标识)

include/net/9p/9p.h 第 363-367 行:

struct p9_qid {
    u8  type;     /* 文件类型(P9_QTDIR/P9_QTFILE 等) */
    u32 version;  /* 单调递增的版本号(修改时递增) */
    u64 path;     /* 服务端唯一文件 ID(类似 inode 号) */
};

QID 类型标志(第 311-321 行):

enum p9_qid_t {
    P9_QTDIR     = 0x80,  /* 目录 */
    P9_QTAPPEND  = 0x40,  /* 追加文件 */
    P9_QTEXCL    = 0x20,  /* 独占使用 */
    P9_QTMOUNT   = 0x10,  /* 挂载点 */
    P9_QTAUTH    = 0x08,  /* 认证文件 */
    P9_QTTMP     = 0x04,  /* 非持久化文件 */
    P9_QTSYMLINK = 0x02,  /* 符号链接 */
    P9_QTLINK    = 0x01,  /* 硬链接 */
    P9_QTFILE    = 0x00,  /* 普通文件 */
};

version=0 表示 synthetic 文件,客户端不应缓存。

7.4 p9_req_t 结构

include/net/9p/client.h 第 83-91 行:

struct p9_req_t {
    int status;           /* REQ_STATUS_ALLOC/SENT/RCVD/FLSHD/ERROR */
    int t_err;            /* 传输层错误码 */
    refcount_t refcount;
    wait_queue_head_t wq; /* 等待响应的等待队列 */
    struct p9_fcall tc;   /* 请求缓冲区(T 消息) */
    struct p9_fcall rc;   /* 响应缓冲区(R 消息) */
    struct list_head req_list;
};

请求状态枚举(第 65-72 行):

enum p9_req_status_t {
    REQ_STATUS_ALLOC,    /* 已分配但未发送 */
    REQ_STATUS_UNSENT,   /* 等待发送 */
    REQ_STATUS_SENT,     /* 已发送,等待响应 */
    REQ_STATUS_RCVD,     /* 收到响应 */
    REQ_STATUS_FLSHD,    /* 请求已被 flush(取消) */
    REQ_STATUS_ERROR,    /* 客户端错误 */
};

7.5 v9fs_session_info 结构

fs/9p/v9fs.h 第 112-134 行,VFS 层的会话状态:

struct v9fs_session_info {
    unsigned int flags;         /* V9FS_PROTO_2000L / V9FS_ACCESS_USER... */
    unsigned char nodev;        /* 是否禁用设备文件映射 */
    unsigned short debug;       /* 调试标志 */
    unsigned int afid;          /* 认证 fid */
    unsigned int cache;         /* 缓存模式(p9_cache_bits) */
    char *uname;                /* 挂载用户名 */
    char *aname;                /* 远端文件系统路径 */
    unsigned int maxdata;       /* 客户端最大数据大小 */
    kuid_t dfltuid;
    kgid_t dfltgid;
    kuid_t uid;
    struct p9_client *clnt;     /* 9P 客户端实例 */
    struct list_head slist;
    struct rw_semaphore rename_sem;
    long session_lock_timeout;
};

7.6 v9fs_inode 结构

fs/9p/v9fs.h 第 139-144 行:

struct v9fs_inode {
    struct netfs_inode netfs;       /* netfslib 上下文 + VFS inode */
    struct p9_qid qid;              /* 服务端 QID */
    unsigned int cache_validity;    /* V9FS_INO_INVALID_ATTR */
    struct mutex v_mutex;
};

netfs_inode 嵌套 VFS inode,通过 V9FS_I(inode)container_of 获取:

static inline struct v9fs_inode *V9FS_I(const struct inode *inode)
{
    return container_of(inode, struct v9fs_inode, netfs.inode);
}

7.7 p9_wstat 结构(协议层 stat)

include/net/9p/9p.h 第 391-408 行,对应 9P2000 的 stat 消息:

struct p9_wstat {
    u16 size;           /* stat 结构总大小 */
    u16 type;           /* 服务端类型(主设备号语义) */
    u32 dev;            /* 服务端子类型 */
    struct p9_qid qid;  /* 文件唯一标识符 */
    u32 mode;           /* 权限位(p9_perm_t) */
    u32 atime;          /* 最后访问时间 */
    u32 mtime;          /* 最后修改时间 */
    u64 length;         /* 文件大小 */
    const char *name;   /* 文件名(最后路径分量) */
    const char *uid;    /* 所有者名称 */
    const char *gid;    /* 组名 */
    const char *muid;   /* 最后修改者 */
    char *extension;    /* 9P2000.u 扩展字段 */
    kuid_t n_uid;       /* 数字 UID(9P2000.u) */
    kgid_t n_gid;       /* 数字 GID(9P2000.u) */
    kuid_t n_muid;      /* 数字 muid(9P2000.u) */
};

9P2000.L 使用 p9_stat_dotl(第 410-431 行),字段与 Linux statx 对齐, 包含 st_result_mask、纳秒精度时间戳等。


8. 9P virtio 传输层

8.1 virtio_chan 结构

net/9p/trans_virtio.c 第 64-86 行:

struct virtio_chan {
    bool inuse;                    /* 是否被占用 */
    spinlock_t lock;
    struct p9_client *client;      /* 关联的 9P 客户端 */
    struct virtio_device *vdev;    /* virtio 设备 */
    struct virtqueue *vq;          /* 单个 virtqueue(9P 只用一个) */
    int ring_bufs_avail;           /* ring 是否有空间 */
    wait_queue_head_t *vc_wq;      /* 等待 ring 有空间的等待队列 */
    unsigned long p9_max_pages;    /* 最大 pin 页数限制 */
    struct scatterlist sg[VIRTQUEUE_NUM]; /* 全局 SG 列表(128 项) */
    char *tag;                     /* 设备标签 */
    struct list_head chan_list;
};

VIRTQUEUE_NUM = 128(第 38 行),9P 传输层共用一个 virtqueue(不同于 virtiofs 的多队列设计)。

8.2 普通请求发送(p9_virtio_request)

net/9p/trans_virtio.c 第 255-306 行:

static int p9_virtio_request(struct p9_client *client, struct p9_req_t *req)
{
    WRITE_ONCE(req->status, REQ_STATUS_SENT);
req_retry:
    spin_lock_irqsave(&chan->lock, flags);

    /* out 方向:发送请求消息 (req->tc.sdata) */
    out = pack_sg_list(chan->sg, 0, VIRTQUEUE_NUM,
                       req->tc.sdata, req->tc.size);

    /* in 方向:接收响应消息 (req->rc.sdata) */
    in = pack_sg_list(chan->sg, out, VIRTQUEUE_NUM,
                      req->rc.sdata, req->rc.capacity);

    err = virtqueue_add_sgs(chan->vq, sgs, out_sgs, in_sgs,
                             req, GFP_ATOMIC);
    if (err == -ENOSPC) {
        chan->ring_bufs_avail = 0;
        spin_unlock_irqrestore(&chan->lock, flags);
        /* 等待 ring 有空间(可被 killable 中断) */
        err = io_wait_event_killable(*chan->vc_wq,
                                     chan->ring_bufs_avail);
        goto req_retry;
    }

    virtqueue_kick(chan->vq);
}

8.3 零拷贝请求(p9_virtio_zc_request)

net/9p/trans_virtio.c 第 419-530 行,零拷贝版本将用户空间页直接加入 SG 列表:

static int p9_virtio_zc_request(struct p9_client *client,
                                 struct p9_req_t *req,
                                 struct iov_iter *uidata,  /* 读目标 */
                                 struct iov_iter *uodata,  /* 写来源 */
                                 int inlen, int outlen,
                                 int in_hdr_len)
{
    /* 对写操作:pin 用户页,放入 out SG */
    if (uodata) {
        n = p9_get_mapped_pages(chan, &out_pages, uodata,
                                 outlen, &offs, &need_drop);
        sgs[out_sgs++] = chan->sg + out;
        out += pack_sg_list_p(chan->sg, out, VIRTQUEUE_NUM,
                               out_pages, out_nr_pages, offs, outlen);
    }

    /* 对读操作:pin 用户页,放入 in SG */
    if (uidata) {
        in += pack_sg_list_p(chan->sg, out + in, VIRTQUEUE_NUM,
                              in_pages, in_nr_pages, offs, inlen);
    }
}

全局页 pin 限制(第 43 行):

static atomic_t vp_pinned = ATOMIC_INIT(0);

当 pin 的页数达到 chan->p9_max_pages 时,请求需等待其他零拷贝 IO 完成。

8.4 响应回调(req_done)

net/9p/trans_virtio.c 第 128-154 行,virtqueue 中断触发后:

static void req_done(struct virtqueue *vq)
{
    spin_lock_irqsave(&chan->lock, flags);
    while ((req = virtqueue_get_buf(chan->vq, &len)) != NULL) {
        if (!chan->ring_bufs_avail) {
            chan->ring_bufs_avail = 1;
            need_wakeup = true;
        }
        if (len) {
            req->rc.size = len;
            p9_client_cb(chan->client, req, REQ_STATUS_RCVD);
        }
    }
    spin_unlock_irqrestore(&chan->lock, flags);
    if (need_wakeup)
        wake_up(chan->vc_wq);  /* 唤醒等待 ring 空间的线程 */
}

p9_client_cb()req->status 设为 REQ_STATUS_RCVD 并唤醒等待该请求的任务。

8.5 pack_sg_list 辅助函数

net/9p/trans_virtio.c 第 170-189 行,将线性缓冲区打包进 SG 列表, 自动处理跨页边界(每个 SG 元素不超过剩余页长度):

static int pack_sg_list(struct scatterlist *sg, int start,
                         int limit, char *data, int count)
{
    while (count) {
        s = rest_of_page(data);   /* 当前页剩余字节数 */
        if (s > count) s = count;
        sg_unmark_end(&sg[index]);        /* 保证不提前结束 */
        sg_set_buf(&sg[index++], data, s);
        count -= s;
        data  += s;
    }
    if (index - start)
        sg_mark_end(&sg[index - 1]);   /* 标记链表末尾 */
    return index - start;
}

8.6 传输层注册

9P 传输层通过 p9_trans_module 结构注册(include/net/9p/transport.h), virtio 传输在模块 init 时调用 v9fs_register_trans() 注册, TCP/RDMA 传输同理。客户端通过 mount 选项 trans=virtio 选择传输层。


9. 9P VFS 集成

9.1 VFS inode 操作表

fs/9p/vfs_inode.c 第 33-36 行声明了多套 inode_operations:

static const struct inode_operations v9fs_dir_inode_operations;
static const struct inode_operations v9fs_dir_inode_operations_dotu;
static const struct inode_operations v9fs_file_inode_operations;
static const struct inode_operations v9fs_symlink_inode_operations;

以及 9P2000.L 专用版本(v9fs.h 第 190-192 行):

extern const struct inode_operations v9fs_dir_inode_operations_dotl;
extern const struct inode_operations v9fs_file_inode_operations_dotl;
extern const struct inode_operations v9fs_symlink_inode_operations_dotl;

9.2 Unix 权限位转换

fs/9p/vfs_inode.c 第 45-72 行 unixmode2p9mode(),将 Linux 权限位转换为 9P 格式:

static u32 unixmode2p9mode(struct v9fs_session_info *v9ses, umode_t mode)
{
    res = mode & 0777;
    if (S_ISDIR(mode))
        res |= P9_DMDIR;             /* 0x80000000 */
    if (v9fs_proto_dotu(v9ses)) {
        if (S_ISSOCK(mode))   res |= P9_DMSOCKET;    /* 0x00100000 */
        if (S_ISFIFO(mode))   res |= P9_DMNAMEDPIPE; /* 0x00200000 */
        if (S_ISBLK(mode) ||
            S_ISCHR(mode))    res |= P9_DMDEVICE;    /* 0x00800000 */
        if (mode & S_ISUID)   res |= P9_DMSETUID;    /* 0x00080000 */
        if (mode & S_ISGID)   res |= P9_DMSETGID;    /* 0x00040000 */
        if (mode & S_ISVTX)   res |= P9_DMSETVTX;    /* 0x00010000 */
    }
}

9.3 文件 lookup 路径

9P 的文件查找通过 v9fs_vfs_lookup() 进行,核心是将 VFS dentry 查找 转换为 p9_client_walk() 调用(发送 TWALK 消息):

vfs_lookup(dir_inode, dentry)
    |
    v
v9fs_vfs_lookup()
    |
    +-- v9fs_fid_lookup(dentry->d_parent) -- 获取父目录 fid
    |
    +-- p9_client_walk(dfid, 1, &name, 1)
    |   |
    |   +-- 发送 TWALK(dfid, newfid, [name]) 到服务端
    |   +-- 服务端返回新 fid 及其 QID
    |
    +-- v9fs_inode_from_fid() / v9fs_inode_from_fid_dotl()
    |   |
    |   +-- p9_client_stat() / p9_client_getattr_dotl()
    |   +-- 创建或更新 VFS inode
    |
    +-- d_splice_alias(inode, dentry)

9.4 缓存策略

fs/9p/v9fs.h 第 78-85 行的缓存位标志:

enum p9_cache_bits {
    CACHE_NONE      = 0b00000000,  /* 无缓存 */
    CACHE_FILE      = 0b00000001,  /* 文件缓存(open-to-close) */
    CACHE_META      = 0b00000010,  /* 元数据/目录缓存 */
    CACHE_WRITEBACK = 0b00000100,  /* 文件回写缓存 */
    CACHE_LOOSE     = 0b00001000,  /* 松散一致性(不检查缓存) */
    CACHE_FSCACHE   = 0b10000000,  /* 持久化本地缓存(fscache) */
};

CACHE_SC_MMAP(第 62 行)= CACHE_FILE | CACHE_META,这是支持 mmap 的最小缓存配置。

9.5 QID 版本与缓存一致性

9P 缓存一致性依赖 QID 的 version 字段:

  • 客户端每次访问 inode 前对比缓存的 qid.version 与服务端返回的值
  • 若版本不同(V9FS_INO_INVALID_ATTR 标志),使缓存失效并重新获取
  • version=0 的文件(synthetic)永远不缓存

mount 选项 V9FS_IGNORE_QVv9fs.h 第 44 行)可以忽略版本检查, 适用于已知一致性由上层保证的场景。


10. virtiofs 与 9P 对比

10.1 协议对比

特性 virtiofs 9P (virtio)
基础协议 FUSE(Linux 专有) 9P2000.L(跨平台)
传输方式 virtio ring(多队列) virtio ring(单队列)
消息格式 fuse_in_header + 操作参数 size+type+tag + 操作参数
文件标识 nodeid(64位 inode ID) fid(32位句柄)+ QID
DAX 支持 有(FUSE_SETUPMAPPING)
多队列 是(per-CPU 队列) 否(单队列)
协议版本协商 FUSE_INIT(major.minor) TVERSION(版本字符串)
缓存一致性 服务端主动失效通知 QID version 轮询
host 实现 virtiofsd(FUSE 服务端) QEMU VirtFS / diod / 其他

10.2 性能特性对比

性能特性 virtiofs 9P (virtio)
零拷贝读写 DAX 模式(moffset 映射) ZC 模式(pin 用户页)
最大 IO 大小 由 max_pages 协商 msize - header overhead
CPU 亲和性 mq_map 绑定到 CPU 无(所有 CPU 共用一队列)
中断处理 多队列各自 IRQ 单 IRQ
写回缓存 支持(FUSE_WRITEBACK_CACHE) 支持(CACHE_WRITEBACK)

10.3 架构差异

virtiofs 架构:
  guest-kernel(FUSE) --> virtio-fs-driver --> vhost/vhost-user --> virtiofsd
  重用了成熟的 FUSE 基础设施,virtiofsd 可直接使用 libfuse

9P 架构:
  guest-kernel(9P) --> trans_virtio --> virtio-9p --> QEMU VirtFS
  协议自包含,无需额外守护进程,QEMU 直接实现服务端

11. 挂载流程分析

11.1 virtiofs 挂载流程

mount -t virtiofs myfs /mnt/virtiofs
         |
         v
virtio_fs_get_tree()           [virtio_fs.c:1660]
         |
         +-- virtio_fs_find_instance("myfs")   查找已注册的 virtio_fs 实例
         |
         +-- kzalloc(fuse_conn)                分配连接对象
         |
         +-- kzalloc(fuse_mount)               分配挂载点
         |
         +-- fuse_conn_init(fc, fm,
         |       &virtio_fs_fiq_ops, fs)       初始化连接,绑定操作集
         |
         +-- sget_fc() --> virtio_fs_fill_super()  [virtio_fs.c:1540]
                 |
                 +-- 为每个 vq 分配 fuse_dev
                 |
                 +-- fuse_fill_super_common()   建立超级块、根 inode
                 |
                 +-- fuse_dev_install(fud, fc)  安装 fuse_dev 到连接
                 |
                 +-- fuse_send_init(fm)         发送 FUSE_INIT 握手

virtio_fs_probe()(第 1118 行)在设备枚举时调用,完成:

  1. virtio_fs_read_tag():读取设备 tag
  2. virtio_fs_setup_vqs():初始化 virtqueue
  3. virtio_fs_map_queues():建立 CPU-队列映射
  4. virtio_fs_setup_dax():初始化 DAX 内存窗口
  5. virtio_fs_add_instance():注册到全局实例链表

virtio_fs_get_tree() 在挂载时还设置了多个重要的 fc 标志(第 1697-1700 行):

fc->delete_stale = true;      /* 删除过时的 dentry */
fc->auto_submounts = true;    /* 自动挂载 submounts */
fc->sync_fs = true;           /* 支持 syncfs() 传播到服务端 */
fc->use_pages_for_kvec_io = true; /* 使用页替代指针做 kvec IO */

11.2 9P 挂载流程

mount -t 9p -o trans=virtio myshare /mnt/9p
         |
         v
v9fs_mount() / v9fs_get_tree()
         |
         +-- v9fs_session_init()              初始化会话
         |       |
         |       +-- p9_client_create(fc)     创建 p9_client
         |       |       |
         |       |       +-- trans_mod->create() (p9_virtio_create)
         |       |           找到匹配 tag 的 virtio_chan
         |       |
         |       +-- p9_client_version()      发送 TVERSION
         |       |   ("9P2000.L", msize=131KB)
         |       |
         |       +-- p9_client_attach()       发送 TATTACH
         |           获取根目录 fid (root_fid)
         |
         +-- v9fs_get_inode(sb, S_IFDIR)
         |
         +-- v9fs_inode_from_fid(v9ses, root_fid, sb)
                 |
                 +-- p9_client_stat() / p9_client_getattr_dotl()
                 +-- 创建根 inode

12. 性能特性与调优

12.1 virtiofs 多队列

virtio_fs_map_queues()(第 859-895 行)的两阶段映射策略:

  1. 优先使用传输层 affinity(如 PCIe MSI-X):

    mask = vdev->config->get_vq_affinity(vdev, VQ_REQUEST + q);
    for_each_cpu(cpu, mask)
        fs->mq_map[cpu] = q + VQ_REQUEST;
  2. 回退到均匀分配

    masks = group_cpus_evenly(fs->num_request_queues, &nr_masks);
    for (q = 0; q < fs->num_request_queues; q++)
        for_each_cpu(cpu, &masks[q % nr_masks])
            fs->mq_map[cpu] = q + VQ_REQUEST;
  3. 最终回退:所有 CPU 使用第一个请求队列。

12.2 FUSE 请求协商参数

fuse_init_outinclude/uapi/linux/fuse.h 第 914-929 行)中的关键字段:

struct fuse_init_out {
    uint32_t  major;                /* FUSE 主版本(固定为 7) */
    uint32_t  minor;                /* FUSE 次版本(当前 42) */
    uint32_t  max_readahead;        /* 最大预读字节数 */
    uint32_t  flags;                /* FUSE_WRITEBACK_CACHE 等 */
    uint16_t  max_background;       /* 最大背景请求数 */
    uint16_t  congestion_threshold;
    uint32_t  max_write;            /* 最大单次写入字节数 */
    uint32_t  time_gran;            /* 时间戳精度(纳秒) */
    uint16_t  max_pages;            /* 单个请求最大页数 */
    uint16_t  map_alignment;        /* DAX 映射对齐(log2) */
    uint32_t  flags2;               /* 高位 flags(FUSE_HAS_INODE_DAX 等)*/
    uint32_t  max_stack_depth;
    uint16_t  request_timeout;      /* 请求超时(秒) */
    uint16_t  unused[11];
};

12.3 virtio ring 队列深度

fs/fuse/virtio_fs.c 第 1682 行获取实际 ring 大小:

virtqueue_size = virtqueue_get_vring_size(fs->vqs[VQ_REQUEST].vq);
if (WARN_ON(virtqueue_size <= FUSE_HEADER_OVERHEAD))
    goto out_err;

FUSE_HEADER_OVERHEAD = 4(第 29 行),表示每个请求至少占用 4 个 SG 描述符。

12.4 9P 的 msize 调优

net/9p/client.c 第 88 行读取 mount 选项中的 msize:

clnt->msize = ctx->client_opts.msize;

msize 决定单次 READ/WRITE 的最大 payload:

实际 IO 大小 = msize - P9_IOHDRSZ(24字节)

对于 virtio 传输,msize 还受 VIRTQUEUE_NUM * PAGE_SIZE 限制(128 * 4KB = 512KB)。

12.5 in_flight 计数与背压机制

virtio_fs_vq.in_flight 跟踪飞行中的请求数:

static inline void inc_in_flight_req(struct virtio_fs_vq *fsvq)
{
    fsvq->in_flight++;
}

static inline void dec_in_flight_req(struct virtio_fs_vq *fsvq)
{
    WARN_ON(fsvq->in_flight <= 0);
    fsvq->in_flight--;
    if (!fsvq->in_flight)
        complete(&fsvq->in_flight_zero); /* 通知 drain 等待者 */
}

virtio_fs_drain_queue()(第 296-315 行)在设备热拔插或卸载时等待 所有飞行请求完成,通过 wait_for_completion(&fsvq->in_flight_zero) 实现。

12.6 sysfs 暴露的队列信息

每个 virtio_fs 实例在 /sys/fs/virtio_fs/<index>/ 下暴露:

  • tag:文件系统标签
  • mqs/<vq_index>/name:队列名称
  • mqs/<vq_index>/cpu_list:映射到该队列的 CPU 列表

13. virtiofs 队列深度设计

13.1 hiprio 与 request 队列的分工

+-------------------+
|  VQ_HIPRIO (索引0) |  <-- FORGET / INTERRUPT(高优先级,无等待响应)
+-------------------+
|  VQ_REQUEST (索引1)|  <-- 普通读写 open/create 等请求(CPU 0 绑定)
+-------------------+
|  VQ_REQUEST (索引2)|  <-- CPU 1 绑定
+-------------------+
|        ...        |
+-------------------+
|  VQ_REQUEST (索引N)|  <-- CPU N-1 绑定
+-------------------+

高优先级队列的存在意义:FORGET 请求通知服务端减少 inode 引用计数, 若被普通请求阻塞则会造成内存泄漏。单独分配队列确保 FORGET 始终可以被立即处理。

13.2 virtio_fs_init_vq 初始化

fs/fuse/virtio_fs.c 第 907-930 行,两种队列类型使用不同的工作队列处理器:

static void virtio_fs_init_vq(struct virtio_fs_vq *fsvq, char *name,
                               int vq_type)
{
    strscpy(fsvq->name, name, VQ_NAME_LEN);
    spin_lock_init(&fsvq->lock);
    INIT_LIST_HEAD(&fsvq->queued_reqs);
    INIT_LIST_HEAD(&fsvq->end_reqs);
    init_completion(&fsvq->in_flight_zero);

    if (vq_type == VQ_REQUEST) {
        INIT_WORK(&fsvq->done_work, virtio_fs_requests_done_work);
        INIT_WORK(&fsvq->dispatch_work,
                  virtio_fs_request_dispatch_work);
    } else {
        /* hiprio 队列使用单独的 forget/interrupt 处理器 */
        INIT_WORK(&fsvq->done_work, virtio_fs_hiprio_done_work);
        INIT_WORK(&fsvq->dispatch_work,
                  virtio_fs_hiprio_dispatch_work);
    }
}

13.3 队列连接状态管理

fsvq->connected 标志控制队列是否接受新请求(第 430-433 行在 start_all_queues 中设置):

static void virtio_fs_start_all_queues(struct virtio_fs *fs)
{
    for (i = 0; i < fs->nvqs; i++) {
        fsvq = &fs->vqs[i];
        spin_lock(&fsvq->lock);
        fsvq->connected = true;
        spin_unlock(&fsvq->lock);
    }
}

设备移除时(virtio_fs_stop_all_queues)先将 connected 设为 false, 之后再 drain 所有飞行请求,确保不接受新请求的同时完成已在途的请求。

13.4 dispatch_work 重试机制

当 virtqueue 满时,queued_reqs 链表积压请求。 virtio_fs_request_dispatch_work() 会按顺序重新提交这些请求:

virtqueue 完成 -> done_work -> 发现 queued_reqs 非空
    -> schedule dispatch_work
    -> dispatch_work: 从 queued_reqs 取出请求
    -> 尝试重新 virtio_fs_enqueue_req()
    -> 若再次失败(ENOSPC),重新放回 queued_reqs

此设计避免了忙等待,同时保证请求的 FIFO 顺序。


14. DAX window 内存管理机制

14.1 fuse_conn_dax 结构

fs/fuse/dax.c 第 71-93 行,DAX 连接级状态:

struct fuse_conn_dax {
    struct dax_device *dev;

    spinlock_t lock;

    /* 正在使用的 DAX 映射范围列表 */
    unsigned long nr_busy_ranges;
    struct list_head busy_ranges;

    /* 异步释放工作队列 */
    struct delayed_work free_work;

    /* 等待有可用范围的等待队列 */
    wait_queue_head_t range_waitq;

    /* 空闲的 DAX 内存范围 */
    long nr_free_ranges;
    struct list_head free_ranges;

    unsigned long nr_ranges;
};

14.2 fuse_dax_mapping 结构

fs/fuse/dax.c 第 35-59 行,每个 DAX 映射区间的描述符:

struct fuse_dax_mapping {
    struct inode *inode;           /* 所属 inode */
    struct list_head list;         /* 挂在 fcd->free_ranges */
    struct interval_tree_node itn; /* 区间树节点(按文件偏移索引) */
    struct list_head busy_list;    /* 挂在 fcd->busy_ranges */

    u64 window_offset;             /* DAX 窗口内偏移 */
    loff_t length;                 /* 映射长度(字节) */
    bool writable;                 /* 读写还是只读映射 */
    refcount_t refcnt;             /* iomap 使用时的引用计数 */
};

14.3 DAX 映射的分配单位

fs/fuse/dax.c 第 20-22 行:

#define FUSE_DAX_SHIFT  21
#define FUSE_DAX_SZ     (1 << FUSE_DAX_SHIFT)  /* 2MB */
#define FUSE_DAX_PAGES  (FUSE_DAX_SZ / PAGE_SIZE) /* 512 页 */

每次 FUSE_SETUPMAPPING 申请 2MB 对齐的 DAX 窗口区间,这与 常见的 FUSE_INIT 协商的 map_alignment = 21(2MB)对应。

14.4 DAX 内存回收

当空闲范围低于总范围数的 20%(FUSE_DAX_RECLAIM_THRESHOLD)时, 触发 fcd->free_work 延迟工作(dax.c:108-118):

static void __kick_dmap_free_worker(struct fuse_conn_dax *fcd,
                                     unsigned long delay_ms)
{
    free_threshold = max_t(unsigned long,
        fcd->nr_ranges * FUSE_DAX_RECLAIM_THRESHOLD / 100, 1);
    if (fcd->nr_free_ranges < free_threshold)
        queue_delayed_work(system_long_wq, &fcd->free_work,
                           msecs_to_jiffies(delay_ms));
}

回收时,内核发送 FUSE_REMOVEMAPPING 通知 virtiofsd 解除映射, 再将对应的 fuse_dax_mapping 归还到 fcd->free_ranges

14.5 DAX 区间树

每个 inode 的 fuse_inode_dax 维护一棵 interval_treedax.c:62-69):

struct fuse_inode_dax {
    struct rw_semaphore sem;    /* 保护树的并发修改 */
    struct rb_root_cached tree; /* 按文件偏移排序的区间树 */
    unsigned long nr;           /* 节点数量 */
};

查找时通过 interval_tree_iter_first(&fi_dax->tree, start, end) 找到 覆盖目标文件范围的 fuse_dax_mapping,避免重复申请。


15. 9P TCP 传输层与 fd 传输层

15.1 p9_conn 结构(fd/TCP 传输)

net/9p/trans_fd.c 第 77-97 行,fd 传输的多路复用连接状态:

struct p9_conn {
    struct list_head mux_list;
    struct p9_client *client;
    int err;
    spinlock_t req_lock;
    struct list_head req_list;         /* 已发送等待响应的请求 */
    struct list_head unsent_req_list;  /* 待发送的请求 */
    struct p9_req_t *rreq;             /* 当前正在读取的响应 */
    struct p9_req_t *wreq;             /* 当前正在写入的请求 */
    char tmp_buf[P9_HDRSZ];            /* 读取消息头的临时缓冲 */
    struct p9_fcall rc;                /* 当前读取帧 */
    int wpos;                          /* 写位置 */
    int wsize;                         /* 本帧待写字节数 */
    char *wbuf;                        /* 当前写缓冲 */
    struct list_head poll_pending_link;
    struct p9_poll_wait poll_wait[MAXPOLLWADDR];
    poll_table pt;
    struct work_struct rq;             /* 读工作队列 */
    struct work_struct wq;             /* 写工作队列 */
    unsigned long wsched;
};

15.2 p9_trans_fd 结构

net/9p/trans_fd.c 第 107-111 行:

struct p9_trans_fd {
    struct file *rd;    /* 读文件(可与 wr 相同) */
    struct file *wr;    /* 写文件 */
    struct p9_conn conn;
};

TCP 传输通过 socket 创建 rdwr 指向同一个 struct file。 fd 传输(trans=fd)允许 rfdwfd 分别指定(用于管道场景)。

15.3 TCP 传输的读写工作队列

fd 传输使用事件驱动模型(net/9p/trans_fd.c:37-38):

static struct p9_trans_module p9_tcp_trans;
static struct p9_trans_module p9_fd_trans;

全局轮询列表(第 115-117 行):

static DEFINE_SPINLOCK(p9_poll_lock);
static LIST_HEAD(p9_poll_pending_list);
static DECLARE_WORK(p9_poll_work, p9_poll_workfn);

当 socket 可读/写时,p9_poll_workfn 遍历 p9_poll_pending_list, 对每个连接调用读/写工作队列(rq/wq)进行实际的数据传输。

15.4 各传输层能力对比

+------------------+----------+-----------+----------+----------+
| 特性             | virtio   | TCP/fd    | RDMA     | XEN      |
+------------------+----------+-----------+----------+----------+
| 零拷贝           | 是(pin页) | 否        | 是(RDMA) | 否       |
| 多路复用         | tag IDR  | tag IDR   | tag IDR  | tag IDR  |
| pooled_rbuffers  | 否       | 否        | 是       | 否       |
| 最大 msize       | 512KB    | 无限制    | 无限制   | 受限     |
| 内核态           | 是       | 是        | 是       | 是       |
+------------------+----------+-----------+----------+----------+

pooled_rbuffersp9_trans_module 字段)为真时,p9_client_rpc 使用 固定大小 msize 作为响应缓冲容量,以匹配 RDMA 的池化缓冲区设计。


16. virtiofs 安全模型与 namespace 隔离

16.1 User Namespace 支持

virtiofs 的文件系统类型标志包含 FS_ALLOW_IDMAPvirtio_fs.c:1759):

static struct file_system_type virtio_fs_type = {
    .owner           = THIS_MODULE,
    .name            = "virtiofs",
    .init_fs_context = virtio_fs_init_fs_context,
    .kill_sb         = virtio_kill_sb,
    .fs_flags        = FS_ALLOW_IDMAP,
};

FS_ALLOW_IDMAP 允许使用 mount --bind -o idmap 进行 UID/GID 映射, 这是容器场景中 rootless 模式的基础。

16.2 FUSE 请求中的 uid/gid/pid 传递

每个 fuse_in_header 携带调用者的 uidgidpid(第 263-284 行)。 virtiofsd 接收到请求后,使用这些字段来执行安全检查:

guest 应用进程(uid=1000, gid=1000)
    --> fuse_in_header.uid = 1000, gid = 1000
    --> virtiofsd 用此 uid/gid 以进行 host 上的访问控制

通过 idmap mount,virtiofsd 可以配置 UID 映射:

  • guest uid=0 --> host uid=100000(rootless 容器场景)

16.3 pid_namespace 与用户隔离

fuse_conn 中保存了创建连接的 pid_namespace(fuse_i.h:668):

struct pid_namespace *pid_ns;  /* The pid namespace for this mount */
struct user_namespace *user_ns; /* The user namespace for this mount */

pid_ns 用于在 FUSE 请求的 pid 字段中传递正确的 pid, 使 virtiofsd 能够识别请求来源进程(用于审计、调试等)。

16.4 default_permissions 与 allow_other

virtio_fs_ctx_set_defaults() 默认启用这两个选项(第 1528-1538 行):

static inline void virtio_fs_ctx_set_defaults(struct fuse_fs_context *ctx)
{
    ctx->rootmode = S_IFDIR;
    ctx->default_permissions = 1;  /* 内核层做权限检查 */
    ctx->allow_other = 1;          /* 允许非挂载用户访问 */
    ctx->max_read = UINT_MAX;
    ctx->blksize = 512;
    ctx->destroy = true;
    ctx->no_control = true;        /* 不在 fusectl 下创建条目 */
    ctx->no_force_umount = true;
}

default_permissions=1 使内核在调用 FUSE 操作前先检查文件权限, 减少不必要的往返(避免对明显失败的操作发送请求到 virtiofsd)。

16.5 安全属性(security xattr)初始化

fuse_conn.init_securityfuse_i.h:895)标志控制创建新 inode 时 是否自动初始化 SELinux/AppArmor 安全标签:

unsigned int init_security:1;  /* Initialize security xattrs when creating a new inode */

这通过 fuse_init_security() 实现,在 mknod/create 完成后调用 安全模块的 inode_init_security 钩子,并将结果通过 FUSE_SETXATTR 传递给 virtiofsd。


17. xattr 与 ACL 支持

17.1 FUSE xattr 操作

fs/fuse/xattr.c 中实现了完整的 xattr 操作集:

setxattr(第 14-48 行)

int fuse_setxattr(struct inode *inode, const char *name, const void *value,
                  size_t size, int flags, unsigned int extra_flags)
{
    FUSE_ARGS(args);
    struct fuse_setxattr_in inarg;

    inarg.size = size;
    inarg.flags = flags;
    inarg.setxattr_flags = extra_flags;  /* 扩展 xattr 标志 */

    args.opcode = FUSE_SETXATTR;
    args.nodeid = get_node_id(inode);
    args.in_numargs = 3;
    /* in_args[0]: fuse_setxattr_in */
    /* in_args[1]: xattr 名称(含 NUL) */
    /* in_args[2]: xattr 值 */
    err = fuse_simple_request(fm, &args);
}

setxattr_ext 标志(fuse_i.h:799)控制是否发送完整的 fuse_setxattr_in 或兼容的 FUSE_COMPAT_SETXATTR_IN_SIZE(不含 setxattr_flags 字段)。

getxattr(第 51-88 行):两次请求设计

  • size=0:返回 xattr 值大小(通过 fuse_getxattr_out.size
  • size>0:直接返回值到提供的缓冲区

17.2 virtiofs xattr 的特殊考虑

virtiofsd 使用 passthrough 模式时,会直接映射 host 文件的 xattr。 常见 xattr 命名空间的处理:

security.*  -> SELinux/AppArmor 标签(需 virtiofsd 特权)
trusted.*   -> 需要 CAP_SYS_ADMIN
user.*      -> 普通用户空间 xattr
system.*    -> 内核保留(如 system.posix_acl_access)

virtiofsd 提供 --sandbox=chroot 模式来限制 host 文件系统访问范围, 仅暴露指定的 shared directory,xattr 也被限制在此范围内。

17.3 9P xattr 支持

fs/9p/xattr.c 中,9P2000.L 通过 TXATTRWALK/TXATTRCREATE 协议消息实现 xattr。

v9fs_fill_super() 中的 xattr handler 安装(vfs_super.c:44-47):

if (v9fs_proto_dotl(v9ses)) {
    sb->s_op = &v9fs_super_ops_dotl;
    if (!(v9ses->flags & V9FS_NO_XATTR))
        sb->s_xattr = v9fs_xattr_handlers;
}

V9FS_NO_XATTR 标志(v9fs.h:43)可以在 mount 时通过 noxattr 选项禁用 xattr, 以避免额外的协议往返开销。

17.4 POSIX ACL 支持

9P 的 POSIX ACL 由 fs/9p/acl.c 实现,通过 xattr 接口读写 system.posix_acl_accesssystem.posix_acl_default

v9fs_fill_super() 中的 ACL 标志设置(第 68-71 行):

#ifdef CONFIG_9P_FS_POSIX_ACL
    if ((v9ses->flags & V9FS_ACL_MASK) == V9FS_POSIX_ACL)
        sb->s_flags |= SB_POSIXACL;
#endif

virtiofs 使用 FUSE 的 fc->posix_acl 标志(fuse_i.h:862)支持 POSIX ACL, 通过标准的 posix_acl_xattr 接口与 virtiofsd 交互。


18. virtiofs 卸载与热拔插流程

18.1 正常卸载流程

umount /mnt/virtiofs
    |
    v
virtio_kill_sb()           [virtio_fs.c:1636]
    |
    +-- fuse_mount_remove(fm)    从 fc->mounts 移除
    |
    +-- virtio_fs_conn_destroy(fm)
    |       |
    |       +-- fuse_dax_cancel_work(fc)   停止 DAX 回收工作
    |       |
    |       +-- fsvq->connected = false   停止 hiprio 队列接受新请求
    |       |
    |       +-- virtio_fs_drain_all_queues()  等待飞行请求完成
    |       |
    |       +-- fuse_conn_destroy(fm)
    |       |     --> 发送 FUSE_DESTROY 请求
    |       |
    |       +-- virtio_fs_stop_all_queues()   停止所有队列
    |       +-- virtio_fs_drain_all_queues()  再次 drain
    |       +-- virtio_fs_free_devs()         释放 fuse_dev
    |
    +-- kill_anon_super(sb)
    +-- fuse_mount_destroy(fm)

18.2 热拔插(设备移除)流程

当 virtio 设备被强制移除时(virtio_fs_remove,第 1164-1197 行):

virtio_fs_remove()
    |
    +-- 从 virtio_fs_instances 链表移除(virtio_fs_remove_instance)
    |
    +-- virtio_fs_stop_all_queues()
    |
    +-- virtio_fs_drain_all_queues_locked()  等待飞行请求完成
    |     注意:此时 fsvq->connected 已为 false,
    |           新请求会收到 -ENOTCONN 错误
    |
    +-- virtio_reset_device(vdev)           复位 virtio 设备
    +-- virtio_fs_cleanup_vqs(vdev)         销毁 virtqueue
    +-- virtio_fs_put_locked(fs)            释放引用

设备移除后,已挂载的文件系统进入"僵死"状态,所有后续 IO 返回 -ENOTCONN。 用户必须 umount 才能完全清理。

18.3 drain 机制详解

virtio_fs_drain_queue()(第 296-315 行)利用 completion 等待计数归零:

static void virtio_fs_drain_queue(struct virtio_fs_vq *fsvq)
{
    WARN_ON(fsvq->in_flight < 0);

    spin_lock(&fsvq->lock);
    if (fsvq->in_flight) {
        reinit_completion(&fsvq->in_flight_zero);
        spin_unlock(&fsvq->lock);
        wait_for_completion(&fsvq->in_flight_zero);
    } else {
        spin_unlock(&fsvq->lock);
    }

    flush_work(&fsvq->done_work);     /* 等待 done_work 工作队列 */
    flush_work(&fsvq->dispatch_work); /* 等待 dispatch_work 工作队列 */
}

这是一个无超时等待,理论上在设备正常工作时 in_flight 会最终归零。 若设备故障无响应,此处会永久阻塞——这是 virtio 设备驱动的通用问题, 目前尚无内核级超时处理。


19. fuse_conn 能力位协商详解

19.1 FUSE_INIT 握手

FUSE_INIT 是建立连接的第一个请求,double 方向都可以协商能力标志。

关键 flags(include/uapi/linux/fuse.h):

FUSE_ASYNC_READ         (1 << 0)   异步读请求
FUSE_POSIX_LOCKS        (1 << 1)   POSIX 锁支持
FUSE_FILE_OPS           (1 << 2)   文件操作支持
FUSE_ATOMIC_O_TRUNC     (1 << 3)   原子 O_TRUNC
FUSE_EXPORT_SUPPORT     (1 << 4)   NFS 导出支持
FUSE_BIG_WRITES         (1 << 5)   大写入(废弃,由 max_write 代替)
FUSE_DONT_MASK          (1 << 6)   不应用 umask
FUSE_SPLICE_WRITE       (1 << 7)   splice 写
FUSE_SPLICE_MOVE        (1 << 8)   splice 移动
FUSE_SPLICE_READ        (1 << 9)   splice 读
FUSE_FLOCK_LOCKS        (1 << 10)  BSD flock 支持
FUSE_HAS_IOCTL_DIR      (1 << 11)  目录上的 ioctl
FUSE_AUTO_INVAL_DATA    (1 << 12)  自动数据缓存失效
FUSE_DO_READDIRPLUS     (1 << 13)  READDIRPLUS 支持
FUSE_READDIRPLUS_AUTO   (1 << 14)  自适应 READDIRPLUS
FUSE_ASYNC_DIO          (1 << 15)  异步直接 IO
FUSE_WRITEBACK_CACHE    (1 << 16)  回写缓存
FUSE_NO_OPEN_SUPPORT    (1 << 17)  不需要 open/release
FUSE_PARALLEL_DIROPS    (1 << 18)  并行目录操作
FUSE_HANDLE_KILLPRIV    (1 << 19)  处理 kill-suid
FUSE_POSIX_ACL          (1 << 20)  POSIX ACL
FUSE_ABORT_ERROR        (1 << 21)  abort 时返回唯一错误
FUSE_MAX_PAGES          (1 << 22)  max_pages 字段有效
FUSE_CACHE_SYMLINKS     (1 << 23)  缓存符号链接
FUSE_NO_OPENDIR_SUPPORT (1 << 24)  不需要 opendir/releasedir
FUSE_EXPLICIT_INVAL_DATA (1 << 25) 显式数据缓存失效
FUSE_MAP_ALIGNMENT      (1 << 26)  map_alignment 字段有效(DAX)
FUSE_SUBMOUNTS          (1 << 27)  submounts 支持
FUSE_HANDLE_KILLPRIV_V2 (1 << 28)  v2 kill-priv 语义
FUSE_SETXATTR_EXT       (1 << 29)  扩展 setxattr(含 setxattr_flags)
FUSE_INIT_EXT           (1 << 30)  init 消息中 flags2 字段有效
FUSE_INIT_RESERVED      (1 << 31)  保留

19.2 flags2 扩展标志

flags 字段已满,协议通过 flags2 字段扩展(需 FUSE_INIT_EXT 标志):

FUSE_SECURITY_CTX   (1 << 0)   安全上下文(SELinux label 传递)
FUSE_HAS_INODE_DAX  (1 << 1)   per-inode DAX 支持
FUSE_CREATE_SUPP_GROUP (1 << 2) 创建时携带附加组信息
FUSE_HAS_EXPIRE_ONLY (1 << 3)  只支持 expire(不支持 inval)
FUSE_DIRECT_IO_ALLOW_MMAP (1 << 4) FOPEN_DIRECT_IO 模式下允许 mmap
FUSE_PASSTHROUGH    (1 << 5)   passthrough IO 支持
FUSE_NO_EXPORT_SUPPORT (1 << 6) 不支持 NFS 导出(即使设置了 EXPORT_SUPPORT)
FUSE_HAS_RESEND     (1 << 7)   支持请求重发(io_uring 用)

19.3 virtiofs 的特殊能力设置

virtiofs 在 fuse_fill_super_common() 之后,由 virtio_fs_get_tree() 追加设置:

fc->delete_stale     = true;  /* 不保留过时 dentry */
fc->auto_submounts   = true;  /* 自动处理服务端通知的 submounts */
fc->sync_fs          = true;  /* 允许 syncfs() 跨越到服务端 */
fc->use_pages_for_kvec_io = true; /* kvec IO 用页,而非指针 */

auto_submounts 对应 FUSE_SUBMOUNTS 能力:当 virtiofsd 实现文件系统挂载点时, 服务端可以通过 FUSE_ATTR_SUBMOUNT 标志通知客户端,客户端自动创建 submount。


20. 9P 协议编解码机制

20.1 p9_fcall 缓冲区

include/net/9p/client.h 中的 p9_fcall 是消息序列化/反序列化的容器:

struct p9_fcall {
    u32 size;      /* 消息实际大小 */
    u8 id;         /* 消息类型(p9_msg_t) */
    u16 tag;       /* 事务标签 */

    size_t offset; /* 序列化/反序列化当前偏移 */
    size_t capacity; /* 缓冲区容量 */

    struct kmem_cache *cache; /* 来自哪个 cache(用于正确释放)*/
    u8 *sdata;     /* 实际数据缓冲区 */
    bool zc;       /* 是否零拷贝模式 */
};

20.2 p9pdu 序列化原理

net/9p/protocol.c 中的 p9pdu_vwritef()p9pdu_vreadf() 实现了 基于格式字符串的序列化/反序列化(类似 printf/scanf 但针对 9P 类型):

格式字符串中的类型描述符:
b -> uint8_t   (1 字节)
w -> uint16_t  (2 字节, 小端)
d -> uint32_t  (4 字节, 小端)
q -> uint64_t  (8 字节, 小端)
s -> string    (2 字节长度 + 数据,不含 NUL)
Q -> struct p9_qid (13 字节)
S -> struct p9_wstat
A -> struct p9_stat_dotl
? -> 根据协议版本条件包含

例如,TWALK 消息的格式字符串:

p9_client_walk():
    p9_client_rpc(c, P9_TWALK, "ddd?k", fid->fid, nfid->fid, nwname, wnames);
    /* d=fid, d=newfid, d=nwnames, ?k=条件字符串数组 */

20.3 p9_client_rpc 通用 RPC 路径

net/9p/client.c 第 550-639 行的核心 RPC 函数:

static struct p9_req_t *
p9_client_rpc(struct p9_client *c, int8_t type, const char *fmt, ...)
{
    /* 1. 准备请求:分配 tag,序列化参数 */
    req = p9_client_prepare_req(c, type, tsize, rsize, fmt, ap);

    /* 2. 发送到传输层 */
    err = c->trans_mod->request(c, req);

    /* 3. 等待响应(可被信号中断) */
    err = io_wait_event_killable(req->wq,
                                 READ_ONCE(req->status) >= REQ_STATUS_RCVD);

    /* 4. 若被信号中断,尝试 flush(取消)请求 */
    if (err == -ERESTARTSYS && c->status == Connected) {
        if (c->trans_mod->cancel(c, req))
            p9_client_flush(c, req);
    }

    /* 5. 检查响应中的错误码 */
    err = p9_check_errors(c, req);
    return req;
}

20.4 错误处理

9P2000.L 使用 RLERROR 消息返回 Linux errno(而非 9P2000 的错误字符串):

/* net/9p/client.c */
static int p9_check_errors(struct p9_client *c, struct p9_req_t *req)
{
    if (req->rc.id == P9_RERROR) {
        /* 9P2000/2000.u 文本错误 */
        err = p9pdu_readf(&req->rc, c->proto_version, "s?d",
                          &ename, &ecode);
    } else if (req->rc.id == P9_RLERROR) {
        /* 9P2000.L 数字错误码 */
        err = p9pdu_readf(&req->rc, c->proto_version, "d", &ecode);
    }
}

20.5 tag 分配与多路复用

p9_tag_alloc() 通过 IDR 分配唯一的请求 tag(16 位,最大 65535 个并发请求):

struct p9_req_t *p9_tag_alloc(struct p9_client *c, int8_t type,
                               uint t_size, uint r_size,
                               const char *fmt, va_list ap)
{
    int tag = idr_alloc(&c->reqs, req, P9_NOTAG + 1, P9_NOTAG, GFP_NOWAIT);
    req->tc.tag = tag;
    req->rc.tag = P9_NOTAG;  /* 响应 tag 在收到后会被匹配 */
}

21. DAX 与非 DAX 路径性能对比

21.1 读取路径对比

非 DAX 读取路径(普通 FUSE 模式):
==================================
应用 read()
    --> VFS read_iter
    --> fuse_file_read_iter / fuse_readpages
    --> 分配内核页(guest 内存)
    --> 发送 FUSE_READ 请求(携带页的 SG)
    --> virtiofsd: pread(host_fd, ...) --> 数据写入 guest 页
    --> guest: 从内核页 copy_to_user
    --> 应用收到数据
    数据路径:host_file -> guest_kernel_page -> guest_userspace
    拷贝次数:2次(host pread + copy_to_user)

DAX 读取路径(mmap 模式):
===========================
应用 mmap() + 访问内存
    --> page fault
    --> fuse_dax_fault --> dax_iomap_fault
    --> 查找 fuse_dax_mapping(已有则直接使用)
    --> 若无:发送 FUSE_SETUPMAPPING
    --> virtiofsd: mmap(host_fd) 到 DAX window
    --> guest: 建立 PTE 指向 DAX window 物理地址
    --> 后续访问:直接读 PCI BAR 内存,无内核介入
    数据路径:host_file 通过 DAX window 直接可见
    拷贝次数:0次(mmap 后无额外拷贝)

21.2 写入路径对比

非 DAX 写入路径(回写缓存模式):
================================
应用 write()
    --> VFS write_iter
    --> fuse_perform_write
    --> 将数据写入 guest 页缓存
    --> 标记页为 dirty
    --> writeback(异步):
    --> 发送 FUSE_WRITE(携带 dirty 页的 SG)
    --> virtiofsd: pwrite(host_fd, ...)
    拷贝次数:1次(write_iter 拷贝到 page cache,writeback 无额外拷贝)

DAX 写入路径:
==============
应用通过 mmap 写入(已建立映射)
    --> 直接写 PCI BAR 物理地址(通过 PTE)
    --> 无内核介入,无拷贝
    --> host 通过 mmap 的共享内存直接看到修改
    拷贝次数:0次

DAX 通过 write() 写入:
=======================
应用 write()
    --> VFS write_iter
    --> iomap_write_iter(DAX 模式)
    --> 不经过 page cache,直接写 DAX 窗口
    --> virtiofsd 通过共享内存看到修改
    拷贝次数:1次(userspace -> DAX window)

21.3 延迟分析

路径 主要延迟来源 典型延迟
非 DAX 读 virtio ring 通知 + virtiofsd pread ~10-100 µs
非 DAX 写 写入 page cache(即时)+ writeback 写:<1 µs
DAX 首次访问 FUSE_SETUPMAPPING + PTE 建立 ~50-200 µs
DAX 后续访问 直接内存访问(无系统调用) ~1-10 ns

DAX 模式的初始化开销较大,但对大文件的随机访问工作负载(如数据库页读取), 摊销后的每次访问延迟极低。

21.4 内存占用对比

非 DAX 模式:
  guest 页缓存 = 文件数据副本(guest 内存)
  读写都经过 guest page cache
  内存占用 = 缓存文件大小(最大为 guest RAM 的可用部分)

DAX 模式:
  无 guest 页缓存(数据直接在 DAX 窗口)
  DAX 窗口大小 = host 预分配的 PCI BAR 大小(固定)
  内存占用 = 活跃映射的 DAX ranges(每个 2MB)
  内存压力下:触发回收,通过 FUSE_REMOVEMAPPING 释放

22. Kata Containers 场景下的应用

22.1 Kata Containers 架构概述

+-------------------------------------------------------------+
|  宿主机(Host)                                              |
|                                                              |
|  containerd / CRI-O                                          |
|       |                                                      |
|       v                                                      |
|  kata-runtime                                                |
|       |                                                      |
|       +-- QEMU/cloud-hypervisor(轻量 VM)                  |
|       |       |                                              |
|       |       +-- virtio-fs 设备(共享容器根目录)           |
|       |       +-- virtio-net、virtio-blk 等                 |
|       |                                                      |
|       +-- virtiofsd(每个 VM 独立实例)                      |
|               |                                              |
|               +-- passthrough 到 OCI bundle 根目录           |
|               +-- shared rootfs(通常是 overlayfs)          |
+-------------------------------------------------------------+
         |  virtio PCIe 共享内存
+-------------------------------------------------------------+
|  Guest VM(轻量内核 kata-kernel)                            |
|                                                              |
|  virtiofs 客户端                                             |
|  --> 挂载为容器根目录 /                                      |
|                                                              |
|  应用进程(容器负载)                                        |
+-------------------------------------------------------------+

22.2 virtiofs 在 Kata 中的挂载

Kata 容器默认使用 virtiofs 挂载容器的 rootfs 和数据卷:

# Host: virtiofsd 启动命令(简化)
virtiofsd --socket-path /run/kata-containers/xxx/virtiofs.sock \
           --shared-dir /var/lib/containers/overlay/xxx/merged \
           --sandbox=chroot

# Guest 内核: 内核命令行包含
# virtiofs.tag=kataShared virtiofs.mount-point=/

virtiofsd 运行在 VM 外(宿主机),通过 UNIX socket 与 QEMU 的 vhost-user 后端通信,QEMU 将其转化为 virtio-fs 设备暴露给 guest 内核。

22.3 DAX 在 Kata 中的意义

Kata Containers 使用 virtiofs DAX 的主要场景:

  1. 共享镜像层:多个 Kata VM 共享同一容器镜像层(只读), 通过 DAX 直接映射同一物理内存,不需要在每个 VM 中分别缓存。

  2. 大文件访问:数据库容器、机器学习推理容器的大文件 通过 DAX mmap 实现接近原生的访问性能。

  3. 内存气球配合:当 VM 内存压力大时,DAX 区域可通过回收机制 释放 DAX window 映射,将宿主机内存还给其他用途。

22.4 多个 VM 共享 virtiofsd

+--------------------+    +--------------------+
|  Kata VM 1         |    |  Kata VM 2         |
|  virtiofs client   |    |  virtiofs client   |
+---------|----------+    +---------|----------+
          |  virtio-fs                |  virtio-fs
+---------|--------------------------|----------+
|         v                          v          |
|  QEMU 1 (vhost-user frontend)  QEMU 2         |
+---------|--------------------------|----------+
          |  UNIX socket              |  UNIX socket
+---------|--------------------------|----------+
|         v                          v          |
|  virtiofsd 1                  virtiofsd 2     |
|  (share: /overlay/vm1/merged) (share: /overlay/vm2/merged)
+--------------------------------------------------+
          共享只读层(通过 overlayfs)可以复用

每个 VM 拥有独立的 virtiofsd 实例,保证安全隔离。 只读镜像层在 host 侧通过 overlayfs lower dir 共享,在 guest 侧 每个 VM 看到的是独立的可写视图(overlay 的 upper dir 各自独立)。

22.5 9P 在旧版 Kata 中的使用

早期 Kata Containers(v1.x)使用 virtio-9p 而非 virtiofs:

优点:无需独立 virtiofsd 进程,QEMU 内置实现
缺点:
  - QEMU VirtFS (9P) 性能不如 virtiofsd passthrough
  - 无 DAX 支持,所有文件数据经过 guest 页缓存
  - 单线程服务端,高并发 IO 性能差
  - 路径解析在 QEMU 内进行,与 host VFS 有语义差异

现代 Kata Containers(v2.x)已全面切换到 virtiofs,9P 仅在特殊场景保留。

22.6 virtiofs 安全隔离的重要性

Kata Containers 的核心价值是 VM 级别的隔离。virtiofs 的安全考量:

  1. virtiofsd 沙箱:通过 --sandbox=chroot 限制 virtiofsd 只能访问 指定的 shared dir,即使 guest 发出任意文件路径请求也无法逃逸。

  2. SELinux/AppArmor 标签:virtiofsd 可以使用安全模块保护 自身不被攻击,同时正确传递文件的 security xattr。

  3. uid/gid 隔离:通过 idmap mount,guest 内的 root(uid=0) 映射到 host 上的非特权 uid,防止 guest root 直接操作 host 文件。

  4. virtio 协议隔离:guest 通过 virtio 消息与 virtiofsd 通信, 攻击面限于 FUSE 协议层,不涉及 host kernel 的直接访问。


23. sysfs 接口与可观测性

23.1 virtiofs sysfs 目录结构

virtiofs 实例在 /sys/fs/virtio_fs/ 下创建目录(第 37-38 行,第 419-420 行):

/sys/fs/virtio_fs/
└── 0/                          ← vdev->index(第一个 virtio-fs 设备)
    ├── tag                     ← 文件系统标签(只读)
    ├── device -> /sys/bus/virtio/devices/virtio0  ← 符号链接
    └── mqs/                    ← 队列子目录
        ├── 0/                  ← VQ_HIPRIO
        │   ├── name            ← "hiprio"
        │   └── cpu_list        ← "0, 1, 2, ..." (所有 CPU)
        ├── 1/                  ← VQ_REQUEST (第0个请求队列)
        │   ├── name            ← "requests.0"
        │   └── cpu_list        ← "0, 4, 8, ..." (绑定的 CPU)
        └── 2/
            ├── name            ← "requests.1"
            └── cpu_list        ← "1, 5, 9, ..."

cpu_list_show()(第 231-259 行)实现展示:

static ssize_t cpu_list_show(struct kobject *kobj,
                              struct kobj_attribute *attr, char *buf)
{
    qid = fsvq->vq->index;
    for (cpu = 0; cpu < nr_cpu_ids; cpu++) {
        /* hiprio 队列对应所有 CPU;request 队列按 mq_map 过滤 */
        if (qid < VQ_REQUEST || (fs->mq_map[cpu] == qid)) {
            /* 追加 cpu 到输出 */
        }
    }
}

23.2 FUSE 控制文件系统

普通 FUSE 挂载会在 /sys/fs/fuse/connections/<minor>/ 下创建控制节点, 但 virtiofs 设置了 ctx->no_control = true(第 1536 行), 不在 fusectl 文件系统中创建条目,避免用户通过 fusectl 干扰 virtiofs 连接。

23.3 9P 调试接口

9P 内置了详细的调试系统(include/net/9p/9p.h):

enum p9_debug_flags {
    P9_DEBUG_ERROR  = (1<<0),    /* 错误日志 */
    P9_DEBUG_9P     = (1<<2),    /* 协议消息追踪 */
    P9_DEBUG_VFS    = (1<<3),    /* VFS 层调用追踪 */
    P9_DEBUG_CONV   = (1<<4),    /* 序列化/反序列化 */
    P9_DEBUG_MUX    = (1<<5),    /* 多路复用层 */
    P9_DEBUG_TRANS  = (1<<6),    /* 传输层 */
    P9_DEBUG_SLABS  = (1<<7),    /* slab 分配 */
    P9_DEBUG_FCALL  = (1<<8),    /* fcall 详细内容 */
};

通过 /sys/module/9p/parameters/debug 动态设置调试级别:

echo 0x3 > /sys/module/9p/parameters/debug  # 开启 ERROR + 9P 追踪

23.4 tracepoint 支持

9P 使用 tracepoints(net/9p/client.c:29-30):

#define CREATE_TRACE_POINTS
#include <trace/events/9p.h>

virtiofs 通过 FUSE 的 tracepoints 记录请求(fs/fuse/trace.c), 可通过 trace-cmd 或 ftrace 进行详细的 IO 追踪:

# 追踪 FUSE 操作
trace-cmd record -e fuse:fuse_request_send \
                  -e fuse:fuse_request_end \
                  sleep 5
trace-cmd report

23.5 virtio ring 统计

通过 /sys/bus/virtio/devices/virtio0/ 可以访问 virtio 设备统计:

/sys/bus/virtio/devices/virtio0/
├── features           ← 协商的 virtio features
├── status             ← 设备状态
├── vendor             ← 厂商 ID
└── device             ← 设备 ID(26 for FS)

virtqueue 级别的统计通过 virtqueue_get_vring_size() 获取 ring 大小, 通过 vring_interrupt() 的 IRQ 统计可以观察中断频率。


附录:关键文件路径索引

文件路径 内容
fs/fuse/virtio_fs.c virtiofs 驱动核心
fs/fuse/fuse_i.h FUSE 内部数据结构
fs/fuse/dax.c FUSE DAX 映射管理
fs/fuse/xattr.c FUSE xattr 操作
fs/fuse/passthrough.c FUSE passthrough IO
fs/fuse/dev.c FUSE 设备操作
fs/fuse/dir.c FUSE 目录操作
fs/fuse/file.c FUSE 文件操作
include/uapi/linux/fuse.h FUSE 协议定义(uapi)
include/uapi/linux/virtio_fs.h virtio-fs 设备配置
net/9p/client.c 9P 客户端核心
net/9p/trans_virtio.c 9P virtio 传输层
net/9p/trans_fd.c 9P TCP/fd 传输层
net/9p/protocol.c 9P 消息序列化/反序列化
include/net/9p/9p.h 9P 协议定义
include/net/9p/client.h 9P 客户端数据结构
fs/9p/v9fs.h 9P VFS 会话结构
fs/9p/vfs_inode.c 9P VFS inode 操作
fs/9p/vfs_inode_dotl.c 9P2000.L VFS inode 操作
fs/9p/vfs_super.c 9P 超级块操作
fs/9p/fid.c 9P fid 管理
fs/9p/xattr.c 9P xattr 支持
fs/9p/acl.c 9P POSIX ACL 支持

总结

本文对 Linux 内核中 virtiofs 与 9P 文件系统进行了深度分析,核心要点如下:

virtiofs 的架构优势

  • 复用 FUSE 协议栈,继承其成熟的 VFS 集成和安全模型
  • 多队列设计(per-CPU virtqueue)消除了全局锁争用
  • DAX 模式通过 PCI BAR 共享内存实现零拷贝,完全绕过 guest 页缓存
  • virtio_fs_fiq_ops 回调机制将 virtio 传输透明接入 FUSE 框架

9P 的设计哲学

  • 协议自包含,传输层(virtio/TCP/RDMA)可替换
  • Fid 机制比 FUSE 的 nodeid 更灵活,支持多用户并发访问
  • QID 版本字段提供轻量级缓存一致性机制
  • 9P2000.L 扩展使其能够完整表达 Linux POSIX 语义

容器场景的演进

  • Kata Containers 从 9P 迁移到 virtiofs,主要驱动是性能和 DAX 支持
  • virtiofsd 的沙箱机制(chroot)提供了足够的安全隔离
  • UID/GID 映射通过 FS_ALLOW_IDMAP + idmap mount 实现 rootless 容器

性能关键路径

  • 非 DAX:VFS -> FUSE -> virtio ring -> virtiofsd 每次 IO 约 10-100µs
  • DAX mmap:建立映射后直接访问 PCI BAR,延迟降至纳秒级
  • 9P 零拷贝:pin 用户页直接加入 virtio SG,比标准路径少一次拷贝

由 Claude Code 分析生成