前段時間在自研的基於iSCSI的SAN 上跑mysql,CPU的iowait很大,后面改用Native AIO,有了非常大的改觀。這里簡單總結一下Native AIO的實現。對於以IO為最大瓶頸的數據庫,native AIO幾乎不二的選擇,僅僅依靠多線程,顯然無法解決磁盤和網絡的問題。
1 API 與data struct
AIO的主要接口:
System call |
Description |
io_setup( ) |
Initializes an asynchronous context for the current process |
io_submit( ) |
Submits one or more asynchronous I/O operations |
io_getevents( ) |
Gets the completion status of some outstanding asynchronous I/O operations |
io_cancel( ) |
Cancels an outstanding I/O operation |
io_destroy( ) |
Removes an asynchronous context for the current process |
1.1 AIO上下文
使用AIO的第一步就是創建AIO上下文,AIO上下文用於跟蹤進程請求的異步IO的運行情況。AIO上下文在用戶空間對應數據結果aio_context_t:
//linux/aio_abi.h typedef unsigned long aio_context_t;
//創建AIO上下文 int io_setup(unsigned nr_events, aio_context_t *ctxp); |
Io_setup創建接收nr_events事件的AIO上下文。
kioctx:
AIO上下文在內核空間對應數據結構kioctx,它保存異步IO的所有信息:
//AIO環境 struct kioctx { atomic_t users; int dead; struct mm_struct *mm;
/* This needs improving */ unsigned long user_id; //ring_info.mmap_base,AIO環的起始地址 struct kioctx *next; //下一個aio環境
wait_queue_head_t wait; //等待進程隊列
spinlock_t ctx_lock;
int reqs_active; struct list_head active_reqs; /* used for cancellation */ struct list_head run_list; /* used for kicked reqs,正在運行的IO請求鏈表 */
unsigned max_reqs;//異步IO操作的最大數量
struct aio_ring_info ring_info; //AIO Ring
struct work_struct wq; }; |
一個進程可以創建多個AIO上下文,這些AIO上下文構成一個單向鏈表。
struct mm_struct { ... /* aio bits */ rwlock_t ioctx_list_lock; struct kioctx *ioctx_list; //進程的AIO上下文鏈表
struct kioctx default_kioctx; } |
AIO Ring
AIO上下文kioctx對象包含一個重要的數據結構AIO Ring:
//aio.h //AIO環 #define AIO_RING_PAGES 8 struct aio_ring_info { unsigned long mmap_base; //AIO ring用戶態起始地址 unsigned long mmap_size; //緩沖區長度
struct page **ring_pages;//AIO環頁框指針數組 spinlock_t ring_lock; long nr_pages;
unsigned nr, tail;
struct page *internal_pages[AIO_RING_PAGES]; }; |
AIO Ring對應用戶態進程地址空間的一段內存緩存區,用戶態進程可以訪問,內核也可訪問。實際上,內核先調用kmalloc函數分配一些頁框,然后通過do_mmap映射到用戶態地址空間,詳細請參考aio_setup_ring函數。
AIO Ring是一個環形緩沖區,內核用它來報告異步IO的完成情況,用戶態進程也可以直接檢查異步IO完成情況,從而避免系統調用的開銷。
AIO結構很簡單:aio_ring + io_event數組:
struct aio_ring { unsigned id; /* kernel internal index number */ unsigned nr; /* number of io_events */ unsigned head; unsigned tail;
unsigned magic; unsigned compat_features; unsigned incompat_features; unsigned header_length; /* size of aio_ring */
struct io_event io_events[0]; }; /* 128 bytes + ring size */ |
系統調用io_setup有2個參數:(1) nr_events確認最大的異步IO請求數,這將確定AIO Ring大小,即io_event數量;(2) ctxp:AIO上下文句柄的指針,實際上也是AIO Ring的起始地址aio_ring_info.mmap_base,參見函數aio_setup_ring。
1.2 提交IO請求
想要進行異步IO,需要通過系統調用io_submit提交異步IO請求。
//提交異步IO請求/aio.c asmlinkage long sys_io_submit(aio_context_t ctx_id, long nr, struct iocb __user * __user *iocbpp) |
參數:
(1)ctx_id:AIO上下文句柄,內核通過它查找對應的kioctx對象;
(2)iocb數組,每個iocb描述一個異步IO請求;
(3)nr:iocb數組的大小。
iocb
//用戶態異步IO請求描述符/aio_abi.h struct iocb { /* these are internal to the kernel/libc. */ __u64 aio_data; /* data是留給用來自定義的指針:可以設置為IO完成后的callback函數 */ __u32 PADDED(aio_key, aio_reserved1); /* the kernel sets aio_key to the req # */
/* common fields */ __u16 aio_lio_opcode; /* see IOCB_CMD_ above,操作的類型:IO_CMD_PWRITE | IO_CMD_PREAD */ __s16 aio_reqprio; __u32 aio_fildes; //IO操作的文件描述符
__u64 aio_buf; //IO的buffer __u64 aio_nbytes; //IO請求字節數 __s64 aio_offset;//偏移
/* extra parameters */ __u64 aio_reserved2; /* TODO: use this for a (struct sigevent *) */ __u64 aio_reserved3; }; /* 64 bytes */ |
數據結構iocb用來描述用戶空間的異步IO請求,對應的內核數據結構為kiocb。
io_submit的流程:
函數io_submit_one對每個iocb分配一個kiocb對象,加入到AIO上下文kioctx的IO請求隊列run_list;然后調用aio_run_iocb發起IO操作,它實際上調用kiocb的ki_retry方法(aio_pread/aio_pwrite)。
如果ki_retry方法返回-EIOCBRETRY,表明異步IO請求已經提交,但是還沒全部完成,稍后kiocb的ki_retry方法還會被繼續調用,來繼續完成IO請求;否則,調用aio_complete,在AIO Ring加入一個表示一個IO完成的io_event。
1.3 收集完成的IO請求
asmlinkage long sys_io_getevents(aio_context_t ctx_id, long min_nr, long nr, struct io_event __user *events, struct timespec __user *timeout) |
參數:
(1)ctx_id:AIO上下文句柄;
(2)min_nr:至少收集min_nr個已經完成的IO請求才返回;
(3)nr:最多收集nr個已經完成的IO請求;
(4)timeout:等待的時間
(5)events:由應用層分配,內核將完成的io_event拷貝到該緩沖區,所以,events數組要保證至少有nr個io_event。
io_event:
//aio_abi.h struct io_event { __u64 data; /* the data field from the iocb */ __u64 obj; /* what iocb this event came from */ __s64 res; /* result code for this event */ __s64 res2; /* secondary result */ }; |
io_event是用來描述返回結果的:
(1)data對應iocb的aio_data,返回用戶定義的指針;
(2)obj就是之前提交IO任務時的iocb;
(3)res和res2來表示IO任務完成的狀態。
io_getevents的流程:
比較簡單,掃描AIO上下文kiocxt的AIO Ring,檢查是否有完成的io_event。如果至少有min_nr個完成IO事件(或者超時),則將完成的io_event拷貝到events,並返回io_event的個數或者錯誤;否則,將進程本身加入到kiocxt的等待隊列,掛起進程。
2 AIO工作隊列
2.1 創建AIO工作隊列
//aio.c static struct workqueue_struct *aio_wq;//AIO工作隊列 static int __init aio_setup(void) { ... aio_wq = create_workqueue("aio"); ...
|
2.2 創建work_struct
static struct kioctx *ioctx_alloc(unsigned nr_events) { ... INIT_WORK(&ctx->wq, aio_kick_handler, ctx); |
函數aio_kick_hanlder由aio內核線程處理aio work時調用:
static void aio_kick_handler(void *data) { requeue =__aio_run_iocbs(ctx); ... /* * we're in a worker thread already, don't use queue_delayed_work, */ if (requeue) queue_work(aio_wq, &ctx->wq); } |
邏輯很簡單,調用__aio_run_iocbs繼續處理kioctx中的待完成異步IO,如果需要,則將aio work繼續加入aio工作隊列,下一次再處理。
2.3 調度工作
函數aio_run_iocbs發起異步IO請求后,如果kioctx的run_list還有未完成的IO,則調用queue_delayed_work將work_struct(kioctx->wq)加入到AIO工作隊列aio_wq,由aio內核線程繼續發起異步IO。
3 AIO與epoll
在使用AIO時,需要通過系統調用io_getevents獲取已經完成的IO事件,而系統調用io_getevents是阻塞的,所以有2種方式:(1)使用多線程,用專門的線程調用io_getevents,參考MySQL5.5及以上版本;(2)對於單線程程序,可以通過epoll來使用AIO;不過,這需要系統調用eventfd的支持,而該系統調用只在2.6.22之后的內核才支持。
eventfd 是 Linux-native aio 其中的一個 API,用來生成 file descriptors,這些 file descriptors 可為應用程序提供更高效 “等待/通知” 的事件機制。和 pipe 作用相似,但比 pipe 更好,一方面它只用到一個 file descriptor(pipe 要用兩個),節省了內核資源;另一方面,eventfd 的緩沖區管理要簡單得多,pipe 需要不定長的緩沖區,而 eventfd 全部緩沖只有定長 8 bytes。
關於AIO與epoll的結合,請參考:
nginx 0.8.x穩定版對linux aio的支持(http://www.pagefault.info/?p=76)
4 AIO與direct IO
AIO需要與direct IO結合。
關於direct IO的簡單實現,可以參考:
Linux 中直接 I/O 機制的介紹
http://www.ibm.com/developerworks/cn/linux/l-cn-directio/index.html
5 案例
(1)同步IO
(2)Native AIO