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 的工作就需要用戶進程考慮了

