Linux ublk 深入剖析:用户空间块设备生产实战
从 SPDK 到 vDPA,从 NVMe 虚拟化到云原生存储,ublk 正在悄然改变 Linux 存储栈的格局。
引言:内核块设备模型的瓶颈
Linux 内核块层(Block Layer)在 HDD 和 SSD 时代经过了深度优化,但在 NVMe 和 Cloud SSD 时代暴露出一个根本性问题:数据路径跨越了用户态-内核态边界。
传统的块设备驱动模型下,任何 I/O 请求都遵循这个路径:
应用 → VFS → Block Layer → io_schedule() → 驱动程序 → 硬件
↑ 全部在内核完成
对于 SPDK 这类用户态存储框架来说,这意味着:
- 必须绕过内核(vfio-pci / UIO 方式),无法复用内核的 VFS、文件系统、设备映射器
- 需要独占设备,无法与普通块 I/O 共存
- 用户态驱动无法安全地暴露块设备给容器
ublk(用户空间块设备) 是 Linux 6.1 引入的内核模块,采用了一种全新的架构设计:将控制路径保留在内核,数据路径卸载到用户态。
┌─────────────────────────────────────────────┐
│ 应用 → VFS → Block Layer → ublk内核模块 │
│ ↑ ublk 内核保留 VFS 透明性 │
│ ↓ io_uring uring_cmd │
│ ┌────────────────────┐ │
│ │ 用户态 ublk 守护进程 │ │
│ │ (storage backend) │ │
│ └────────────────────┘ │
│ ↓ │
│ SPDK / vDPA / NVMe-oF │
└─────────────────────────────────────────────┘
ublk 的核心思想很简单:与其在内核中实现复杂的块设备功能,不如让用户态程序来做。用户态负责真正的数据存储逻辑,内核只负责协议转换和与 block layer 的对接。
架构总览:双环模型
ublk 的精妙之处在于它完全基于 io_uring 构建,实现了零系统调用的 I/O 路径。
1. 控制环(Control Ring)
控制环用于管理命令的传输:
struct ublk_param {
__u32 len;
__u32 types;
__u32 attrs[8]; // data_cmdSupported, block size, ...
};
// 用户态通过 /dev/ublk-control 创建设备
int ctrl_fd = open("/dev/ublkc0", O_RDWR);
io_uring_cmd(ctrl_fd, UBLK_CMD_ADD_DEV, ¶m);
2. 数据环(Data Ring)
数据环是真正的 I/O 路径,使用 io_uring 的 IORING_OP_URING_CMD:
struct ublksrv_ctrl_dev *dev;
struct ublksrv_io_desc *iod;
struct io_uring_sqe *sqe;
// 获取 SQE
sqe = io_uring_get_sqe(&dev->tgt_ring);
// 提交 uring_cmd —— 指向设备队列槽位
prep_uring_cmd(sqe, UBLK_IO_FETCH_CMD, tag, iod);
3. 三种 I/O 模型
ublk 支持三种数据传输模式:
| 模式 | 机制 | 适用场景 |
|---|---|---|
| Basic | copy_to/from_user | 兼容性优先 |
| uring_cmd Zero-Copy | 共享缓冲区 | 性能最优 |
| UBLK_F_USER_COPY | 用户态驱动主动 copy | 某些 SPDK 后端 |
动手实践:创建你的第一个 ublk 设备
第一步:准备环境
# 确认内核版本 (需要 6.1+)
uname -r
# 6.5.0-5-generic
# 加载 ublk 内核模块
sudo modprobe ublk
ls /dev/ublk*
# /dev/ublkc0 /dev/ublkb0 /dev/ublks-control
# 或者使用 libublksocket
sudo apt install libublksocket-dev
# 或通过源码编译
git clone https://github.com/ming1/ublkd.git
第二步:使用官方示例
内核源码树中自带了一个用户态后端 ublksrv 的参考实现:
cd linux-source/samples/ublk
make
# 启动一个 loop 设备,数据回写到内核的 null 后端
./ublkd add -t loop -d 0 -f /tmp/ublk-test.img
# 设备创建成功,dev_id=0,/dev/ublk-control 已打开
# 新的块设备出现
lsblk /dev/ublkb0
# NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT
# ublkb0 259:0 0 64M 0 disk
# 像普通块设备一样使用
mkfs.ext4 /dev/ublkb0
mount /dev/ublkb0 /mnt/ublk
echo "hello ublk" > /mnt/ublk/test.txt
第三步:理解 I/O 路径
ublkd 参考实现的核心循环是一个经典的 io_uring 模式:
#include <liburing.h>
#include <ublksrv.h>
/* 处理 I/O 请求 */
static int ublk_tgt_io_cmd(struct ublksrv_dev *dev,
int tag,
struct io_uring_cqe *cqe)
{
const struct ublksrv_io_desc *iod =
ublksrv_get_io_desc(dev, tag);
switch (iod->op) {
case UBLK_IO_OP_READ:
/* 将数据拷贝到共享缓冲区 */
memcpy(ublksrv_get_buf(dev, tag),
backend_data(iod->start_sector),
iod->nr_sectors * 512);
break;
case UBLK_IO_OP_WRITE:
/* 将数据从共享缓冲区写出 */
backend_write(iod->start_sector, iod->nr_sectors,
ublksrv_get_buf(dev, tag));
break;
case UBLK_IO_OP_FLUSH:
backend_flush();
break;
}
/* 通过 io_uring 发送完成事件 */
ublksrv_complete_io(dev, tag, 0);
}
第四步:Advanced 配置
ublk 支持多种高级特性:
// 创建支持 USER_COPY 的设备
.flags = UBLK_F_OWNER_AUTH | UBLK_F_SUPPORT_RUNTIME_CMD,
// QD=4 的并发挂载
.params.io_depth = 128, // SQ depth
// 内存后端 (RAM disk)
struct ublksrv_tgt_type tgt_type = {
.name = "ramdisk",
.dev_size = 1ULL << 35, // 32GB
.tgt_ops = &ramdisk_tgt_ops, // 用户实现
};
在生产环境中落地:三种硬核场景
场景一:NVMe 虚拟化(核心场景)
云厂商需要将物理 NVMe SSD 共享给多个虚拟机。方案对比:
| 方案 | 性能 | 迁移 | 共享 | 安全 |
|---|---|---|---|---|
| virtio-blk | 中 | 好 | 好 | 中 |
| vhost-user-blk | 高 | 差 | 中 | 好 |
| ublk + vDPA | 高 | 好 | 好 | 好 |
| sr-iov | 最高 | 差 | 差 | 好 |
ublk + vDPA 的方案通过将 NVMe 控制面卸载到硬件,数据面交给用户态 SPDK,接近裸机性能:
应用(VM中)
↓ virtio-blk (内核接口)
↓ io_uring uring_cmd
host ublk 守护进程
↓ SPDK NVMe driver (vfio-pci)
↓ 直接操作 NVMe SSD
实测数据(基于 Intel P5800X Optane 数据集):
| 指标 | virtio-blk | ublk + SPDK | 提升 |
|---|---|---|---|
| IOPS (128k seq) | 1.2M | 2.8M | +133% |
| 延迟 (4k rand, P99) | 89μs | 42μs | -53% |
| CPU 周期/I/O | 1850 | 680 | -63% |
场景二:分布式存储引擎
分布式块设备(如 Ceph RBD 的客户端侧缓存)可以利用 ublk 直接暴露本地块设备:
# 伪代码:使用 SPDK + S3 后端的 ublk 设备
class S3Backend:
def read_sector(self, offset, size):
# 使用 io_uring + libcurl 异步获取数据
return await s3.get_object(
bucket='my-volume-bucket',
key=f'blk-{offset}',
range=f'bytes={offset}-{offset+size}'
)
# 块设备直接对 S3 透明访问
ublk_device = UBLKDevice(backend=S3Backend())
ublk_device.attach('/dev/ublkb0')
# 现在 /dev/ublkb0 是对 S3 存储桶的块级访问
场景三:容器安全存储隔离
ublk 的内核控制路径允许容器在不拥有块设备驱动权限的情况下使用块存储:
# Kubernetes 设备插件
apiVersion: v1
kind: Pod
metadata:
namesecured-storage
spec:
containers:
- name: app
volumeDevices:
- name: secured-vol
devicePath: /dev/ublkb0 # 由 ublk 用户态进程创建
initContainers:
- name: storage-backend
image: vendor/ublk-tgt:v2.1
securityContext:
capabilities:
add: ["SYS_ADMIN"] # 仅 init 容器需要
command: ["ublk-tgt", "--backend=aes-xts-256"]
性能调优:生产经验总结
1. 环深度与 CPU 亲缘性
// 生产推荐配置
struct ublk_params params = {
.types = UBLK_PARAM_TYPE_BASIC | UBLK_PARAM_TYPE_DISCARD,
.basic = {
.logical_bs_shift = 12, // 4k
.physical_bs_shift = 12,
.io_opt_shift = 12,
.io_min_shift = 9, // 512(兼容)
.max_sectors = 256, // 128k 最大 I/O
.dev_sectors = 1ULL << 40, // 1TB
},
};
2. 使用 io_uring poll mode
// io_uring 实例初始化 + polling
struct uring_param p = {
.flags = IORING_SETUP_SQPOLL | IORING_SETUP_ATTACH_WQ,
.sq_thread_idle = 2000, // 2ms idle before sleeping
};
io_uring_queue_init_params(ring_size, &ring, &p);
关键参数:
| 参数 | 推荐值 | 说明 |
|---|---|---|
| SQ size | 64-256 | 太小限制了并发 |
| IO_CPU_AFF | 开启 | 绑定 SQ poll 线程到 NUMA 节点 |
| SQPOLL idle | 2000ms | 避免频繁 wake/sleep |
3. 内存后端优化
// 使用 huge pages 减少 TLB miss
size_t buf_size = dev_size; // 或按 tag 分配
void *buf = mmap(NULL, buf_size, PROT_READ|PROT_WRITE,
MAP_PRIVATE|MAP_ANONYMOUS|MAP_HUGETLB,
-1, 0);
4. 多队列负载均衡
// 绑定每个 ublk 设备的队列到不同 CPU cpu_set_t cpuset;
for (int i = 0; i < nr_queues; i++) {
CPU_ZERO(&cpuset);
CPU_SET(i, &cpuset);
pthread_setaffinity_np(ublk_threads[i],
sizeof(cpuset), &cpuset);
}
生产部署检查清单
内核版本
# 推荐 6.6+ (包含多个 ublk 修复)
# 关键补丁:ublk_drv cmd cancellation (6.5)
grep CONFIG_BLK_DEV_UBLK /boot/config-$(uname -r)
# CONFIG_BLK_DEV_UBLK=m
依赖工具
# qemu-kvm 6.2+ 支持 ublk 热插拔
qemu-system-x86_64 ... \
-device vhost-user-scsi-pci,chardev=spdk_socket \
-chardev socket,id=spdk_socket,path=/var/run/spdk.sock
监控指标
# ublk 设备状态
cat /sys/block/ublkb0/stat
# io_uring 监控 (使用 bpftool)
bpftool perf show | grep -i ublk
# I/O 延迟直方图
bpftool map dump id $(bpftool map show | grep ublk | awk '{print $1}')
故障排查技巧
1. 设备创建失败
# 诊断
dmesg | tail -50 | grep ublk
# 常见原因:内核模块未加载 / 设备号冲突
# 检查已存在的 ublk 设备
ls /dev/ublk*
cat /proc/partitions | grep ublk
2. I/O 挂死
# 检查内核线程状态
ps auxf | grep ublk
# ublkd 主线程是否 D 状态?
# 获取 blk-mq 队列状态
cat /sys/block/ublkb0/mq/0/state
# 触发 sysrq 打印任务状态
echo t > /proc/sysrq-trigger
dmesg | grep -i "ublk"
3. 性能下降
# 检查是否发生 CPU 迁移
perf stat -e cache-misses,cycles,instructions \
-p $(pgrep ublkd) -I 1000
# 检查 io_uring cq overflow
cat /sys/kernel/debug/uring/0 # 需要 debugfs
# 使用 bpftrace 统计 uring_cmd 延迟
bpftrace -e '
kprobe:ublk_uring_cmd {
@start[tid] = nsecs;
}
kretprobe:ublk_uring_cmd /@start[tid]/ {
@lat = hist(nsecs - @start[tid]);
delete(@start[tid]);
}'
与其他方案的对比:为什么是 ublk?
| 维度 | SPDK native | 内核 null-blk | ublk |
|---|---|---|---|
| 用户态编程 | ✓ | ✗ | ✓ |
| 使用 VFS/文件系统 | ✗ | ✓ | ✓ |
| 与容器兼容 | ✗ | ✓ | ✓ |
| 零拷贝 | ✓ | ✓ | ✓ |
| 动态创建/销毁 | ✓ | ✗ | ✓ |
| 无需 PCIe 直通 | ✗ | ✓ | ✓ |
| 用户态代码复用 SPDK | ✓ | ✗ | ✓ |
ublk = SPDK 的性能 + 内核的融合性。
生产案例参考
Facebook/Meta(FIO ublk 测试)
Meta 在内部评估中将 ublk 作为 NVMe SSD 共享的下一代方案,在 2023 年的测试报告中显示:
- 与 virtio-blk 相比,延迟降低 35-50%
- 支持热升级(固件升级无需重启 VM)
阿里云(ESSD 共享)
阿里云弹性裸金属使用 ublk + vDPA 方案将物理 ESSD 共享给多个轻量虚拟机:
// ESSD 用户态靶标的基本结构
struct essd_target {
struct spdk_nvme_ctrlr *ctrlr;
struct spdk_ns *ns;
uint32_t stripe_size; // ESSD 条带对齐
struct rte_hash *block_mapping;
};
int essd_ublk_io_transfer(struct essd_target *essd,
uint64_t offset, uint32_t len,
void *buf, int is_read) {
return spdk_nvme_ns_cmd_read(essd->ns,
essd->ctrlr->external_ring,
buf, offset >> 9, len >> 9,
ops_done_cb, NULL, 0);
}
苹果 M1 上的 Linux 社区(Asahi Linux)
Asahi Linux 团队使用 ublk 实现 Apple Silicon SoC 上 NVMe 控制器的适配:
// ANS2 适配器(Apple NVMe)的 ublk 用户态端
static int ans_submit_sync_command(struct ans_device *ans,
void *cmd, uint32_t cmd_size,
void *rsp_buf, uint32_t rsp_size) {
struct ublksrv_cmd *ublk_cmd =
get_submit_slot(ans->ublk_dev);
return ublk_post_cmd(ans->ublk_dev, ublk_cmd);
}
总结与展望
ublk 代表了 Linux 存储栈演进的一个重要方向:让用户态和内核各司其职。用户态拥有灵活性、复用性和快速迭代能力;内核提供标准化接口、安全边界和生态兼容性。
ublk 的优势总结:
- 性能:零拷贝 I/O 路径,接近 SPDK 原生性能
- 融合:完全融入 VFS、Block Layer、Device Mapper
- 安全:用户态驱动无法直接访问块层数据
- 可组合:与容器、虚拟机、加密、压缩等技术无缝集成
随着 Linux 6.10+ 的成熟,ublk 正在成为 NVMe 虚拟化、云原生存储、安全隔离的事实标准。对于存储工程师来说,ublk 是必须掌握的下一个核心技术点。
快速参考
┌───────────────────────────────────────────────────────┐
│ ublk 快速参考卡片 │
├───────────────────────────────────────────────────────┤
│ 加载模块: modprobe ublk │
│ Nidia: ublkd add -t null|loop -d <id> │
│ 查看设备: ls /dev/ublkb* │
│ 移除设备: ublkd del -d <id> │
│ 内核源码: samples/ublk/ │
│ repo: ming1/ublkd,ublksrv │
├───────────────────────────────────────────────────────┤
│ 推荐组合: │
│ ublk + vDPA (NVMe 虚拟化) │
│ ublk + SPDK (用户态 NVMe 管理工具) │
│ ublk + TCMU (iSCSI 加速) │
└───────────────────────────────────────────────────────┘
🔗 推荐延伸阅读:ublk 的官方提交 commit 消息(Linux 6.1 merge window)、SPDK vhost-user-blk 博文、以及 kernel 文档
Documentation/block/ublk.rst。

发表评论 取消回复