linux异步io底层原理,异步IO简析
什么是異步IO
《UNIX網(wǎng)絡(luò)編程卷1》中的IO多路復(fù)章節(jié)總結(jié)了幾種典型IO模型,包括:
阻塞IO
非阻塞IO
IO復(fù)用
信號驅(qū)動式IO
異步IO
這些IO模型在本質(zhì)上都是圍繞著同步、異步、阻塞、非阻塞這幾個特點在做一些不同的選擇。IO的過程是應(yīng)用程序從某個設(shè)備讀取數(shù)據(jù),或者往設(shè)備寫入數(shù)據(jù)。操作系統(tǒng)把這些設(shè)備抽象為描述符fd,應(yīng)用程序則在這些fd上面進行讀寫操作。由于fd的底層是設(shè)備,這里就會有個問題:設(shè)備還沒有準備好數(shù)據(jù)的讀寫,比如網(wǎng)卡還沒有收到數(shù)據(jù),此時如果應(yīng)用程序去讀相應(yīng)的fd,肯定是沒有數(shù)據(jù)的。那么當遇到這種情況時,應(yīng)用程序應(yīng)該如何反應(yīng)呢,有幾個選擇:
阻塞:應(yīng)用程序一直等待數(shù)據(jù)ready,然后返回
非阻塞:應(yīng)用程序立即返回,去跑跑其他邏輯,然后定期來看下數(shù)據(jù)是否ready
此外,即使設(shè)備中已經(jīng)有數(shù)據(jù),操作系統(tǒng)還需將數(shù)據(jù)從內(nèi)核拷貝到用戶的緩存,這也需要一些時間,具體長短和用戶設(shè)定的讀取數(shù)據(jù)量大小有關(guān)。換言之,一次IO操作可能是比較耗時的,那么是否有必要一直等待IO完成,或者,是否有必要定期去檢查數(shù)據(jù)是否ready呢。顯然不是必須的,因此這里又有了同步、異步的概念:
同步:同步和阻塞的意思是一樣的, 每次發(fā)起IO請求后,等待完成才返回
異步:發(fā)起IO請求后立即返回,等到內(nèi)核將IO完成后,才以某種形式通知應(yīng)用
異步IO的核心在于:應(yīng)用程序不需要花費時間在IO上,只需要提交一個IO操作,當內(nèi)核執(zhí)行這個IO操作時,應(yīng)用可以去運行其他邏輯,也不需要定期去查看IO是否完成,當內(nèi)核完成這個IO操作后會以某種方式通知應(yīng)用。
內(nèi)核通知應(yīng)用的方式其實并不多,上面說的信號驅(qū)動IO,就是內(nèi)核將數(shù)據(jù)準備好之后,用信號的方式通知應(yīng)用。但是信號這種方式會打亂應(yīng)用程序的執(zhí)行流,讓邏輯變得混亂,在實際中使用的很少。另外一種通知的方式是讓應(yīng)用主動來詢問,例如現(xiàn)有的io_getevents系統(tǒng)調(diào)用,它可以讓應(yīng)用知道到現(xiàn)在是否已經(jīng)完成了IO操作。
當使用特定的參數(shù)時,io_getevents會阻塞直到指定的IO操作全部完成。這看起來似乎又變成了阻塞IO的樣子,但實際上有些區(qū)別,一個重要的不同在于:應(yīng)用可以同時提交多個IO請求,然后在一個io_getevents中等待他們?nèi)客瓿伞_@個和IO多路復(fù)用的機制很相似,從應(yīng)用的角度看,就好像執(zhí)行一個批量的操作,這顯然是能夠提升效率的。
總結(jié)一下,異步IO的基本邏輯是:應(yīng)用提交一些IO操作到內(nèi)核,然后不需要去關(guān)注這些IO,等到適當?shù)臅r機,或者內(nèi)核發(fā)信號給應(yīng)用,或者應(yīng)用主動詢問內(nèi)核,來獲取到IO操作的執(zhí)行是否完成。
為什么需要異步IO
在理想的情況下,運行中的程序會盡可能發(fā)揮硬件的能力,包括CPU的計算能力以及存儲設(shè)備的IO能力,來獲得最好的性能。在近幾年,存儲設(shè)備的IO性能提升很快,如果還是使用之前的阻塞IO模式,設(shè)備的能力會得不到發(fā)揮,這和我們程序運行的初衷是相悖的。也就是說,在硬件設(shè)備相同的條件下,我們需要盡力改善代碼,來獲取更好的性能。事實上,很多東西都在做這樣的事情,比如協(xié)程、事件驅(qū)動這些機制,本質(zhì)上都是在盡可能的提升程序的執(zhí)行效率。
那么異步IO是怎么提高性能的呢?上面說到,異步IO的本質(zhì)就是應(yīng)用將一批IO提交給內(nèi)核,然后就不用管了,可以去做其他事情。這個過程對性能的提升體現(xiàn)在兩個地方:
應(yīng)用不再阻塞在IO這里,由內(nèi)核來操作IO,應(yīng)用可以執(zhí)行其他邏輯,此時應(yīng)用的運行和IO執(zhí)行變成了并行的關(guān)系
可以批量的進行IO操作,讓設(shè)備的能力得到最大發(fā)揮
這里有也有值得商榷的地方:1,是不是真正的在并行執(zhí)行,如果cpu資源有限,應(yīng)用線程和內(nèi)核線程不能同時在各自的cpu核心上運行,那么其實也不是并行(從設(shè)備拷貝數(shù)據(jù)到內(nèi)核可能不需要cpu參與,只要硬件就夠了,但是從內(nèi)核往用戶控件拷貝是肯定需要內(nèi)核線程來操作的)。2,批量提交IO給內(nèi)核,是不是這個量越大越好。這些都需要看具體的情況。
有哪些異步IO的實現(xiàn)
現(xiàn)有的異步IO實現(xiàn)主要包括兩個:
以aio_為前綴的一系列函數(shù),包括 aio_read,aio_write, aio_suspend等,這個異步IO的實現(xiàn)是在用戶態(tài)使用線程池實現(xiàn)的,性能不怎么樣,它只是暴露出異步IO風格的接口。
libaio包提供的系列函數(shù),libaio是包裝在io_setp,io_submit等系統(tǒng)調(diào)用上的lib,這個一套正兒八經(jīng)在內(nèi)核實現(xiàn)的異步IO機制。
第一個這里就不說了,第二個libaio也存在很多問題,導(dǎo)致沒有沒廣泛的應(yīng)用,主要的缺陷如下:
只能支持O_DIRECT模式,也就是沒有緩沖的讀寫
只能支持ext2, ext3, jfs, xfs文件系統(tǒng)
不支持fsync
不支持socket
不支持管道pipes
api設(shè)計的不夠好:一次IO需要至少兩次系統(tǒng)調(diào)用;submit + completion一共需要拷貝104字節(jié)數(shù)據(jù),本來應(yīng)該是0拷貝的(這個量不大,可能也不是一個嚴重的問題);此外,api很難使用正確
由于這些原因,libaio只在一些底層軟件如數(shù)據(jù)庫中有被使用,大多數(shù)普通的應(yīng)用都沒有使用libaio。
io_uring
在linux5.1以后,內(nèi)核引入了一種全新的異步IO機制,也就是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等
簡單易用的api接口
ring buffer
io_uring的最大特色在于對性能的提升,它通過讓用戶態(tài)的應(yīng)用和內(nèi)核共享數(shù)據(jù)結(jié)構(gòu)來實現(xiàn)這一點,這個數(shù)據(jù)結(jié)構(gòu)就是ring buffer,這也是io_uring名字的由來。
上面講過,異步IO的基本邏輯是應(yīng)用提交IO請求到內(nèi)核,內(nèi)核執(zhí)行這些IO請求,然后應(yīng)用再以某種方式來獲取到IO執(zhí)行完成的情況。很明顯這個過程需要應(yīng)用和內(nèi)核交換信息,應(yīng)用需要告訴內(nèi)核有一個新的IO請求到來,內(nèi)核需要告訴應(yīng)用某個IO已經(jīng)完成。之前的異步IO做法是讓應(yīng)用通過系統(tǒng)調(diào)用來獲取這些信息,但是系統(tǒng)調(diào)用是一個相對較重的操作,它需要中斷當前的進程,保持上下文,陷入內(nèi)核,執(zhí)行相應(yīng)邏輯后再返回。io_uring的做法是直接讓應(yīng)用和內(nèi)核共享兩個ring buffer,一個是submission ring,一個completion ring,這兩個ring以queue的形式工作,應(yīng)用和內(nèi)核通過訪問這兩個ring來獲取需要的信息。對于SQ(submission queue)來說,應(yīng)用是生產(chǎn)者,內(nèi)核是消費者;對于CQ(completion queue)則是相反的。
在多線程中共享數(shù)據(jù)結(jié)構(gòu)時,必須要做好同步的工作,因為這里有競爭條件。類似的,當應(yīng)用和內(nèi)核共享數(shù)據(jù)結(jié)構(gòu)時,也需要做同步。一般的做法是使用互斥鎖,但是由于這里應(yīng)用是和內(nèi)核在共享數(shù)據(jù),如果使用鎖,則必須要使用某種形式的系統(tǒng)調(diào)用。一方面,互斥鎖對性能是有損耗的,另外,系統(tǒng)調(diào)用也是要避免的。io_uring的做法是使用memory ordering來避免出現(xiàn)數(shù)據(jù)不一致(在多核心的cpu架構(gòu)中,每個核都有自己的多級緩存,線程只會在一個核心上運行,當某個線程連續(xù)更改了內(nèi)存中某個變量的值,運行在其他核心上的線程的緩存需要做相應(yīng)更新,此時其他線程可能會看到這些變量的變更順序和發(fā)起更改的順序不一致,memory ordering主要是用于防止這種現(xiàn)象,也就是它可以保證所有線程看到相同的變更順序)。在使用ring buffer當做queue的這種場景下,生產(chǎn)和消費都是通過修改相應(yīng)的head、tail值來進行的,使用memory ordering能夠保證兩邊看到的數(shù)據(jù)是一致的。 相比于使用互斥鎖,memory ordering的效率更高。
高級特性
FIXED FILES:在每次提交一個IO請求后,內(nèi)核會獲取這個IO請求中fd的一個引用,在使用完成后釋放這個引用。在高IOPS、同時操作的文件基本不變的情況下,這個過程會有顯著的性能損耗。io_uring支持使用一個或一組固定的fd,這樣避免了內(nèi)核頻繁的創(chuàng)建和銷毀fd的應(yīng)用
FIXED BUFFER:在使用O_DIRECT模式的IO時,內(nèi)核會將用戶給的緩沖區(qū)map到內(nèi)核的內(nèi)存地址,然后在上面做讀寫,完成后再unmap這些地址。這個是比較昂貴的操作,為了避免頻繁額map和unmap,io_uring提供了FIXED BUFFER的能力,即可以讓應(yīng)用重復(fù)的使用同一塊已經(jīng)映射好的緩沖區(qū)。
POLLED IO:這種模式下,應(yīng)用使用輪詢的方式來查詢IO完成情況,應(yīng)用會不停的詢問硬件驅(qū)動,相應(yīng)的IO是否已經(jīng)完成,從而避免了由硬件設(shè)備中斷來告知IO已經(jīng)完成。對于IOPS高的應(yīng)用來說,頻繁的硬件中斷會帶來很多效率上的損耗。polled io這種模式適合用在IOPS高,硬件性能高的場景,能夠有效提升應(yīng)用的性能。
KERNAL SIDE POLLING:內(nèi)核側(cè)的輪詢模式,使用這種方式,在應(yīng)用提交一個IO操作到submission queue后,不需要通過系統(tǒng)調(diào)用來告訴內(nèi)核有一個新的IO請求到來。內(nèi)核中會有一個專職的線程關(guān)注submission queue,一旦有新的entry,會立即處理它。
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)一起來。但實際上,io_uring和epoll還是有些差別的,io_uring最多能夠提交4096個IO請求到submission queue中,epoll則可以同時管理數(shù)以百萬的連接,僅這一點就使得io_uring不可能代替epoll。
總結(jié)
以上是生活随笔為你收集整理的linux异步io底层原理,异步IO简析的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: [机器学习笔记]Note14--推荐系统
- 下一篇: Linux中防火墙命令笔记