Linux 存储与文件系统深度剖析(八):直接 IO 与异步 IO 实现

一、IO 模型对比

在深入探讨 Direct IO 与异步 IO 实现之前,有必要先厘清 Linux 下各种 IO 模型的本质差异。POSIX 标准定义了同步与异步两大类 IO,而 Linux 在此之上提供了更丰富的变体。

1.1 四种经典 IO 模型

同步阻塞 IO(Blocking IO)是最直观的模型。read() 系统调用发出后,进程进入睡眠,内核等待数据就绪并完成内存拷贝,随后唤醒进程。整个过程中用户进程挂起,无法做其他事情。这是绝大多数传统应用的默认行为。

同步非阻塞 IO(Non-blocking IO)通过设置 O_NONBLOCK 标志,让 read() 在数据未就绪时立即返回 EAGAIN,而不是阻塞。应用程序需要循环轮询,CPU 利用率高但响应延迟低。这种模型适合极少数对延迟极度敏感的场景,但大多数情况下会造成 CPU 空转。

IO 多路复用(IO Multiplexing)通过 select/poll/epoll 等机制,让单线程同时监听多个文件描述符。epoll 采用事件驱动模型,内核通过红黑树管理监听集合,通过双向链表维护就绪队列,时间复杂度为 O(1)。当 fd 就绪时,epoll_wait 返回,应用再调用 read()/write(),此时数据已就绪,IO 操作本身不再阻塞。注意:IO 多路复用仍属于同步 IO,因为真正的数据拷贝(内核空间到用户空间)依然由调用 read() 的进程同步完成。

异步 IO(Asynchronous IO)是真正的异步模型。应用提交 IO 请求后立即返回,内核在后台完成数据读写和内存拷贝,完成后通过信号、回调或完成队列通知应用。Linux AIO(io_submit/io_getevents)和 io_uring 都属于此类,但实现机制和能力有本质区别。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
┌─────────────────────────────────────────────────────────────────┐
│ IO 模型对比图 │
│ │
│ 同步阻塞 同步非阻塞 IO多路复用 异步IO │
│ │
│ 进程 进程 进程 进程 │
│ │ │ │ │ │
│ │ read() │ read() │ epoll_wait │ io_submit() │
│ │ │ EAGAIN ← │ │ ←── 立即返回 │
│ │ 阻塞等待 │ 轮询... │ 阻塞等待 │ │
│ │ │ read() │ 数据就绪↓ │ 内核处理中 │
│ │ 数据就绪 │ 数据就绪 │ read() │ │
│ │ ←── 返回 │ ←── 返回 │ ←── 返回 │ 完成通知 ← │
└─────────────────────────────────────────────────────────────────┘

两个关键维度区分这些模型:等待数据就绪的过程是否阻塞;数据拷贝(内核→用户)的过程是否阻塞。只有真正的异步 IO 在两个维度上都不阻塞调用者。

1.2 Linux 信号驱动 IO

Linux 还支持信号驱动 IO(SIGIO/O_ASYNC),通过 fcntl(fd, F_SETOWN, pid) 注册信号接收者。当 fd 就绪时内核发送 SIGIO 信号。但这种模式在实践中很少使用,因为信号处理函数受到很多限制,且无法区分多个 fd 的就绪事件。


二、直接 IO(O_DIRECT)实现

2.1 为什么数据库需要 Direct IO

Linux 的页缓存(Page Cache)是提升 IO 性能的核心机制:读操作的数据被缓存在内存中供后续复用,写操作先写入内存中的脏页(dirty page),由内核的 pdflush/writeback 线程异步刷盘。对于大多数应用,这种双重缓冲能显著提升吞吐量。

然而,对于数据库系统(PostgreSQL、MySQL InnoDB、Oracle 等),页缓存是一个障碍:

  1. 双重缓冲浪费内存:数据库有自己的 Buffer Pool,页缓存与之重叠,同样的数据在内存中存两份。
  2. 缓存污染:大规模全表扫描会把热数据从页缓存中驱逐,破坏缓存效果。
  3. fsync 语义复杂:数据库需要精确控制数据落盘时机(WAL 机制),通过页缓存的异步写入会引入不确定性。
  4. O_DIRECT 绕过页缓存,数据直接在用户缓冲区与磁盘之间传输(通过 DMA),数据库可以自主管理缓存,实现更精确的持久化控制。

2.2 对齐要求

Direct IO 有严格的内存和偏移对齐要求,违反会得到 EINVAL

  • 内存缓冲区地址:必须按扇区大小(通常 512 字节)或文件系统块大小(通常 4096 字节)对齐
  • 文件偏移量:同样须对齐
  • 传输长度:须为扇区/块大小的整数倍

内核在 do_blockdev_direct_IO 入口检查这些约束:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/* fs/direct-io.c */
static inline int dio_bio_reap(struct dio *dio, struct dio_submit *sdio)
{
int ret = 0;

if (sdio->reap_counter++ >= 64) {
while (dio->bio_list) {
unsigned long flags;
struct bio *bio;
int ret2;

spin_lock_irqsave(&dio->bio_lock, flags);
bio = dio->bio_list;
dio->bio_list = bio->bi_private;
spin_unlock_irqrestore(&dio->bio_lock, flags);
ret2 = blkdev_issue_flush(bio->bi_bdev);
if (ret == 0)
ret = ret2;
bio_put(bio);
}
sdio->reap_counter = 0;
}
return ret;
}

2.3 struct dio 结构体与核心函数

struct dio 是 Direct IO 操作的核心控制块,定义在 fs/direct-io.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/* fs/direct-io.c */
struct dio {
int flags; /* flags from file open */
blk_opf_t opf; /* request operation type and flags */
struct gendisk *bio_disk;
struct inode *inode;
loff_t i_size; /* i_size when submitted */
dio_iodone_t *end_io; /* IO completion function */

void *private; /* copy from map_bh.b_private */

/* BIO completion state */
spinlock_t bio_lock; /* protects BIO fields below */
int page_errors; /* errno from get_user_pages() */
int is_async; /* non-zero if this is async DIO */
bool defer_completion; /* defer AIO completion to workqueue? */
bool should_dirty; /* if pages should be dirtied */
int io_error; /* IO error in completion path */
unsigned long refcount; /* direct_io_worker() and bios */
struct bio *bio_list; /* singly linked via bi_private */
struct task_struct *waiter; /* waiting task (NULL if none) */

/* AIO related stuff */
struct kiocb *iocb; /* kiocb */
ssize_t result; /* IO result */

/*
* pages[] (and any fields after it) are not zeroed out at
* allocation time. Don't add new fields after pages[] unless
* you handle that.
*/
union {
struct page *pages[DIO_PAGES]; /* page buffer */
struct work_struct complete_work; /* deferred AIO completion */
};
} ____cacheline_aligned_in_smp;

do_blockdev_direct_IO 是发起 Direct IO 的核心入口,它协调用户空间缓冲区的 pin(通过 get_user_pages)、构建 bio 链并提交到块层:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
ssize_t __blockdev_direct_IO(struct kiocb *iocb, struct inode *inode,
struct block_device *bdev,
struct iov_iter *iter,
get_block_t get_block,
dio_iodone_t end_io,
int flags)
{
unsigned i_blkbits = READ_ONCE(inode->i_blkbits);
unsigned blkbits = i_blkbits;
unsigned blocksize_mask = (1 << blkbits) - 1;
ssize_t retval = -EINVAL;
const size_t count = iov_iter_count(iter);
loff_t offset = iocb->ki_pos;
const loff_t end = offset + count;
struct dio *dio;
struct dio_submit sdio = { 0, };
struct buffer_head map_bh = { 0, };

/* ... 对齐检查 ... */
if ((offset & blocksize_mask) || (count & blocksize_mask)) {
if (bdev) {
blkbits = blksize_bits(bdev_logical_block_size(bdev));
blocksize_mask = (1 << blkbits) - 1;
if ((offset & blocksize_mask) || (count & blocksize_mask))
goto out;
} else {
goto out;
}
}

dio = kmem_cache_alloc(dio_cache, GFP_KERNEL);
/* ... 初始化 dio,提交 bio,等待完成 ... */
}

2.4 dio_bio_submit 与 DMA 传输

dio_bio_submit 将构建好的 bio 提交到通用块层(Generic Block Layer),最终由设备驱动程序通过 DMA 完成数据传输:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
static void dio_bio_submit(struct dio *dio, struct dio_submit *sdio)
{
struct bio *bio = sdio->bio;
unsigned long flags;

bio->bi_private = dio;

spin_lock_irqsave(&dio->bio_lock, flags);
dio->refcount++;
spin_unlock_irqrestore(&dio->bio_lock, flags);

if (dio->is_async && dio->opf == REQ_OP_READ && dio->should_dirty)
bio_set_pages_dirty(bio);

dio->bio_disk = bio->bi_bdev->bd_disk;

if (sdio->submit_io) {
sdio->submit_io(bio, dio->inode, sdio->logical_offset_in_bio);
sdio->bio = NULL;
sdio->boundary = 0;
sdio->logical_offset_in_bio = 0;
} else {
submit_bio(bio);
}

sdio->bio = NULL;
sdio->boundary = 0;
sdio->logical_offset_in_bio = 0;
}

DMA(Direct Memory Access)传输的原理:控制器从 bio 中取出物理内存页地址(bio_vec 数组),通过总线直接在磁盘控制器与主存之间搬运数据,CPU 全程不参与数据拷贝,完成后通过中断通知内核。这是 Direct IO 高效的根本原因——减少了一次内核缓冲区到用户缓冲区的 memcpy


三、Linux 内核 AIO(fs/aio.c

3.1 struct kiocb 字段解析

struct kiocb(Kernel IO Control Block)是内核异步 IO 的基本请求描述符,定义在 include/linux/fs.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct kiocb {
struct file *ki_filp; /* 目标文件 */
loff_t ki_pos; /* 当前 IO 偏移量 */
void (*ki_complete)(struct kiocb *iocb, long ret); /* 完成回调 */
void *private;
int ki_flags; /* IOCB_* 标志位 */
u16 ki_ioprio; /* 请求优先级 (IOPRIO_PRIO_VALUE) */
union {
/* AIO 完成事件关联 */
struct wait_page_queue *ki_waitq;
/* 用于 poll 路径 */
__poll_t (*ki_poll)(struct file *, struct poll_table_struct *);
};
};

ki_flags 中的关键标志:

  • IOCB_EVENTFD:完成时通过 eventfd 通知
  • IOCB_DIRECT:使用 Direct IO 路径
  • IOCB_NOWAIT:如果操作需要等待则立即返回 EAGAIN
  • IOCB_NOIO:不允许发起新 IO(用于预读路径)

3.2 io_submit() 系统调用实现

Linux AIO 的提交入口是 io_submit() 系统调用,对应内核函数 __io_submit_one

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
/* fs/aio.c */
static int __io_submit_one(struct kioctx *ctx, const struct iocb *iocb,
struct iocb __user *user_iocb, struct aio_kiocb *req,
bool compat)
{
req->ki_filp = fget(iocb->aio_fildes);
if (unlikely(!req->ki_filp))
return -EBADF;

if (iocb->aio_flags & IOCB_FLAG_RESFD) {
/*
* eventfd 通知模式:完成时写入 eventfd
*/
req->ki_eventfd = eventfd_ctx_fdget(iocb->aio_resfd);
if (IS_ERR(req->ki_eventfd)) {
int ret = PTR_ERR(req->ki_eventfd);
req->ki_eventfd = NULL;
return ret;
}
}

ret = put_user(KIOCB_KEY, &user_iocb->aio_key);
if (unlikely(ret))
return ret;

req->ki_res.obj = (u64)(unsigned long)user_iocb;
req->ki_res.data = iocb->aio_data;
req->ki_res.res = 0;
req->ki_res.res2 = 0;

switch (iocb->aio_lio_opcode) {
case IOCB_CMD_PREAD:
return aio_read(&req->rw, iocb, false, compat);
case IOCB_CMD_PWRITE:
return aio_write(&req->rw, iocb, false, compat);
case IOCB_CMD_PREADV:
return aio_read(&req->rw, iocb, true, compat);
case IOCB_CMD_PWRITEV:
return aio_write(&req->rw, iocb, true, compat);
case IOCB_CMD_FSYNC:
return aio_fsync(&req->fsync, iocb, false);
case IOCB_CMD_FDSYNC:
return aio_fsync(&req->fsync, iocb, true);
case IOCB_CMD_POLL:
return aio_poll(req, iocb);
default:
pr_debug("EINVAL: no operation provided\n");
return -EINVAL;
}
}

io_submit() 每次调用可以批量提交多个 iocb 请求,内部对每个请求调用 __io_submit_one,分配 aio_kiocb,填充后提交到相应的文件操作实现。

3.3 io_getevents() 轮询机制

提交后,应用通过 io_getevents() 收割完成事件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/* fs/aio.c */
static long read_events(struct kioctx *ctx, long min_nr, long nr,
struct io_event __user *event,
ktime_t until)
{
long ret = 0;

/*
* Note that aio_read_events() is being called as the conditional
* in a wait_event() loop. When the conditional is satisfied,
* the loop exits; otherwise we wait until an event is reaped.
*/
if (!wait_event_interruptible_hrtimeout(ctx->wait,
aio_read_events(ctx, min_nr, nr, event, &ret), until))
return ret;

if (!ret && !time_after(jiffies, ctx->mmap_base))
ret = -EINTR;
return ret;
}

完成事件写入 io_event 结构:

1
2
3
4
5
6
struct io_event {
__u64 data; /* 用户设置的 aio_data,用于关联请求 */
__u64 obj; /* 指向原始 iocb 的指针 */
__s64 res; /* 操作结果(>=0 成功,<0 errno)*/
__s64 res2; /* 二级结果(如 preadv2 flags) */
};

3.4 Linux AIO 的局限性

Linux 内核 AIO 存在若干根本性限制,这也是 io_uring 诞生的主要动因:

  1. 只支持 O_DIRECT:对 Buffered IO 的 aio_read/aio_write 实际上并不是真正异步的——当页缓存缺页时会同步阻塞在工作队列线程里,只是把阻塞转移到了内核线程,并未消除。
  2. 每次 syscall 开销大io_submit 每次需要从用户空间拷贝 iocb 结构(每个 64 字节),无法利用共享内存避免拷贝。
  3. io_getevents 轮询开销:需要进入内核态才能获取完成事件。
  4. 不支持网络 IO:Linux AIO 仅适用于文件描述符,不支持 socket。
  5. 不支持 fsync(早期版本):无法异步地刷盘。

四、io_uring:现代异步 IO 框架

io_uring 由 Jens Axboe 在 2019 年引入(Linux 5.1),彻底解决了 Linux AIO 的局限性。其核心思路是通过共享内存环形队列实现用户态与内核态的零拷贝通信。

4.1 struct io_ring_ctx 核心上下文

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
/* io_uring/io_uring.c(简化) */
struct io_ring_ctx {
/* 不可变字段(初始化后不再修改) */
struct {
unsigned int flags;
unsigned int compat : 1;
unsigned int drain_next : 1;
unsigned int restricted : 1;
unsigned int off_timeout_used : 1;
unsigned int drain_active : 1;
} ____cacheline_aligned_in_smp;

/* 提交队列(SQ)相关 */
struct {
struct mutex uring_lock;
u32 *sq_array; /* SQ 索引数组(共享内存) */
struct io_uring_sqe *sq_sqes; /* SQE 环(共享内存) */
unsigned cached_sq_head;
unsigned sq_entries;
struct io_wq_work_list defer_list;
} ____cacheline_aligned_in_smp;

/* 完成队列(CQ)相关 */
struct {
unsigned cached_cq_tail;
unsigned cq_entries;
struct io_ev_fd __rcu *io_ev_fd; /* eventfd 通知 */
struct wait_queue_head cq_wait;
unsigned cq_extra;
} ____cacheline_aligned_in_smp;

struct io_rings *rings; /* 指向共享内存中的环头 */

/* 注册资源 */
struct io_rsrc_data *file_data; /* 注册文件表 */
struct io_rsrc_data *buf_data; /* 注册缓冲区表 */

/* SQPOLL 内核线程 */
struct io_sq_data *sq_data;

/* 工作队列 */
struct io_wq *io_wq;

/* 其他字段... */
};

io_ring_ctx 按缓存行对齐拆分,SQ 和 CQ 各占独立缓存行,避免多核并发时的伪共享(false sharing)。

4.2 SQE 与 CQE 共享内存设计

io_uring 的精髓在于:内核与用户空间共享同一块物理内存,通过生产者-消费者模型通信,无需系统调用即可提交和收割大量 IO。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
用户空间                          内核空间
┌──────────────────────────────────────────────┐
│ 共享内存(mmap) │
│ │
│ SQ Ring CQ Ring │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ head (R:内核)│ │ head (R:用户)│ │
│ │ tail (W:用户)│ │ tail (W:内核)│ │
│ │ sq_array[] │ │ cqes[] │ │
│ └─────────────┘ └─────────────┘ │
│ │
│ SQE Array(独立 mmap) │
│ ┌────┬────┬────┬────┐ │
│ │SQE0│SQE1│SQE2│SQE3│ ← 用户填写 │
│ └────┴────┴────┴────┘ │
└──────────────────────────────────────────────┘

SQE(Submission Queue Entry)结构,每个 64 字节:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
struct io_uring_sqe {
__u8 opcode; /* IORING_OP_READ/WRITE/... */
__u8 flags; /* IOSQE_* 标志 */
__u16 ioprio;
__s32 fd; /* 目标文件描述符(或注册文件索引) */
union {
__u64 off; /* 偏移量(pread/pwrite 用) */
__u64 addr2;
};
union {
__u64 addr; /* 缓冲区地址(或注册缓冲区索引) */
__u64 splice_off_in;
};
__u32 len; /* 缓冲区长度 */
union {
__kernel_rwf_t rw_flags;
__u32 fsync_flags;
__u16 poll_events;
__u32 poll32_events;
__u32 sync_range_flags;
__u32 msg_flags;
__u32 timeout_flags;
__u32 accept_flags;
__u32 cancel_flags;
__u32 open_flags;
__u32 statx_flags;
__u32 fadvise_advice;
__u32 splice_flags;
__u32 rename_flags;
__u32 unlink_flags;
__u32 hardlink_flags;
__u32 xattr_flags;
__u32 msg_ring_flags;
__u32 uring_cmd_flags;
};
__u64 user_data; /* 用户自定义标识,原样返回 CQE */
union {
__u16 buf_index; /* 注册缓冲区索引 */
__u16 buf_group;
} __attribute__((packed));
__u16 personality;
union {
__s32 splice_fd_in;
__u32 file_index;
__u32 optlen;
struct {
__u16 addr_len;
__u16 __pad3[1];
};
};
/* ... */
};

CQE(Completion Queue Entry)结构,每个 16 字节:

1
2
3
4
5
struct io_uring_cqe {
__u64 user_data; /* 对应 SQE 的 user_data */
__s32 res; /* 操作结果 */
__u32 flags; /* IORING_CQE_F_* */
};

4.3 io_uring_setup() 与初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/* io_uring/io_uring.c */
static struct io_ring_ctx *io_ring_ctx_alloc(struct io_uring_params *p)
{
struct io_ring_ctx *ctx;
int hash_bits;

ctx = kzalloc(sizeof(*ctx), GFP_KERNEL);
if (!ctx)
return NULL;

xa_init(&ctx->io_bl_xa);

hash_bits = ilog2(p->cq_entries);
hash_bits = clamp(hash_bits, 1, 8);
ctx->cancel_table.hbs = hash_bits;
ctx->cancel_table.hash = kcalloc(1U << hash_bits,
sizeof(struct hlist_head), GFP_KERNEL);
if (!ctx->cancel_table.hash)
goto err;

ctx->cancel_table_locked.hbs = hash_bits;
ctx->cancel_table_locked.hash = kcalloc(1U << hash_bits,
sizeof(struct hlist_head), GFP_KERNEL);
if (!ctx->cancel_table_locked.hash)
goto err;

if (percpu_ref_init(&ctx->refs, io_ring_ctx_ref_free,
0, GFP_KERNEL))
goto err;

ctx->flags = p->flags;
init_waitqueue_head(&ctx->sqo_sq_wait);
INIT_LIST_HEAD(&ctx->sqd_list);
init_waitqueue_head(&ctx->poll_wait);
INIT_LIST_HEAD(&ctx->cq_overflow_list);
/* ... 更多初始化 ... */
return ctx;
err:
io_ring_ctx_free(ctx);
return NULL;
}

4.4 IORING_SETUP_SQPOLL 内核轮询线程模式

开启 IORING_SETUP_SQPOLL 后,内核创建一个内核线程(io_sq_thread)持续轮询 SQ 环:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/* io_uring/sqpoll.c(简化) */
static int io_sq_thread(void *data)
{
struct io_sq_data *sqd = data;
struct io_ring_ctx *ctx;
unsigned long timeout = 0;
char buf[TASK_COMM_LEN];
DEFINE_WAIT(wait);

snprintf(buf, sizeof(buf), "iou-sqp-%d", sqd->task_pid);
set_task_comm(current, buf);

while (!test_bit(IO_SQ_THREAD_SHOULD_STOP, &sqd->state)) {
int ret;
bool cap_entries, sqt_spin = false;

/* 轮询所有关联的 ring ctx */
list_for_each_entry(ctx, &sqd->ctx_list, sqd_list) {
if (io_sq_have_work(ctx))
sqt_spin |= io_sq_thread_handle_c(ctx, &cap_entries);
}

if (sqt_spin || !time_after(jiffies, timeout)) {
io_run_task_work();
cond_resched();
if (sqt_spin)
timeout = jiffies + sqd->sq_thread_idle;
continue;
}

/* 超过 idle 时间,进入睡眠等待新任务 */
prepare_to_wait(&sqd->wait, &wait, TASK_INTERRUPTIBLE);
/* ... */
}
return 0;
}

SQPOLL 模式下,应用提交 SQE 只需写共享内存,内核线程自动发现并处理,完全零系统调用。这对高 IOPS 场景(NVMe SSD,百万级 IOPS)效果显著,系统调用开销本身可能成为瓶颈。

4.5 固定缓冲区与固定文件

注册固定资源可以避免每次 IO 时重复的 get_user_pages(锁定内存页)和 fget(引用计数)开销:

1
2
3
4
5
6
7
8
9
10
11
/* 注册固定缓冲区 */
struct iovec iov[2];
iov[0].iov_base = buf0;
iov[0].iov_len = BUF_SIZE;
iov[1].iov_base = buf1;
iov[1].iov_len = BUF_SIZE;
io_uring_register(ring_fd, IORING_REGISTER_BUFFERS, iov, 2);

/* 使用固定缓冲区时,SQE 中用 buf_index 代替地址 */
sqe->opcode = IORING_OP_READ_FIXED;
sqe->buf_index = 0; /* 使用第 0 个注册缓冲区 */

注册文件(IORING_REGISTER_FILES)类似,将 fd 数组预先注册,SQE 中 flags |= IOSQE_FIXED_FILEfd 字段为数组索引而非真实 fd,绕过每次 fget/fput 的引用计数操作。

4.6 链式请求(IOSQE_IO_LINK)

io_uring 支持将多个 SQE 链接为有序序列,前一个完成后才提交下一个:

1
2
3
4
5
6
7
8
9
10
11
12
/* 读取文件 → 处理 → 写入另一个文件,串行执行 */
sqe = io_uring_get_sqe(&ring);
io_uring_prep_read(sqe, src_fd, buf, len, 0);
sqe->flags |= IOSQE_IO_LINK; /* 链接到下一个 SQE */
sqe->user_data = 1;

sqe = io_uring_get_sqe(&ring);
io_uring_prep_write(sqe, dst_fd, buf, len, 0);
sqe->user_data = 2; /* 最后一个不设 IOSQE_IO_LINK */

io_uring_submit(&ring);
/* 内核保证先执行 read,read 完成后才执行 write */

链式请求中任意一步失败,后续步骤会以 -ECANCELED 取消,类似事务语义。

4.7 用户态使用示例(liburing)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include <liburing.h>
#include <fcntl.h>
#include <string.h>

#define QUEUE_DEPTH 64
#define BLOCK_SZ 4096

int main(void)
{
struct io_uring ring;
struct io_uring_sqe *sqe;
struct io_uring_cqe *cqe;
char buf[BLOCK_SZ];
int fd, ret;

/* 初始化 io_uring,深度为 64 */
io_uring_queue_init(QUEUE_DEPTH, &ring, 0);

fd = open("/tmp/testfile", O_RDONLY | O_DIRECT);

/* 获取 SQE 并填写读请求 */
sqe = io_uring_get_sqe(&ring);
io_uring_prep_read(sqe, fd, buf, BLOCK_SZ, 0);
sqe->user_data = 42; /* 自定义标识 */

/* 提交(写 SQ tail,触发内核处理) */
io_uring_submit(&ring);

/* 等待完成事件 */
ret = io_uring_wait_cqe(&ring, &cqe);
if (ret == 0 && cqe->res > 0) {
printf("读取 %d 字节,user_data=%llu\n",
cqe->res, (unsigned long long)cqe->user_data);
}

/* 标记 CQE 已消费(推进 CQ head) */
io_uring_cqe_seen(&ring, cqe);

io_uring_queue_exit(&ring);
close(fd);
return 0;
}

五、性能对比与调优

5.1 Direct IO vs Buffered IO 适用场景

维度 Buffered IO Direct IO
缓存效果 热数据命中后接近内存速度 无缓存,每次都落盘
内存占用 页缓存占用物理内存 仅用户缓冲区
适用场景 随机小读写、反复访问相同数据 数据库 Buffer Pool、流式大文件读写
对齐要求 严格(512B 或 4096B)
写安全性 fsync 保证持久化 数据直达磁盘(仍需考虑磁盘缓存)
顺序读写吞吐 接近(预读机制补偿) 略低(无预读,需自行管理)

推荐使用 Direct IO 的典型场景

  • 数据库引擎(PostgreSQL 的 effective_io_concurrency,MySQL InnoDB 的 innodb_flush_method=O_DIRECT
  • 视频转码、备份等大文件单次流式读写,防止污染页缓存
  • 实时数据采集,对时延有精确要求

5.2 AIO vs io_uring 性能对比

Linux AIO 和 io_uring 在不同场景下的典型性能差异(参考 Jens Axboe 的 benchmark 数据):

指标 Linux AIO io_uring(默认) io_uring(SQPOLL)
最大 IOPS(4K 随机读,NVMe) ~600K ~800K ~1200K+
每 IO 系统调用次数 2(submit+getevents) 1(submit,collect 可批量) 0(SQPOLL 模式)
支持 Buffered IO 伪异步 真异步 真异步
支持网络 IO 是(recv/send/accept)
支持 fsync 否(早期)
固定缓冲区

5.3 fio 测试命令示例

测试 Buffered IO 顺序写吞吐量

1
2
3
4
5
6
fio --name=buffered-seq-write \
--rw=write --bs=1M --size=4G \
--numjobs=4 --iodepth=1 \
--ioengine=sync \
--filename=/data/testfile \
--group_reporting

测试 Direct IO 随机读 IOPS

1
2
3
4
5
6
fio --name=direct-rand-read \
--rw=randread --bs=4k --size=4G \
--numjobs=4 --iodepth=32 \
--ioengine=libaio --direct=1 \
--filename=/data/testfile \
--group_reporting

测试 io_uring 随机读写(混合 70/30)

1
2
3
4
5
6
7
8
9
10
11
fio --name=io-uring-mixed \
--rw=randrw --rwmixread=70 \
--bs=4k --size=4G \
--numjobs=4 --iodepth=128 \
--ioengine=io_uring \
--hipri=1 \ # 使用 polling 模式
--sqthread_poll=1 \ # 开启 SQPOLL
--registerfiles=1 \ # 注册文件
--fixedbufs=1 \ # 注册缓冲区
--filename=/dev/nvme0n1 \ # 直接测试裸设备
--group_reporting

解读关键指标

  • IOPS:每秒完成的 IO 操作次数,评估随机 IO 能力
  • BW:带宽(MB/s),评估顺序 IO 吞吐
  • lat (usec):延迟,avg 是平均值,99.00th 是 P99 尾延迟——对数据库场景尤为重要
  • clat:完成延迟(Completion Latency),从 IO 提交到完成的时间

5.4 调优建议

内核参数

1
2
3
4
5
6
7
8
# 提升 AIO 最大并发请求数(默认 65536)
echo 1048576 > /proc/sys/fs/aio-max-nr

# 调整调度器为 none(NVMe SSD 不需要 IO 调度器)
echo none > /sys/block/nvme0n1/queue/scheduler

# 增大设备队列深度
echo 256 > /sys/block/nvme0n1/queue/nr_requests

io_uring SQPOLL 注意事项:SQPOLL 内核线程会绑定在特定 CPU 上持续运行,适合专用 IO 服务器。混合负载场景下应通过 IORING_SETUP_SQ_AFF 绑定隔离的 CPU core,避免争抢业务线程的 CPU。


总结

本文系统梳理了 Linux IO 模型的演进脉络:从传统的同步阻塞 IO,到 Linux AIO 尝试异步化(但受制于 O_DIRECT 限制),再到 io_uring 通过共享内存环形队列实现真正的高性能零开销异步 IO 框架。

Direct IO 解决了数据库等场景的双重缓冲问题,而 io_uring 则从根本上消除了异步 IO 的系统调用开销,并将异步能力从文件扩展到网络、定时器、进程管理等几乎所有内核操作。理解这些机制的实现细节,是构建高性能存储系统的必要基础。

下一篇将深入探讨 Linux 文件系统的 VFS 层设计,以及 ext4、XFS 等具体文件系统的日志机制(Journaling)实现。