知道異步IO已經(jīng)很久了,但是直到最近续担,才真正用它來(lái)解決一下實(shí)際問(wèn)題(在一個(gè)CPU密集型的應(yīng)用中嫁审,有一些需要處理的數(shù)據(jù)可能放在磁盤上磅废。預(yù)先知道這些數(shù)據(jù)的位置,所以預(yù)先發(fā)起異步IO讀請(qǐng)求侦鹏。等到真正需要用到這些數(shù)據(jù)的時(shí)候铜跑,再等待異步IO完成湿右。使用了異步IO荐操,在發(fā)起IO請(qǐng)求到實(shí)際使用數(shù)據(jù)這段時(shí)間內(nèi)芜抒,程序還可以繼續(xù)做其他事情)。
假此機(jī)會(huì)托启,也順便研究了一下linux下的異步IO的實(shí)現(xiàn)宅倒。
linux下主要有兩套異步IO,一套是由glibc實(shí)現(xiàn)的(以下稱之為glibc版本)屯耸、一套是由linux內(nèi)核實(shí)現(xiàn)拐迁,并由libaio來(lái)封裝調(diào)用接口(以下稱之為linux版本)。
glibc版本
接口 glibc版本主要包含如下接口:
int aio_read(struct aiocb *aiocbp); /* 提交一個(gè)異步讀 */
int aio_write(struct aiocb *aiocbp); /* 提交一個(gè)異步寫 */
int aio_cancel(int fildes, struct aiocb *aiocbp); /* 取消一個(gè)異步請(qǐng)求(或基于一個(gè)fd的所有異步請(qǐng)求疗绣,aiocbp==NULL) */
int aio_error(const struct aiocb *aiocbp); /* 查看一個(gè)異步請(qǐng)求的狀態(tài)(進(jìn)行中EINPROGRESS线召?還是已經(jīng)結(jié)束或出錯(cuò)?) */
ssize_t aio_return(struct aiocb *aiocbp); /* 查看一個(gè)異步請(qǐng)求的返回值(跟同步讀寫定義的一樣) */
int aio_suspend(const struct aiocb * const list[], int nent, const struct timespec *timeout); /* 阻塞等待請(qǐng)求完成 */
其中持痰,struct aiocb主要包含以下字段:
int aio_fildes; /* 要被讀寫的fd */
void * aio_buf; /* 讀寫操作對(duì)應(yīng)的內(nèi)存buffer */
__off64_t aio_offset; /* 讀寫操作對(duì)應(yīng)的文件偏移 */
size_t aio_nbytes; /* 需要讀寫的字節(jié)長(zhǎng)度 */
int aio_reqprio; /* 請(qǐng)求的優(yōu)先級(jí) */
struct sigevent aio_sigevent; /* 異步事件灶搜,定義異步操作完成時(shí)的通知信號(hào)或回調(diào)函數(shù) */
實(shí)現(xiàn) glibc的aio實(shí)現(xiàn)是比較通俗易懂的:
1、異步請(qǐng)求被提交到request_queue中工窍;
2割卖、request_queue實(shí)際上是一個(gè)表結(jié)構(gòu),"行"是fd患雏、"列"是具體的請(qǐng)求鹏溯。也就是說(shuō),同一個(gè)fd的請(qǐng)求會(huì)被組織在一起淹仑;
3丙挽、異步請(qǐng)求有優(yōu)先級(jí)概念,屬于同一個(gè)fd的請(qǐng)求會(huì)按優(yōu)先級(jí)排序匀借,并且最終被按優(yōu)先級(jí)順序處理颜阐;
4、隨著異步請(qǐng)求的提交吓肋,一些異步處理線程被動(dòng)態(tài)創(chuàng)建凳怨。這些線程要做的事情就是從request_queue中取出請(qǐng)求,然后處理之是鬼;
5肤舞、為避免異步處理線程之間的競(jìng)爭(zhēng),同一個(gè)fd所對(duì)應(yīng)的請(qǐng)求只由一個(gè)線程來(lái)處理均蜜;
6李剖、異步處理線程同步地處理每一個(gè)請(qǐng)求,處理完成后在對(duì)應(yīng)的aiocb中填充結(jié)果囤耳,然后觸發(fā)可能的信號(hào)通知或回調(diào)函數(shù)(回調(diào)函數(shù)是需要?jiǎng)?chuàng)建新線程來(lái)調(diào)用的)篙顺;
7偶芍、異步處理線程在完成某個(gè)fd的所有請(qǐng)求后,進(jìn)入閑置狀態(tài)慰安;
8腋寨、異步處理線程在閑置狀態(tài)時(shí),如果request_queue中有新的fd加入化焕,則重新投入工作萄窜,去處理這個(gè)新fd的請(qǐng)求(新fd和它上一次處理的fd可以不是同一個(gè));
9撒桨、異步處理線程處于閑置狀態(tài)一段時(shí)間后(沒有新的請(qǐng)求)查刻,則會(huì)自動(dòng)退出。等到再有新的請(qǐng)求時(shí)凤类,再去動(dòng)態(tài)創(chuàng)建穗泵;
看起來(lái),換作是我們谜疤,要在用戶態(tài)實(shí)現(xiàn)一個(gè)異步IO佃延,似乎大概也會(huì)設(shè)計(jì)成類似的樣子……
linux版本
接口 下面再來(lái)看看linux版本的異步IO。它主要包含如下系統(tǒng)調(diào)用接口:
int io_setup(int maxevents, io_context_t *ctxp); /* 創(chuàng)建一個(gè)異步IO上下文(io_context_t是一個(gè)句柄) */
int io_destroy(io_context_t ctx); /* 銷毀一個(gè)異步IO上下文(如果有正在進(jìn)行的異步IO夷磕,取消并等待它們完成) */
long io_submit(aio_context_t ctx_id, long nr, struct iocb **iocbpp); /* 提交異步IO請(qǐng)求 */
long io_cancel(aio_context_t ctx_id, struct iocb *iocb, struct io_event *result); /* 取消一個(gè)異步IO請(qǐng)求 */
longio_getevents(aio_context_t ctx_id, long min_nr, long nr, structio_event *events, struct timespec *timeout) /*等待并獲取異步IO請(qǐng)求的事件(也就是異步請(qǐng)求的處理結(jié)果) */
其中履肃,struct iocb主要包含以下字段:
__u16 aio_lio_opcode; /* 請(qǐng)求類型(如:IOCB_CMD_PREAD=讀、IOCB_CMD_PWRITE=寫坐桩、等) */
__u32 aio_fildes; /* 要被操作的fd */
__u64 aio_buf; /* 讀寫操作對(duì)應(yīng)的內(nèi)存buffer */
__u64 aio_nbytes; /* 需要讀寫的字節(jié)長(zhǎng)度 */
__s64 aio_offset; /* 讀寫操作對(duì)應(yīng)的文件偏移 */
__u64 aio_data; /* 請(qǐng)求可攜帶的私有數(shù)據(jù)(在io_getevents時(shí)能夠從io_event結(jié)果中取得) */
__u32 aio_flags; /* 可選IOCB_FLAG_RESFD標(biāo)記尺棋,表示異步請(qǐng)求處理完成時(shí)使用eventfd進(jìn)行通知(百度一下) */
__u32 aio_resfd; /* 有IOCB_FLAG_RESFD標(biāo)記時(shí),接收通知的eventfd */
其中绵跷,struct io_event主要包含以下字段:
__u64 data; /* 對(duì)應(yīng)iocb的aio_data的值 */
__u64 obj; /* 指向?qū)?yīng)iocb的指針 */
__s64 res; /* 對(duì)應(yīng)IO請(qǐng)求的結(jié)果(>=0: 相當(dāng)于對(duì)應(yīng)的同步調(diào)用的返回值膘螟;<0: -errno) */
實(shí)現(xiàn) io_context_t句柄在內(nèi)核中對(duì)應(yīng)一個(gè)struct kioctx結(jié)構(gòu),用來(lái)給一組異步IO請(qǐng)求提供一個(gè)上下文碾局。其主要包含以下字段:
struct mm_struct* mm; /* 調(diào)用者進(jìn)程對(duì)應(yīng)的內(nèi)存管理結(jié)構(gòu)(代表了調(diào)用者的虛擬地址空間) */
unsigned long user_id; /* 上下文ID荆残,也就是io_context_t句柄的值(等于ring_info.mmap_base) */
struct hlist_node list; /* 屬于同一地址空間的所有kioctx結(jié)構(gòu)通過(guò)這個(gè)list串連起來(lái),鏈表頭是mm->ioctx_list */
wait_queue_head_t wait; /* 等待隊(duì)列(io_getevents系統(tǒng)調(diào)用可能需要等待净当,調(diào)用者就在該等待隊(duì)列上睡眠) */
int reqs_active; /* 進(jìn)行中的請(qǐng)求數(shù)目 */
struct list_head active_reqs; /* 進(jìn)行中的請(qǐng)求隊(duì)列 */
unsigned max_reqs; /* 最大請(qǐng)求數(shù)(對(duì)應(yīng)io_setup調(diào)用的int maxevents參數(shù)) */
struct list_head run_list; /* 需要aio線程處理的請(qǐng)求列表(某些情況下脊阴,IO請(qǐng)求可能交給aio線程來(lái)提交) */
struct delayed_work wq; /* 延遲任務(wù)隊(duì)列(當(dāng)需要aio線程處理請(qǐng)求時(shí),將wq掛入aio線程對(duì)應(yīng)的請(qǐng)求隊(duì)列) */
struct aio_ring_info ring_info; /* 存放請(qǐng)求結(jié)果io_event結(jié)構(gòu)的ring buffer */
其中蚯瞧,這個(gè)aio_ring_info結(jié)構(gòu)比較值得一提,它是用于存放請(qǐng)求結(jié)果io_event結(jié)構(gòu)的ring buffer品擎。它主要包含了如下字段:
unsigned long mmap_base; /* ring buffer的地始地址 */
unsigned long mmap_size; /* ring buffer分配空間的大小 */
struct page** ring_pages; /* ring buffer對(duì)應(yīng)的page數(shù)組 */
long nr_pages; /* 分配空間對(duì)應(yīng)的頁(yè)面數(shù)目(nr_pages * PAGE_SIZE = mmap_size) */
unsigned nr, tail; /* 包含io_event的數(shù)目及存取游標(biāo) */
這個(gè)數(shù)據(jù)結(jié)構(gòu)看起來(lái)有些奇怪埋合,直接弄一個(gè)io_event數(shù)組不就完事了么?為什么要維護(hù)mmap_base萄传、mmap_size甚颂、ring_pages蜜猾、nr_pages這么復(fù)雜的一組信息,而又把io_event結(jié)構(gòu)隱藏起來(lái)呢振诬?
這里的奇妙之處就在于蹭睡,io_event結(jié)構(gòu)的buffer是在用戶態(tài)地址空間上分配的。注意赶么,我們?cè)趦?nèi)核里面看到了諸多數(shù)據(jù)結(jié)構(gòu)都是在內(nèi)核地址空間上分配的肩豁,因?yàn)檫@些結(jié)構(gòu)都是內(nèi)核專有的,沒必要給用戶程序看到辫呻,更不能讓用戶程序去修改清钥。而這里的io_event卻是有意讓用戶程序看到,而且用戶就算修改了也不會(huì)對(duì)內(nèi)核的正確性造成影響放闺。于是這里使用了這樣一個(gè)有些取巧的辦法祟昭,由內(nèi)核在用戶態(tài)地址空間上分配buffer。(如果換一個(gè)保守點(diǎn)的做法怖侦,內(nèi)核態(tài)可以維護(hù)io_event的buffer篡悟,然后io_getevents的時(shí)候,將對(duì)應(yīng)的io_event復(fù)制一份到用戶空間匾寝。)
按照這樣的思路搬葬,io_setup時(shí),內(nèi)核會(huì)通過(guò)mmap在對(duì)應(yīng)的用戶空間分配一段內(nèi)存旗吁,mmap_base踩萎、mmap_size就是這個(gè)內(nèi)存映射對(duì)應(yīng)的位置和大小。然后很钓,光有映射還不行香府,還必須立馬分配物理內(nèi)存,ring_pages码倦、nr_pages就是分配好的物理頁(yè)面企孩。(因?yàn)檫@些內(nèi)存是要被內(nèi)核直接訪問(wèn)的,內(nèi)核會(huì)將異步IO的結(jié)果寫入其中袁稽。如果物理頁(yè)面延遲分配勿璃,那么內(nèi)核訪問(wèn)這些內(nèi)存的時(shí)候會(huì)發(fā)生缺頁(yè)異常。而處理內(nèi)核態(tài)的缺頁(yè)異常又很麻煩推汽,所以還不如直接分配物理內(nèi)存的好补疑。其二,內(nèi)核在訪問(wèn)這個(gè)buffer里的信息時(shí)歹撒,也并不是通過(guò)mmap_base這個(gè)虛擬地址去直接訪問(wèn)的莲组。既然是異步,那么結(jié)果寫回的時(shí)候可能是在另一個(gè)上下文上面暖夭,虛擬地址空間都不同锹杈。為了避免進(jìn)行虛擬地址空間的切換撵孤,內(nèi)核干脆直接通過(guò)kmap將ring_pages映射到高端內(nèi)存上去訪問(wèn)好了。)
然后竭望,在mmap_base指向的用戶空間的地址上邪码,會(huì)存放著一個(gè)struct aio_ring結(jié)構(gòu),用來(lái)管理這個(gè)ring buffer咬清。其主要包含了如下字段:
unsigned id; /* 等于aio_ring_info中的user_id */
unsigned nr; /* 等于aio_ring_info中的nr */
unsigned head,tail; /* io_events數(shù)組的游標(biāo) */
unsigned magic,compat_features,incompat_features;
unsigned header_length; /* aio_ring結(jié)構(gòu)的大小 */
struct io_event io_events[0]; /* io_event的buffer */
終于闭专,我們期待的io_event數(shù)組出現(xiàn)了。
看到這里枫振,如果前面的內(nèi)容你已經(jīng)理解清楚了喻圃,你一定會(huì)有個(gè)疑問(wèn):既然整個(gè)aio_ring結(jié)構(gòu)及其中的io_event緩沖都是放在用戶空間的,內(nèi)核還提供io_getevents系統(tǒng)調(diào)用干什么粪滤?用戶程序不是直接就可以取用io_event斧拍,并且修改游標(biāo)了么(內(nèi)核作為生產(chǎn)者,修改aio_ring->tail杖小;用戶作為消費(fèi)者肆汹,修改aio_ring->head)?我想予权,aio_ring之所以要放在用戶空間昂勉,其原本用意應(yīng)該就是這樣的。
那么扫腺,用戶空間如何知道aio_ring結(jié)構(gòu)的地址(aio_ring_info->mmap_base)呢岗照?其實(shí)kioctx結(jié)構(gòu)中的user_id,也就是io_setup返回給用戶的io_context_t笆环,就等于aio_ring_info->mmap_base攒至。
然后,aio_ring結(jié)構(gòu)中還有諸如magic躁劣、compat_features迫吐、incompat_features這樣的字段,用戶空間可以讀這些magic账忘,以確定數(shù)據(jù)結(jié)構(gòu)沒有被異常篡改志膀。如果一切可控,那么就自己動(dòng)手鳖擒、豐衣足食溉浙;否則就還是走io_getevents系統(tǒng)調(diào)用。而io_getevents系統(tǒng)調(diào)用通過(guò)aio_ring_info->ring_pages得到aio_ring結(jié)構(gòu)蒋荚,再將相應(yīng)的io_event拷貝到用戶空間放航。
下面貼一段libaio中的io_getevents的代碼(前面提到過(guò),linux版本的異步IO是由用戶態(tài)的libaio來(lái)封裝的):
int io_getevents_0_4(io_context_t ctx, long min_nr, long nr, struct io_event * events, struct timespec * timeout){
struct aio_ring *ring;
ring = (struct aio_ring*)ctx;
if (ring==NULL || ring->magic != AIO_RING_MAGIC)
goto do_syscall;
if (timeout!=NULL && timeout->tv_sec == 0 && timeout->tv_nsec == 0) {
if (ring->head == ring->tail)
return 0;
}
do_syscall:
return __io_getevents_0_4(ctx, min_nr, nr, events, timeout);
}
其中確實(shí)用到了用戶空間上的aio_ring結(jié)構(gòu)的信息圆裕,不過(guò)尺度還是不夠大广鳍。
以上就是異步IO的context的結(jié)構(gòu)。那么吓妆,為什么linux版本的異步IO需要“上下文”這么個(gè)概念赊时,而glibc版本則不需要呢?
在glibc版本中行拢,異步處理線程是glibc在調(diào)用者進(jìn)程中動(dòng)態(tài)創(chuàng)建的線程祖秒,它和調(diào)用者必定是在同一個(gè)虛擬地址空間中的。這里已經(jīng)隱含了“同一上下文”這么個(gè)關(guān)系舟奠。
而對(duì)于內(nèi)核來(lái)說(shuō)竭缝,要面對(duì)的是任意的進(jìn)程,任意的虛擬地址空間沼瘫。當(dāng)處理一個(gè)異步請(qǐng)求時(shí)抬纸,內(nèi)核需要在調(diào)用者對(duì)應(yīng)的地址空間中存取數(shù)據(jù),必須知道這個(gè)虛擬地址空間是什么耿戚。不過(guò)當(dāng)然湿故,如果設(shè)計(jì)上要想把“上下文”這個(gè)概念隱藏了也是肯定可以的(比如讓每個(gè)mm隱含一個(gè)異步IO上下文)。具體如何選擇膜蛔,只是設(shè)計(jì)上的問(wèn)題坛猪。
struct iocb在內(nèi)核中又對(duì)應(yīng)到struct kiocb結(jié)構(gòu),主要包含以下字段:
struct kioctx* ki_ctx; /* 請(qǐng)求對(duì)應(yīng)的kioctx(上下文結(jié)構(gòu)) */
struct list_head ki_run_list; /* 需要aio線程處理的請(qǐng)求皂股,通過(guò)該字段鏈入ki_ctx->run_list */
struct list_head ki_list; /* 鏈入ki_ctx->active_reqs */
struct file* ki_filp; /* 對(duì)應(yīng)的文件指針 */
void __user* ki_obj.user; /* 指向用戶態(tài)的iocb結(jié)構(gòu) */
__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; /* 該請(qǐng)求剩余字節(jié)數(shù)(初值等于iocb->aio_nbytes) */
struct eventfd_ctx* ki_eventfd; /* 由iocb->aio_resfd對(duì)應(yīng)的eventfd對(duì)象 */
ssize_t (*ki_retry)(struct kiocb *); /*由ki_opcode選擇的請(qǐng)求提交函數(shù)*/
調(diào)用io_submit后墅茉,對(duì)應(yīng)于用戶傳遞的每一個(gè)iocb結(jié)構(gòu),會(huì)在內(nèi)核態(tài)生成一個(gè)與之對(duì)應(yīng)的kiocb結(jié)構(gòu)呜呐,并且在對(duì)應(yīng)kioctx結(jié)構(gòu)的ring_info中預(yù)留一個(gè)io_events的空間就斤。之后,請(qǐng)求的處理結(jié)果就被寫到這個(gè)io_event中卵史。
然后战转,對(duì)應(yīng)的異步讀寫(或其他)請(qǐng)求就被提交到了虛擬文件系統(tǒng),實(shí)際上就是調(diào)用了file->f_op->aio_read或file->f_op->aio_write(或其他)以躯。也就是槐秧,在經(jīng)歷磁盤高速緩存層、通用塊層之后忧设,請(qǐng)求被提交到IO調(diào)度層刁标,等待被處理。這個(gè)跟普通的文件讀寫請(qǐng)求是類似的址晕。
在《linux文件讀寫淺析》中可以看到膀懈,對(duì)于非direct-io的讀請(qǐng)求來(lái)說(shuō),如果pagecache不命中谨垃,那么IO請(qǐng)求會(huì)被提交到底層启搂。之后硼控,do_generic_file_read會(huì)通過(guò)lock_page操作,等待數(shù)據(jù)最終讀完胳赌。這一點(diǎn)跟異步IO是背道而馳的牢撼,因?yàn)楫惒骄鸵馕吨?qǐng)求提交后不能等待,必須馬上返回疑苫。而對(duì)于非direct-io的寫請(qǐng)求熏版,寫操作一般僅僅是將數(shù)據(jù)更新作用到page cache上,并不需要真正的寫磁盤捍掺。pagecache寫回磁盤本身是一個(gè)異步的過(guò)程撼短。可見挺勿,對(duì)于非direct-io的文件讀寫曲横,使用linux版本的異步IO接口完全沒有意義(就跟使用同步接口效果一樣)。
為什么會(huì)有這樣的設(shè)計(jì)呢满钟?因?yàn)榉莇irect-io的文件讀寫是只跟page cache打交道的胜榔。而pagecache是內(nèi)存,跟內(nèi)存打交道又不會(huì)存在阻塞湃番,那么也就沒有什么異步的概念了夭织。至于讀寫磁盤時(shí)發(fā)生的阻塞,那是pagecache跟磁盤打交道時(shí)發(fā)生的事情吠撮,跟應(yīng)用程序又沒有直接關(guān)系尊惰。
然而,對(duì)于direct-io來(lái)說(shuō)泥兰,異步則是有意義的弄屡。因?yàn)閐irect-io是應(yīng)用程序的buffer跟磁盤的直接交互(不使用page cache)。
這里鞋诗,在使用direct-io的情況下膀捷,file->f_op->aio_{read,write}提交完IO請(qǐng)求就直接返回了,然后io_submit系統(tǒng)調(diào)用返回削彬。(見后面的執(zhí)行流程全庸。)
通過(guò)linux內(nèi)核異步觸發(fā)的IO調(diào)度(如:被時(shí)鐘中斷觸發(fā)、被其他的IO請(qǐng)求觸發(fā)融痛、等)壶笼,已經(jīng)提交的IO請(qǐng)求被調(diào)度,由對(duì)應(yīng)的設(shè)備驅(qū)動(dòng)程序提交給具體的設(shè)備雁刷。對(duì)于磁盤覆劈,一般來(lái)說(shuō),驅(qū)動(dòng)程序會(huì)發(fā)起一次DMA。然后又經(jīng)過(guò)若干時(shí)間责语,讀寫請(qǐng)求被磁盤處理完成炮障,CPU將收到表示DMA完成的中斷信號(hào),設(shè)備驅(qū)動(dòng)程序注冊(cè)的處理函數(shù)將在中斷上下文中被調(diào)用鹦筹。這個(gè)處理函數(shù)會(huì)調(diào)用end_request函數(shù)來(lái)結(jié)束這次請(qǐng)求铝阐。這個(gè)流程跟《linux文件讀寫淺析》中所說(shuō)的非direct-io讀操作的情況是一樣的。
不同的是铐拐,對(duì)于同步非direct-io,end_request將通過(guò)清除page結(jié)構(gòu)的PG_locked標(biāo)記來(lái)喚醒被阻塞的讀操作流程练对,異步IO和同步IO效果一樣遍蟋。而對(duì)于direct-io,除了喚醒被阻塞的讀操作流程(同步IO)或io_getevents流程(異步IO)之外螟凭,還需要將IO請(qǐng)求的處理結(jié)果填回對(duì)應(yīng)的io_event中虚青。
最后,等到調(diào)用者調(diào)用io_getevents的時(shí)候螺男,就能獲取到請(qǐng)求對(duì)應(yīng)的結(jié)果(io_event)棒厘。而如果調(diào)用io_getevents的時(shí)候結(jié)果還沒出來(lái),流程也會(huì)被阻塞下隧,并且會(huì)在direct-io的end_request過(guò)程中得到喚醒奢人。
linux版本的異步IO也有aio線程(每CPU一個(gè)),但是跟glibc版本中的異步處理線程不同淆院,這里的aio線程是用來(lái)處理請(qǐng)求重試的何乎。某些情況下,file->f_op->aio_{read,write}可能會(huì)返回-EIOCBRETRY土辩,表示需要重試(只有一些特殊的IO設(shè)備會(huì)這樣)支救。而調(diào)用者既然使用的是異步IO接口,肯定不希望里面會(huì)有等待/重試的邏輯拷淘。所以各墨,如果遇到-EIOCBRETRY,內(nèi)核就在當(dāng)前CPU對(duì)應(yīng)的aio線程添加一個(gè)任務(wù)启涯,讓aio線程來(lái)完成請(qǐng)求的重新提交贬堵。而調(diào)用流程可以直接返回,不需要阻塞逝嚎。
請(qǐng)求在aio線程中提交和在調(diào)用者進(jìn)程中提交相比扁瓢,有一個(gè)最大的不同,就是aio線程使用的地址空間可能跟調(diào)用者線程不一樣补君。需要利用kioctx->mm切換到正確的地址空間引几,然后才能發(fā)請(qǐng)求。(參見《淺嘗異步IO》中的討論。)
內(nèi)核處理流程 最后伟桅,整理一下direct-io異步讀操作的處理流程:
io_submit敞掘。對(duì)于提交的iocbpp數(shù)組中的每一個(gè)iocb(異步請(qǐng)求),調(diào)用io_submit_one來(lái)提交它們楣铁;
io_submit_one玖雁。為請(qǐng)求分配一個(gè)kiocb結(jié)構(gòu),并且在對(duì)應(yīng)的kioctx的ring_info中為它預(yù)留一個(gè)對(duì)應(yīng)的io_event盖腕。然后調(diào)用aio_rw_vect_retry來(lái)提交這個(gè)讀請(qǐng)求赫冬;
aio_rw_vect_retry。調(diào)用file->f_op->aio_read溃列。這個(gè)函數(shù)通常是由generic_file_aio_read或者其封裝來(lái)實(shí)現(xiàn)的劲厌;
generic_file_aio_read。對(duì)于非direct-io听隐,會(huì)調(diào)用do_generic_file_read來(lái)處理請(qǐng)求(見《linux文件讀寫淺析》)补鼻。而對(duì)于direct-io,則是調(diào)用mapping->a_ops->direct_IO雅任。這個(gè)函數(shù)通常就是blkdev_direct_IO风范;
blkdev_direct_IO。調(diào)用filemap_write_and_wait_range將相應(yīng)位置可能存在的page cache廢棄掉或刷回磁盤(避免產(chǎn)生不一致)沪么,然后調(diào)用direct_io_worker來(lái)處理請(qǐng)求硼婿;
direct_io_worker。一次讀可能包含多個(gè)讀操作(對(duì)應(yīng)于類readv系統(tǒng)調(diào)用)成玫,對(duì)于其中的每一個(gè)加酵,調(diào)用do_direct_IO;
do_direct_IO哭当。調(diào)用submit_page_section猪腕;
submit_page_section。調(diào)用dio_new_bio分配對(duì)應(yīng)的bio結(jié)構(gòu)钦勘,然后調(diào)用dio_bio_submit來(lái)提交bio陋葡;
dio_bio_submit。調(diào)用submit_bio提交請(qǐng)求彻采。后面的流程就跟非direct-io是一樣的了腐缤,然后等到請(qǐng)求完成,驅(qū)動(dòng)程序?qū)⒄{(diào)用bio->bi_end_io來(lái)結(jié)束這次請(qǐng)求肛响。對(duì)于direct-io下的異步IO岭粤,bio->bi_end_io等于dio_bio_end_aio;
dio_bio_end_aio特笋。調(diào)用wake_up_process喚醒被阻塞的進(jìn)程(異步IO下剃浇,主要是io_getevents的調(diào)用者)。然后調(diào)用aio_complete;
aio_complete虎囚。將處理結(jié)果寫回到對(duì)應(yīng)的io_event中角塑;
比較
從上面的流程可以看出,linux版本的異步IO實(shí)際上只是利用了CPU和IO設(shè)備可以異步工作的特性(IO請(qǐng)求提交的過(guò)程主要還是在調(diào)用者線程上同步完成的淘讥,請(qǐng)求提交后由于CPU與IO設(shè)備可以并行工作圃伶,所以調(diào)用流程可以返回,調(diào)用者可以繼續(xù)做其他事情)蒲列。相比同步IO窒朋,并不會(huì)占用額外的CPU資源。
而glibc版本的異步IO則是利用了線程與線程之間可以異步工作的特性蝗岖,使用了新的線程來(lái)完成IO請(qǐng)求炼邀,這種做法會(huì)額外占用CPU資源(對(duì)線程的創(chuàng)建、銷毀剪侮、調(diào)度都存在CPU開銷,并且調(diào)用者線程和異步處理線程之間還存在線程間通信的開銷)洛退。不過(guò)瓣俯,IO請(qǐng)求提交的過(guò)程都由異步處理線程來(lái)完成了(而linux版本是調(diào)用者來(lái)完成的請(qǐng)求提交),調(diào)用者線程可以更快地響應(yīng)其他事情兵怯。如果CPU資源很富足彩匕,這種實(shí)現(xiàn)倒也還不錯(cuò)。
還有一點(diǎn)媒区,當(dāng)調(diào)用者連續(xù)調(diào)用異步IO接口驼仪,提交多個(gè)異步IO請(qǐng)求時(shí)。在glibc版本的異步IO中袜漩,同一個(gè)fd的讀寫請(qǐng)求由同一個(gè)異步處理線程來(lái)完成绪爸。而異步處理線程又是同步地、一個(gè)一個(gè)地去處理這些請(qǐng)求宙攻。所以奠货,對(duì)于底層的IO調(diào)度器來(lái)說(shuō),它一次只能看到一個(gè)請(qǐng)求座掘。處理完這個(gè)請(qǐng)求递惋,異步處理線程才會(huì)提交下一個(gè)。而內(nèi)核實(shí)現(xiàn)的異步IO溢陪,則是直接將所有請(qǐng)求都提交給了IO調(diào)度器萍虽,IO調(diào)度器能看到所有的請(qǐng)求。請(qǐng)求多了形真,IO調(diào)度器使用的類電梯算法就能發(fā)揮更大的功效杉编。請(qǐng)求少了,極端情況下(比如系統(tǒng)中的IO請(qǐng)求都集中在同一個(gè)fd上,并且不使用預(yù)讀)王财,IO調(diào)度器總是只能看到一個(gè)請(qǐng)求卵迂,那么電梯算法將退化成先來(lái)先服務(wù)算法,可能會(huì)極大的增加碰頭移動(dòng)的開銷绒净。
最后见咒,glibc版本的異步IO支持非direct-io,可以利用內(nèi)核提供的page cache來(lái)提高效率挂疆。而linux版本只支持direct-io改览,cache的工作就只能靠用戶程序來(lái)實(shí)現(xiàn)了。
Linux aio是Linux下的異步讀寫模型缤言。
對(duì)于文件的讀寫宝当,即使以O(shè)_NONBLOCK方式來(lái)打開一個(gè)文件,也會(huì)處于"阻塞"狀態(tài)胆萧。因?yàn)槲募r(shí)時(shí)刻刻處于可讀狀態(tài)庆揩。而從磁盤到內(nèi)存所等待的時(shí)間是驚人的。為了充份發(fā)揮把數(shù)據(jù)從磁盤復(fù)制到內(nèi)存的時(shí)間跌穗,引入了aio模型订晌。linux下有aio封裝,但是aio采用的是線程或信號(hào)用以通知蚌吸,為了能更多的控制io行為锈拨,可以使用更為低級(jí)libaio。
一羹唠、基本函數(shù)與結(jié)構(gòu)
1. libaio函數(shù)
extern int io_setup(int maxevents, io_context_t *ctxp);
extern int io_destroy(io_context_t ctx);
extern int io_submit(io_context_t ctx, long nr, struct iocb *ios[]);
extern int io_cancel(io_context_t ctx, struct iocb *iocb, struct io_event *evt);
extern int io_getevents(io_context_t ctx_id, long min_nr, long nr, struct io_event *events, struct timespec *timeout);
2. 結(jié)構(gòu)
struct io_iocb_poll {
PADDED(int events, __pad1);
}; /* result code is the set of result flags or -'ve errno */
struct io_iocb_sockaddr {
struct sockaddr *addr;
int len;
}; /* result code is the length of the sockaddr, or -'ve errno */
struct io_iocb_common {
PADDEDptr(void *buf, __pad1);
PADDEDul(nbytes, __pad2);
long long offset;
long long __pad3;
unsigned flags;
unsigned resfd;
}; /* result code is the amount read or -'ve errno */
struct io_iocb_vector {
const struct iovec *vec;
int nr;
long long offset;
}; /* result code is the amount read or -'ve errno */
struct iocb {
PADDEDptr(void *data, __pad1); /* Return in the io completion event */
PADDED(unsigned key, __pad2); /* For use in identifying io requests */
short aio_lio_opcode;
short aio_reqprio;
int aio_fildes;
union {
struct io_iocb_common c;
struct io_iocb_vector v;
struct io_iocb_poll poll;
struct io_iocb_sockaddr saddr;
} u;
};
struct io_event {
PADDEDptr(void *data, __pad1);
PADDEDptr(struct iocb *obj, __pad2);
PADDEDul(res, __pad3);
PADDEDul(res2, __pad4);
};
3. 內(nèi)聯(lián)函數(shù)
static inline void io_set_callback(struct iocb *iocb, io_callback_t cb);
static inline void io_prep_pread(struct iocb *iocb, int fd, void *buf, size_t count, long long offset);
static inline void io_prep_pwrite(struct iocb *iocb, int fd, void *buf, size_t count, long long offset);
static inline void io_prep_preadv(struct iocb *iocb, int fd, const struct iovec *iov, int iovcnt, long long offset);
static inline void io_prep_pwritev(struct iocb *iocb, int fd, const struct iovec *iov, int iovcnt, long long offset);
/* Jeff Moyer says this was implemented in Red Hat AS2.1 and RHEL3.
* AFAICT, it was never in mainline, and should not be used. --RR */
static inline void io_prep_poll(struct iocb *iocb, int fd, int events);
static inline int io_poll(io_context_t ctx, struct iocb *iocb, io_callback_t cb, int fd, int events);
static inline void io_prep_fsync(struct iocb *iocb, int fd);
static inline int io_fsync(io_context_t ctx, struct iocb *iocb, io_callback_t cb, int fd);
static inline void io_prep_fdsync(struct iocb *iocb, int fd);
static inline int io_fdsync(io_context_t ctx, struct iocb *iocb, io_callback_t cb, int fd);
static inline void io_set_eventfd(struct iocb *iocb, int eventfd);
二奕枢、使用方法
1、初使化io_context
2佩微、open文件取得fd
3缝彬、根據(jù)fd,buffer offset等息建立iocb
4喊衫、submit iocb到context
5跌造、io_getevents取得events狀態(tài)
6、回到3步
三族购、例子
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <error.h>
#include <errno.h>
#include <fcntl.h>
#include <libaio.h>
int main(int argc, char *argv[])
{
// 每次讀入32K字節(jié)
const int buffer_size = 0x8000;
// 最大事件數(shù) 32
const int nr_events = 32;
int rt;
io_context_t ctx = {0};
// 初使化 io_context_t
rt = io_setup(nr_events, &ctx);
if ( rt != 0 )
error(1, rt, "io_setup");
// 依次讀取參數(shù)作為文件名加入提交到ctx
int pagesize = sysconf(_SC_PAGESIZE);
for (int i=1; i<argc; ++i) {
iocb *cb = (iocb*)malloc(sizeof(iocb));
void *buffer;
// 要使用O_DIRECT, 必須要對(duì)齊
posix_memalign(&buffer, pagesize, buffer_size);
io_prep_pread(cb, open(argv[i], O_RDONLY | O_DIRECT), buffer, buffer_size, 0);
rt = io_submit(ctx, 1, &cb);
if (rt < 0)
error(1, -rt, "io_submit %s", argv[i]);;
}
io_event events[nr_events];
iocb *cbs[nr_events];
int remain = argc - 1;
int n = 0;
// 接收數(shù)據(jù)最小返回的請(qǐng)求數(shù)為1壳贪,最大為nr_events
while (remain && (n = io_getevents(ctx, 1, nr_events, events, 0))) {
int nr_cbs = 0;
for (int i=0; i<n; ++i) {
io_event &event = events[i];
iocb *cb = event.obj;
// event.res為unsigned
//printf("%d receive %d bytes\n", cb->aio_fildes, event.res);
if (event.res > buffer_size) {
printf("%s\n", strerror(-event.res));
}
if (event.res != buffer_size || event.res2 != 0) {
--remain;
// 釋放buffer, fd 與 cb
free(cb->u.c.buf);
close(cb->aio_fildes);
free(cb);
} else {
// 更新cb的offset
cb->u.c.offset += event.res;
cbs[nr_cbs++] = cb;
}
}
if (nr_cbs) {
// 繼續(xù)接收數(shù)據(jù)
io_submit(ctx, nr_cbs, cbs);
}
}
return 0;
}
運(yùn)行
$ truncate foo.txt -s 100K
$ truncate foo2.txt -s 200K
$ g++ -O3 libaio_simple.cc -laio && ./a.out foo.txt foo2.txt
3 received 32768 bytes
4 received 32768 bytes
3 received 32768 bytes
4 received 32768 bytes
3 received 32768 bytes
4 received 32768 bytes
3 received 4096 bytes
3 done.
4 received 32768 bytes
4 received 32768 bytes
4 received 32768 bytes
4 received 8192 bytes
4 done.
四、AIO與epoll
在使用AIO時(shí)寝杖,需要通過(guò)系統(tǒng)調(diào)用<tt>io_getevents</tt><tt>獲取已經(jīng)完成的</tt><tt>IO</tt><tt>事件违施,而</tt>系統(tǒng)調(diào)用<tt>io_getevents</tt><tt>是阻塞的,所以有</tt><tt>2</tt><tt>種方式:</tt><tt>(1)</tt><tt>使用多線程瑟幕,用專門的線程調(diào)用</tt><tt>io_getevents</tt><tt>磕蒲,參考</tt><tt>MySQL5.5</tt><tt>及以上版本留潦;</tt><tt>(2)</tt><tt>對(duì)于單線程程序,可以通過(guò)</tt><tt>epoll</tt><tt>來(lái)使用</tt><tt>AIO</tt><tt>辣往;不過(guò)兔院,這需要系統(tǒng)調(diào)用</tt><tt>eventfd</tt><tt>的支持,而該系統(tǒng)調(diào)用只在</tt><tt>2.6.22</tt><tt>之后的內(nèi)核才支持站削。</tt>
eventfd 是 Linux-native aio 其中的一個(gè) API坊萝,用來(lái)生成 file descriptors,這些 file descriptors 可為應(yīng)用程序提供更高效 “等待/通知” 的事件機(jī)制许起。和 pipe 作用相似十偶,但比 pipe 更好,一方面它只用到一個(gè) file descriptor(pipe 要用兩個(gè))园细,節(jié)省了內(nèi)核資源惦积;另一方面,eventfd 的緩沖區(qū)管理要簡(jiǎn)單得多猛频,pipe 需要不定長(zhǎng)的緩沖區(qū)狮崩,而 eventfd 全部緩沖只有定長(zhǎng) 8 bytes。
關(guān)于AIO與epoll的結(jié)合鹿寻,請(qǐng)參考:
nginx 0.8.x穩(wěn)定版對(duì)linux aio的支持(http://www.pagefault.info/?p=76)
五厉亏、AIO與direct IO
AIO需要與direct IO結(jié)合。
關(guān)于direct IO的簡(jiǎn)單實(shí)現(xiàn)烈和,可以參考:
Linux 中直接 I/O 機(jī)制的介紹
http://www.ibm.com/developerworks/cn/linux/l-cn-directio/index.html
文章轉(zhuǎn)載自:
https://www.cnblogs.com/skyofbitbit/p/3655981.html
https://blog.csdn.net/brucexu1978/article/details/7085924