異步IO簡(jiǎn)析

什么是異步IO

《UNIX網(wǎng)絡(luò)編程卷1》中的IO多路復(fù)章節(jié)總結(jié)了幾種典型IO模型,包括:

  • 阻塞IO
  • 非阻塞IO
  • IO復(fù)用
  • 信號(hào)驅(qū)動(dòng)式IO
  • 異步IO

這些IO模型在本質(zhì)上都是圍繞著同步、異步、阻塞、非阻塞這幾個(gè)特點(diǎn)在做一些不同的選擇。IO的過(guò)程是應(yīng)用程序從某個(gè)設(shè)備讀取數(shù)據(jù),或者往設(shè)備寫入數(shù)據(jù)。操作系統(tǒng)把這些設(shè)備抽象為描述符fd,應(yīng)用程序則在這些fd上面進(jìn)行讀寫操作。由于fd的底層是設(shè)備,這里就會(huì)有個(gè)問(wèn)題:設(shè)備還沒(méi)有準(zhǔn)備好數(shù)據(jù)的讀寫,比如網(wǎng)卡還沒(méi)有收到數(shù)據(jù),此時(shí)如果應(yīng)用程序去讀相應(yīng)的fd,肯定是沒(méi)有數(shù)據(jù)的。那么當(dāng)遇到這種情況時(shí),應(yīng)用程序應(yīng)該如何反應(yīng)呢,有幾個(gè)選擇:

  • 阻塞:應(yīng)用程序一直等待數(shù)據(jù)ready,然后返回
  • 非阻塞:應(yīng)用程序立即返回,去跑跑其他邏輯,然后定期來(lái)看下數(shù)據(jù)是否ready

此外,即使設(shè)備中已經(jīng)有數(shù)據(jù),操作系統(tǒng)還需將數(shù)據(jù)從內(nèi)核拷貝到用戶的緩存,這也需要一些時(shí)間,具體長(zhǎng)短和用戶設(shè)定的讀取數(shù)據(jù)量大小有關(guān)。換言之,一次IO操作可能是比較耗時(shí)的,那么是否有必要一直等待IO完成,或者,是否有必要定期去檢查數(shù)據(jù)是否ready呢。顯然不是必須的,因此這里又有了同步、異步的概念:

  • 同步:同步和阻塞的意思是一樣的, 每次發(fā)起IO請(qǐng)求后,等待完成才返回
  • 異步:發(fā)起IO請(qǐng)求后立即返回,等到內(nèi)核將IO完成后,才以某種形式通知應(yīng)用

異步IO的核心在于:應(yīng)用程序不需要花費(fèi)時(shí)間在IO上,只需要提交一個(gè)IO操作,當(dāng)內(nèi)核執(zhí)行這個(gè)IO操作時(shí),應(yīng)用可以去運(yùn)行其他邏輯,也不需要定期去查看IO是否完成,當(dāng)內(nèi)核完成這個(gè)IO操作后會(huì)以某種方式通知應(yīng)用。

內(nèi)核通知應(yīng)用的方式其實(shí)并不多,上面說(shuō)的信號(hào)驅(qū)動(dòng)IO,就是內(nèi)核將數(shù)據(jù)準(zhǔn)備好之后,用信號(hào)的方式通知應(yīng)用。但是信號(hào)這種方式會(huì)打亂應(yīng)用程序的執(zhí)行流,讓邏輯變得混亂,在實(shí)際中使用的很少。另外一種通知的方式是讓應(yīng)用主動(dòng)來(lái)詢問(wèn),例如現(xiàn)有的io_getevents系統(tǒng)調(diào)用,它可以讓應(yīng)用知道到現(xiàn)在是否已經(jīng)完成了IO操作。

當(dāng)使用特定的參數(shù)時(shí),io_getevents會(huì)阻塞直到指定的IO操作全部完成。這看起來(lái)似乎又變成了阻塞IO的樣子,但實(shí)際上有些區(qū)別,一個(gè)重要的不同在于:應(yīng)用可以同時(shí)提交多個(gè)IO請(qǐng)求,然后在一個(gè)io_getevents中等待他們?nèi)客瓿?。這個(gè)和IO多路復(fù)用的機(jī)制很相似,從應(yīng)用的角度看,就好像執(zhí)行一個(gè)批量的操作,這顯然是能夠提升效率的。

總結(jié)一下,異步IO的基本邏輯是:應(yīng)用提交一些IO操作到內(nèi)核,然后不需要去關(guān)注這些IO,等到適當(dāng)?shù)臅r(shí)機(jī),或者內(nèi)核發(fā)信號(hào)給應(yīng)用,或者應(yīng)用主動(dòng)詢問(wèn)內(nèi)核,來(lái)獲取到IO操作的執(zhí)行是否完成。

為什么需要異步IO

在理想的情況下,運(yùn)行中的程序會(huì)盡可能發(fā)揮硬件的能力,包括CPU的計(jì)算能力以及存儲(chǔ)設(shè)備的IO能力,來(lái)獲得最好的性能。在近幾年,存儲(chǔ)設(shè)備的IO性能提升很快,如果還是使用之前的阻塞IO模式,設(shè)備的能力會(huì)得不到發(fā)揮,這和我們程序運(yùn)行的初衷是相悖的。也就是說(shuō),在硬件設(shè)備相同的條件下,我們需要盡力改善代碼,來(lái)獲取更好的性能。事實(shí)上,很多東西都在做這樣的事情,比如協(xié)程、事件驅(qū)動(dòng)這些機(jī)制,本質(zhì)上都是在盡可能的提升程序的執(zhí)行效率。

那么異步IO是怎么提高性能的呢?上面說(shuō)到,異步IO的本質(zhì)就是應(yīng)用將一批IO提交給內(nèi)核,然后就不用管了,可以去做其他事情。這個(gè)過(guò)程對(duì)性能的提升體現(xiàn)在兩個(gè)地方:

  • 應(yīng)用不再阻塞在IO這里,由內(nèi)核來(lái)操作IO,應(yīng)用可以執(zhí)行其他邏輯,此時(shí)應(yīng)用的運(yùn)行和IO執(zhí)行變成了并行的關(guān)系
  • 可以批量的進(jìn)行IO操作,讓設(shè)備的能力得到最大發(fā)揮

這里有也有值得商榷的地方:1,是不是真正的在并行執(zhí)行,如果cpu資源有限,應(yīng)用線程和內(nèi)核線程不能同時(shí)在各自的cpu核心上運(yùn)行,那么其實(shí)也不是并行(從設(shè)備拷貝數(shù)據(jù)到內(nèi)核可能不需要cpu參與,只要硬件就夠了,但是從內(nèi)核往用戶控件拷貝是肯定需要內(nèi)核線程來(lái)操作的)。2,批量提交IO給內(nèi)核,是不是這個(gè)量越大越好。這些都需要看具體的情況。

有哪些異步IO的實(shí)現(xiàn)

現(xiàn)有的異步IO實(shí)現(xiàn)主要包括兩個(gè):

  • 以aio_為前綴的一系列函數(shù),包括 aio_read,aio_write, aio_suspend等,這個(gè)異步IO的實(shí)現(xiàn)是在用戶態(tài)使用線程池實(shí)現(xiàn)的,性能不怎么樣,它只是暴露出異步IO風(fēng)格的接口。
  • libaio包提供的系列函數(shù),libaio是包裝在io_setp,io_submit等系統(tǒng)調(diào)用上的lib,這個(gè)一套正兒八經(jīng)在內(nèi)核實(shí)現(xiàn)的異步IO機(jī)制。

第一個(gè)這里就不說(shuō)了,第二個(gè)libaio也存在很多問(wèn)題,導(dǎo)致沒(méi)有沒(méi)廣泛的應(yīng)用,主要的缺陷如下:

  • 只能支持O_DIRECT模式,也就是沒(méi)有緩沖的讀寫
  • 只能支持ext2, ext3, jfs, xfs文件系統(tǒng)
  • 不支持fsync
  • 不支持socket
  • 不支持管道pipes
  • api設(shè)計(jì)的不夠好:一次IO需要至少兩次系統(tǒng)調(diào)用;submit + completion一共需要拷貝104字節(jié)數(shù)據(jù),本來(lái)應(yīng)該是0拷貝的(這個(gè)量不大,可能也不是一個(gè)嚴(yán)重的問(wèn)題);此外,api很難使用正確

由于這些原因,libaio只在一些底層軟件如數(shù)據(jù)庫(kù)中有被使用,大多數(shù)普通的應(yīng)用都沒(méi)有使用libaio。

io_uring

在linux5.1以后,內(nèi)核引入了一種全新的異步IO機(jī)制,也就是io_uring。io_uring基本上克服了上述aio的各種缺陷,它的主要特性如下:

  • 支持O_DIRECT以及非O_DIRECT模式的文件讀寫,并能夠支持在各種類型的fd上操作,包括文件、網(wǎng)絡(luò)
  • 高性能,相比于舊的aio,省去了讀書數(shù)據(jù)的拷貝,減少必須的系統(tǒng)調(diào)用次數(shù)
  • 豐富的特性,包括fixed buffer,polled IO等
  • 簡(jiǎn)單易用的api接口

ring buffer

io_uring的最大特色在于對(duì)性能的提升,它通過(guò)讓用戶態(tài)的應(yīng)用和內(nèi)核共享數(shù)據(jù)結(jié)構(gòu)來(lái)實(shí)現(xiàn)這一點(diǎn),這個(gè)數(shù)據(jù)結(jié)構(gòu)就是ring buffer,這也是io_uring名字的由來(lái)。

上面講過(guò),異步IO的基本邏輯是應(yīng)用提交IO請(qǐng)求到內(nèi)核,內(nèi)核執(zhí)行這些IO請(qǐng)求,然后應(yīng)用再以某種方式來(lái)獲取到IO執(zhí)行完成的情況。很明顯這個(gè)過(guò)程需要應(yīng)用和內(nèi)核交換信息,應(yīng)用需要告訴內(nèi)核有一個(gè)新的IO請(qǐng)求到來(lái),內(nèi)核需要告訴應(yīng)用某個(gè)IO已經(jīng)完成。之前的異步IO做法是讓應(yīng)用通過(guò)系統(tǒng)調(diào)用來(lái)獲取這些信息,但是系統(tǒng)調(diào)用是一個(gè)相對(duì)較重的操作,它需要中斷當(dāng)前的進(jìn)程,保持上下文,陷入內(nèi)核,執(zhí)行相應(yīng)邏輯后再返回。io_uring的做法是直接讓應(yīng)用和內(nèi)核共享兩個(gè)ring buffer,一個(gè)是submission ring,一個(gè)completion ring,這兩個(gè)ring以queue的形式工作,應(yīng)用和內(nèi)核通過(guò)訪問(wèn)這兩個(gè)ring來(lái)獲取需要的信息。對(duì)于SQ(submission queue)來(lái)說(shuō),應(yīng)用是生產(chǎn)者,內(nèi)核是消費(fèi)者;對(duì)于CQ(completion queue)則是相反的。

在多線程中共享數(shù)據(jù)結(jié)構(gòu)時(shí),必須要做好同步的工作,因?yàn)檫@里有競(jìng)爭(zhēng)條件。類似的,當(dāng)應(yīng)用和內(nèi)核共享數(shù)據(jù)結(jié)構(gòu)時(shí),也需要做同步。一般的做法是使用互斥鎖,但是由于這里應(yīng)用是和內(nèi)核在共享數(shù)據(jù),如果使用鎖,則必須要使用某種形式的系統(tǒng)調(diào)用。一方面,互斥鎖對(duì)性能是有損耗的,另外,系統(tǒng)調(diào)用也是要避免的。io_uring的做法是使用memory ordering來(lái)避免出現(xiàn)數(shù)據(jù)不一致(在多核心的cpu架構(gòu)中,每個(gè)核都有自己的多級(jí)緩存,線程只會(huì)在一個(gè)核心上運(yùn)行,當(dāng)某個(gè)線程連續(xù)更改了內(nèi)存中某個(gè)變量的值,運(yùn)行在其他核心上的線程的緩存需要做相應(yīng)更新,此時(shí)其他線程可能會(huì)看到這些變量的變更順序和發(fā)起更改的順序不一致,memory ordering主要是用于防止這種現(xiàn)象,也就是它可以保證所有線程看到相同的變更順序)。在使用ring buffer當(dāng)做queue的這種場(chǎng)景下,生產(chǎn)和消費(fèi)都是通過(guò)修改相應(yīng)的head、tail值來(lái)進(jìn)行的,使用memory ordering能夠保證兩邊看到的數(shù)據(jù)是一致的。 相比于使用互斥鎖,memory ordering的效率更高。

高級(jí)特性

  • FIXED FILES:在每次提交一個(gè)IO請(qǐng)求后,內(nèi)核會(huì)獲取這個(gè)IO請(qǐng)求中fd的一個(gè)引用,在使用完成后釋放這個(gè)引用。在高IOPS、同時(shí)操作的文件基本不變的情況下,這個(gè)過(guò)程會(huì)有顯著的性能損耗。io_uring支持使用一個(gè)或一組固定的fd,這樣避免了內(nèi)核頻繁的創(chuàng)建和銷毀fd的應(yīng)用

  • FIXED BUFFER:在使用O_DIRECT模式的IO時(shí),內(nèi)核會(huì)將用戶給的緩沖區(qū)map到內(nèi)核的內(nèi)存地址,然后在上面做讀寫,完成后再unmap這些地址。這個(gè)是比較昂貴的操作,為了避免頻繁額map和unmap,io_uring提供了FIXED BUFFER的能力,即可以讓應(yīng)用重復(fù)的使用同一塊已經(jīng)映射好的緩沖區(qū)。

  • POLLED IO:這種模式下,應(yīng)用使用輪詢的方式來(lái)查詢IO完成情況,應(yīng)用會(huì)不停的詢問(wèn)硬件驅(qū)動(dòng),相應(yīng)的IO是否已經(jīng)完成,從而避免了由硬件設(shè)備中斷來(lái)告知IO已經(jīng)完成。對(duì)于IOPS高的應(yīng)用來(lái)說(shuō),頻繁的硬件中斷會(huì)帶來(lái)很多效率上的損耗。polled io這種模式適合用在IOPS高,硬件性能高的場(chǎng)景,能夠有效提升應(yīng)用的性能。

  • KERNAL SIDE POLLING:內(nèi)核側(cè)的輪詢模式,使用這種方式,在應(yīng)用提交一個(gè)IO操作到submission queue后,不需要通過(guò)系統(tǒng)調(diào)用來(lái)告訴內(nèi)核有一個(gè)新的IO請(qǐng)求到來(lái)。內(nèi)核中會(huì)有一個(gè)專職的線程關(guān)注submission queue,一旦有新的entry,會(huì)立即處理它。

io_uring和io多路復(fù)用在用法上比較類似,都是先提交一些數(shù)據(jù),然后等待相應(yīng)的事件發(fā)生,由于io_uring能夠支持各種設(shè)備的IO,包括文件、網(wǎng)絡(luò),現(xiàn)在似乎可以使用io_uring將網(wǎng)絡(luò)、文件讀寫都統(tǒng)一起來(lái)。但實(shí)際上,io_uring和epoll還是有些差別的,io_uring最多能夠提交4096個(gè)IO請(qǐng)求到submission queue中,epoll則可以同時(shí)管理數(shù)以百萬(wàn)的連接,僅這一點(diǎn)就使得io_uring不可能代替epoll。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容