一、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 等),页缓存是一个障碍:
双重缓冲浪费内存 :数据库有自己的 Buffer Pool,页缓存与之重叠,同样的数据在内存中存两份。
缓存污染 :大规模全表扫描会把热数据从页缓存中驱逐,破坏缓存效果。
fsync 语义复杂 :数据库需要精确控制数据落盘时机(WAL 机制),通过页缓存的异步写入会引入不确定性。
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 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 struct dio { int flags; blk_opf_t opf; struct gendisk *bio_disk ; struct inode *inode ; loff_t i_size; dio_iodone_t *end_io; void *private; spinlock_t bio_lock; int page_errors; int is_async; bool defer_completion; bool should_dirty; int io_error; unsigned long refcount; struct bio *bio_list ; struct task_struct *waiter ; struct kiocb *iocb ; ssize_t result; union { struct page *pages [DIO_PAGES ]; struct work_struct complete_work ; }; } ____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); }
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; void (*ki_complete)(struct kiocb *iocb, long ret); void *private; int ki_flags; u16 ki_ioprio; union { struct wait_page_queue *ki_waitq ; __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 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) { 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 static long read_events (struct kioctx *ctx, long min_nr, long nr, struct io_event __user *event, ktime_t until) { long ret = 0 ; 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; __u64 obj; __s64 res; __s64 res2; };
3.4 Linux AIO 的局限性 Linux 内核 AIO 存在若干根本性限制,这也是 io_uring 诞生的主要动因:
只支持 O_DIRECT :对 Buffered IO 的 aio_read/aio_write 实际上并不是真正异步的——当页缓存缺页时会同步阻塞在工作队列线程里,只是把阻塞转移到了内核线程,并未消除。
每次 syscall 开销大 :io_submit 每次需要从用户空间拷贝 iocb 结构(每个 64 字节),无法利用共享内存避免拷贝。
io_getevents 轮询开销 :需要进入内核态才能获取完成事件。
不支持网络 IO :Linux AIO 仅适用于文件描述符,不支持 socket。
不支持 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 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; struct { struct mutex uring_lock ; u32 *sq_array; struct io_uring_sqe *sq_sqes ; unsigned cached_sq_head; unsigned sq_entries; struct io_wq_work_list defer_list ; } ____cacheline_aligned_in_smp; struct { unsigned cached_cq_tail; unsigned cq_entries; struct io_ev_fd __rcu *io_ev_fd ; 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 ; 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; __u8 flags; __u16 ioprio; __s32 fd; union { __u64 off; __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; 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; __s32 res; __u32 flags; };
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 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 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 ; 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 ; } 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->opcode = IORING_OP_READ_FIXED; sqe->buf_index = 0 ;
注册文件(IORING_REGISTER_FILES)类似,将 fd 数组预先注册,SQE 中 flags |= IOSQE_FIXED_FILE,fd 字段为数组索引而非真实 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->user_data = 1 ; sqe = io_uring_get_sqe(&ring); io_uring_prep_write(sqe, dst_fd, buf, len, 0 ); sqe->user_data = 2 ; io_uring_submit(&ring);
链式请求中任意一步失败,后续步骤会以 -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_queue_init(QUEUE_DEPTH, &ring, 0 ); fd = open("/tmp/testfile" , O_RDONLY | O_DIRECT); sqe = io_uring_get_sqe(&ring); io_uring_prep_read(sqe, fd, buf, BLOCK_SZ, 0 ); sqe->user_data = 42 ; 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); } 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 \ --sqthread_poll=1 \ --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 echo 1048576 > /proc/sys/fs/aio-max-nrecho none > /sys/block/nvme0n1/queue/schedulerecho 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)实现。