POSIX AIO 是在用戶控件模擬異步 IO 的功能,不需要內核支持,而 linux AIO 則是 linux 內核原聲支持的異步 IO 調用,行為更加低級
關於 linux IO 模型及 AIO、POSIX AIO 的簡介,請參看:
libaio 實現的異步 IO 主要包含以下接口:
函數 | 功能 | 原型 |
io_setup | 創建一個異步IO上下文(io_context_t是一個句柄) | int io_setup(int maxevents, io_context_t *ctxp); |
io_destroy | 銷毀一個異步IO上下文(如果有正在進行的異步IO,取消並等待它們完成) | int io_destroy(io_context_t ctx); |
io_submit | 提交異步IO請求 | long io_submit(aio_context_t ctx_id, long nr, struct iocb **iocbpp); |
io_cancel | 取消一個異步IO請求 | long io_cancel(aio_context_t ctx_id, struct iocb *iocb, struct io_event *result); |
io_getevents | 等待並獲取異步IO請求的事件(也就是異步請求的處理結果) | long io_getevents(aio_context_t ctx_id, long min_nr, long nr, struct io_event *events, struct timespec *timeout); |
iocb 結構
struct iocb主要包含以下字段:
struct iocb { /* * 請求類型 * 如:IOCB_CMD_PREAD=讀、IOCB_CMD_PWRITE=寫、等 */ __u16 aio_lio_opcode; /* * 要被操作的fd */ __u32 aio_fildes; /* * 讀寫操作對應的內存buffer */ __u64 aio_buf; /* * 需要讀寫的字節長度 */ __u64 aio_nbytes; /* * 讀寫操作對應的文件偏移 */ __s64 aio_offset; /* * 請求可攜帶的私有數據 * 在io_getevents時能夠從io_event結果中取得) */ __u64 aio_data; /* * 可選IOCB_FLAG_RESFD標記 * 表示異步請求處理完成時使用eventfd進行通知 */ __u32 aio_flags; /* * 有IOCB_FLAG_RESFD標記時,接收通知的eventfd */ __u32 aio_resfd; }
struct io_event { /* * 對應iocb的aio_data的值 */ __u64 data; /* * 指向對應iocb的指針 */ __u64 obj; /* * 對應IO請求的結果 * >=0: 相當於對應的同步調用的返回值;<0: -errno */ __s64 res; }
異步 IO 上下文
aio_context_t 即 AIO 上下文句柄,該結構體對應內核中的一個 struct kioctx 結構,用來給一組異步 IO 請求提供一個上下文環境,每個進程可以有多個 aio_context_t,io_setup 的第一個參數聲明了同時駐留在內核中的異步 IO 上下文數量
kioctx 結構主要包含以下字段:
struct kioctx { /* * 調用者進程對應的內存管理結構 * 代表了調用者的虛擬地址空間 */ struct mm_struct* mm; /* * 上下文ID,也就是io_context_t句柄的值 * 等於ring_info.mmap_base */ unsigned long user_id; /* * 屬於同一地址空間的所有kioctx結構通過這個list串連起來 * 鏈表頭是mm->ioctx_list */ struct hlist_node list; /* * 等待隊列 * io_getevents系統調用可能需要等待 * 調用者就在該等待隊列上睡眠 */ wait_queue_head_t wait; /* * 進行中的請求數目 */ int reqs_active; /* * 進行中的請求隊列 */ struct list_head active_reqs; /* * 最大請求數 * 對應io_setup調用的int maxevents參數 */ unsigned max_reqs; /* * 需要aio線程處理的請求列表 * 某些情況下,IO請求可能交給aio線程來提交 */ struct list_head run_list; /* * 延遲任務隊列 * 當需要aio線程處理請求時,將wq掛入aio線程對應的請求隊列 */ struct delayed_work wq; /* * 存放請求結果io_event結構的ring buffer */ struct aio_ring_info ring_info; }
其中,aio_ring_info 結構用於存放請求結果 io_event 結構的 ring buffer,主要包含以下字段:
aio_ring_info 結構中,nr_page * PAGE_SIZE = mmap_size
以上數據結構都是在內核地址空間上分配的,是內核專有的,用戶程序無法訪問和使用
但是 io_event 結構是內核在用戶地址空間上分配的 buffer,用戶可以修改,但是首地址、大小等信息都是由內核維護的,用戶程序通過 io_getevents 函數修改
實現原理
io_setup 函數創建了一個 AIO 上下文,並通過值-結果參數 aio_context_t 類型指針返回其句柄
io_setup 調用后,內核會通過 mmap 在對應的用戶地址空間分配一段內存,由 aio_ring_info 結構中的 mmap_base、mmap_size 描述這個映射對應的位置和大小,由 ring_pages、nr_pages 描述實際分配的物理內存頁面信息,異步 IO 完成后,內核會將異步 IO 的結果寫入其中
在 mmap_base 指向的用戶地址空間上,會存放着一個 struct aio_ring 結構,用來管理 ring buffer,主要包含以下字段:
unsigned id; // 等於 aio_ring_info 中的 user_id unsigned nr; // 等於 aio_ring_info 中的 nr unsigned head; // io_events 數組隊首 unsigned tail; // io_events 數組游標 unsigned magic; // 用於確定數據結構有沒有異常篡改 unsigned compat_features; unsigned incompat_features; unsigned header_length; // aio_ring 結構大小 struct io_event *io_events; // io_event buffer 首地址
這個數據結構存在於用戶地址空間中,內核作為生產者,在 buffer 中放入數據,並修改 tail 字段,用戶程序作為消費者從 buffer 中取出數據,並修改 head 字段
每一個請求用戶都會創建一個 iocb 結構用於描述這個請求,而對應於用戶傳遞的每一個 iocb 結構,內核都會生成一個與之對應的 kiocb 結構,並只該結構中的 ring_info 中預留一個 io_events 空間,用於保存處理的結果
struct kiocb { struct kioctx* ki_ctx; /* 請求對應的kioctx(上下文結構)*/ struct list_head ki_run_list; /* 需要aio線程處理的請求,通過該字段鏈入ki_ctx->run_list */ struct list_head ki_list; /* 鏈入ki_ctx->active_reqs */ struct file* ki_filp; /* 對應的文件指針*/ void __user* ki_obj.user; /* 指向用戶態的iocb結構*/ __u64 ki_user_data; /* 等於iocb->aio_data */ loff_t ki_pos; /* 等於iocb->aio_offset */ unsigned short ki_opcode; /* 等於iocb->aio_lio_opcode */ size_t ki_nbytes; /* 等於iocb->aio_nbytes */ char __user * ki_buf; /* 等於iocb->aio_buf */ size_t ki_left; /* 該請求剩余字節數(初值等於iocb->aio_nbytes)*/ struct eventfd_ctx* ki_eventfd; /* 由iocb->aio_resfd對應的eventfd對象*/ ssize_t (*ki_retry)(struct kiocb *); /*由ki_opcode選擇的請求提交函數*/ }
這以后,對應的異步讀寫請求就通過調用 file->f_op->aio_read 或 file->f_op->aio_write 被提交到了虛擬文件系統,與普通的文件讀寫請求非常類似,但是提交完后 IO 請求立即返回,而不等待虛擬文件系統完成相應操作
對於虛擬文件系統返回 EIOCBRETRY 需要重試的情況,內核會在當前 CPU 的 aio 線程中添加一個任務,讓 aio 完成該任務的重新提交
與 POSIX AIO 區別
從上圖中的流程就可以看出,linux 版本的 AIO 與 POSIX 版本的 AIO 最大的不同在於 linux 版本的 AIO 實際上利用了 CPU 和 IO 設備異步工作的特性,與同步 IO 相比,很大程度上節約了 CPU 資源的浪費
而 POSIX AIO 利用了線程與線程之間的異步工作特性,在用戶線程中實現 IO 的異步操作
POSIX AIO 支持非 direct-io,而且實現非常靈活,可配置性很高,可以利用內核提供的page cache來提高效率,而 linux 內核實現的 AIO 就只支持 direct-io,cache 的工作就需要用戶進程考慮了