分析基于 Linux 内核主线代码(commit 8a30aeb0d),重点覆盖
fs/fuse/virtio_fs.c、fs/fuse/fuse_i.h、net/9p/client.c、include/net/9p/9p.h、fs/9p/vfs_inode.c、net/9p/trans_virtio.c等文件。
- virtiofs 总体架构
- 核心数据结构
- FUSE 协议在 virtio ring 上的传输
- DAX 模式与零拷贝映射
- FUSE_SETUPMAPPING 与 FUSE_REMOVEMAPPING
- 9P 协议基础
- 9P 核心数据结构
- 9P virtio 传输层
- 9P VFS 集成
- virtiofs 与 9P 对比
- 挂载流程分析
- 性能特性与调优
- virtiofs 队列深度设计
- DAX window 内存管理机制
- 9P TCP 传输层与 fd 传输层
- virtiofs 安全模型与 namespace 隔离
- xattr 与 ACL 支持
- virtiofs 卸载与热拔插流程
- fuse_conn 能力位协商详解
- 9P 协议编解码机制
- DAX 与非 DAX 路径性能对比
- Kata Containers 场景下的应用
- sysfs 接口与可观测性
- 附录:关键文件路径索引
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 ...) |
+----------------------------------------------------------------------+
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 0fs/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()] 查找。
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/ */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"。
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;
};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,
};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 分配的缓冲区。
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 */
};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;
};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() 推迟执行。
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() 唤醒等待进程
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 行 */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)。
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);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 缓冲区,不回写任何数据。
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);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
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);
}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;
}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 已启用 */
#endiffuse_inode 的 DAX 字段(fuse_i.h 第 208-213 行):
#ifdef CONFIG_FUSE_DAX
struct fuse_inode_dax *dax; /* per-inode DAX 状态 */
#endifinclude/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 内存窗口中的偏移 */
};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))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)应用程序 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 结构体,无拷贝)
每个文件的 DAX 映射信息缓存在 fuse_inode.dax(fuse_inode_dax 结构)中,
维护一个区间树(extent tree)记录 foffset -> moffset 的映射关系,
避免对同一文件区域重复发送 FUSE_SETUPMAPPING 请求。
当文件大小变更或文件被删除时,内核通过 FUSE_REMOVEMAPPING 告知
virtiofsd 释放对应的 DAX 窗口区域。
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;
}所有 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
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, /* 删除文件 */
};Fid(File Identifier)是 9P 协议的核心抽象,类似 Unix 的文件描述符, 但作用域是整个会话而非单个进程:
- 每个 fid 是一个 32 位整数,在同一 9P 会话内唯一
TATTACH获取根目录 fidTWALK在目录层次中遍历,可以克隆或移动 fidTOPEN/TLOPEN打开 fid 对应的文件TCLUNK释放 fid(类似 close)TREMOVE删除文件并同时释放 fid
Fid 状态机:
TATTACH
|
v
[已绑定到路径,未打开]
|
+-- TWALK (克隆并遍历)--> [新 fid,绑定到子路径]
|
+-- TOPEN/TLOPEN ---------> [已打开,可 READ/WRITE]
| |
| +-- TCLUNK --> [释放]
|
+-- TCLUNK -------------------> [释放]
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。
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 链表 */
};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 文件,客户端不应缓存。
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, /* 客户端错误 */
};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;
};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);
}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、纳秒精度时间戳等。
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 的多队列设计)。
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);
}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 完成。
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 并唤醒等待该请求的任务。
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;
}9P 传输层通过 p9_trans_module 结构注册(include/net/9p/transport.h),
virtio 传输在模块 init 时调用 v9fs_register_trans() 注册,
TCP/RDMA 传输同理。客户端通过 mount 选项 trans=virtio 选择传输层。
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;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 */
}
}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)
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 的最小缓存配置。
9P 缓存一致性依赖 QID 的 version 字段:
- 客户端每次访问 inode 前对比缓存的
qid.version与服务端返回的值 - 若版本不同(
V9FS_INO_INVALID_ATTR标志),使缓存失效并重新获取 version=0的文件(synthetic)永远不缓存
mount 选项 V9FS_IGNORE_QV(v9fs.h 第 44 行)可以忽略版本检查,
适用于已知一致性由上层保证的场景。
| 特性 | 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 / 其他 |
| 性能特性 | 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) |
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 直接实现服务端
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 行)在设备枚举时调用,完成:
virtio_fs_read_tag():读取设备 tagvirtio_fs_setup_vqs():初始化 virtqueuevirtio_fs_map_queues():建立 CPU-队列映射virtio_fs_setup_dax():初始化 DAX 内存窗口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 */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
virtio_fs_map_queues()(第 859-895 行)的两阶段映射策略:
-
优先使用传输层 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;
-
回退到均匀分配:
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;
-
最终回退:所有 CPU 使用第一个请求队列。
fuse_init_out(include/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];
};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 描述符。
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)。
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) 实现。
每个 virtio_fs 实例在 /sys/fs/virtio_fs/<index>/ 下暴露:
tag:文件系统标签mqs/<vq_index>/name:队列名称mqs/<vq_index>/cpu_list:映射到该队列的 CPU 列表
+-------------------+
| 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 始终可以被立即处理。
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);
}
}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 所有飞行请求,确保不接受新请求的同时完成已在途的请求。
当 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 顺序。
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;
};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 使用时的引用计数 */
};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)对应。
当空闲范围低于总范围数的 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。
每个 inode 的 fuse_inode_dax 维护一棵 interval_tree(dax.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,避免重复申请。
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;
};net/9p/trans_fd.c 第 107-111 行:
struct p9_trans_fd {
struct file *rd; /* 读文件(可与 wr 相同) */
struct file *wr; /* 写文件 */
struct p9_conn conn;
};TCP 传输通过 socket 创建 rd 和 wr 指向同一个 struct file。
fd 传输(trans=fd)允许 rfd 和 wfd 分别指定(用于管道场景)。
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)进行实际的数据传输。
+------------------+----------+-----------+----------+----------+
| 特性 | virtio | TCP/fd | RDMA | XEN |
+------------------+----------+-----------+----------+----------+
| 零拷贝 | 是(pin页) | 否 | 是(RDMA) | 否 |
| 多路复用 | tag IDR | tag IDR | tag IDR | tag IDR |
| pooled_rbuffers | 否 | 否 | 是 | 否 |
| 最大 msize | 512KB | 无限制 | 无限制 | 受限 |
| 内核态 | 是 | 是 | 是 | 是 |
+------------------+----------+-----------+----------+----------+
pooled_rbuffers(p9_trans_module 字段)为真时,p9_client_rpc 使用
固定大小 msize 作为响应缓冲容量,以匹配 RDMA 的池化缓冲区设计。
virtiofs 的文件系统类型标志包含 FS_ALLOW_IDMAP(virtio_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 模式的基础。
每个 fuse_in_header 携带调用者的 uid、gid、pid(第 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 容器场景)
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 能够识别请求来源进程(用于审计、调试等)。
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)。
fuse_conn.init_security(fuse_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。
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:直接返回值到提供的缓冲区
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 也被限制在此范围内。
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,
以避免额外的协议往返开销。
9P 的 POSIX ACL 由 fs/9p/acl.c 实现,通过 xattr 接口读写 system.posix_acl_access
和 system.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;
#endifvirtiofs 使用 FUSE 的 fc->posix_acl 标志(fuse_i.h:862)支持 POSIX ACL,
通过标准的 posix_acl_xattr 接口与 virtiofsd 交互。
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)
当 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 才能完全清理。
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 设备驱动的通用问题,
目前尚无内核级超时处理。
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) 保留
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 用)
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。
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; /* 是否零拷贝模式 */
};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=条件字符串数组 */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;
}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);
}
}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 在收到后会被匹配 */
}非 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 后无额外拷贝)
非 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)
| 路径 | 主要延迟来源 | 典型延迟 |
|---|---|---|
| 非 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 模式的初始化开销较大,但对大文件的随机访问工作负载(如数据库页读取), 摊销后的每次访问延迟极低。
非 DAX 模式:
guest 页缓存 = 文件数据副本(guest 内存)
读写都经过 guest page cache
内存占用 = 缓存文件大小(最大为 guest RAM 的可用部分)
DAX 模式:
无 guest 页缓存(数据直接在 DAX 窗口)
DAX 窗口大小 = host 预分配的 PCI BAR 大小(固定)
内存占用 = 活跃映射的 DAX ranges(每个 2MB)
内存压力下:触发回收,通过 FUSE_REMOVEMAPPING 释放
+-------------------------------------------------------------+
| 宿主机(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 客户端 |
| --> 挂载为容器根目录 / |
| |
| 应用进程(容器负载) |
+-------------------------------------------------------------+
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 内核。
Kata Containers 使用 virtiofs DAX 的主要场景:
-
共享镜像层:多个 Kata VM 共享同一容器镜像层(只读), 通过 DAX 直接映射同一物理内存,不需要在每个 VM 中分别缓存。
-
大文件访问:数据库容器、机器学习推理容器的大文件 通过 DAX mmap 实现接近原生的访问性能。
-
内存气球配合:当 VM 内存压力大时,DAX 区域可通过回收机制 释放 DAX window 映射,将宿主机内存还给其他用途。
+--------------------+ +--------------------+
| 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 各自独立)。
早期 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 仅在特殊场景保留。
Kata Containers 的核心价值是 VM 级别的隔离。virtiofs 的安全考量:
-
virtiofsd 沙箱:通过
--sandbox=chroot限制 virtiofsd 只能访问 指定的 shared dir,即使 guest 发出任意文件路径请求也无法逃逸。 -
SELinux/AppArmor 标签:virtiofsd 可以使用安全模块保护 自身不被攻击,同时正确传递文件的 security xattr。
-
uid/gid 隔离:通过
idmap mount,guest 内的 root(uid=0) 映射到 host 上的非特权 uid,防止 guest root 直接操作 host 文件。 -
virtio 协议隔离:guest 通过 virtio 消息与 virtiofsd 通信, 攻击面限于 FUSE 协议层,不涉及 host kernel 的直接访问。
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 到输出 */
}
}
}普通 FUSE 挂载会在 /sys/fs/fuse/connections/<minor>/ 下创建控制节点,
但 virtiofs 设置了 ctx->no_control = true(第 1536 行),
不在 fusectl 文件系统中创建条目,避免用户通过 fusectl 干扰 virtiofs 连接。
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 追踪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通过 /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 分析生成