圖文詳解io_uring高性能異步IO架構(gòu)(原理篇)
說到高性能網(wǎng)絡(luò)編程,我們第一時間想到的是epoll機(jī)制,epoll很長一段時間統(tǒng)治著整個網(wǎng)絡(luò)編程江湖,然而io_uring的出現(xiàn),似乎在撼動epoll的統(tǒng)治地位,今天我們來揭開io_uring的神秘面紗。
1.io_uring簡介
io_uring是一個Linux內(nèi)核的異步I/O框架,它提供了高性能的異步I/O操作,io_uring的目標(biāo)是通過減少系統(tǒng)調(diào)用和上下文切換的開銷來提高I/O操作的性能。
io_uring通過使用環(huán)形緩沖區(qū)和事件驅(qū)動的方式來實(shí)現(xiàn)高效的異步I/O操作。
io_uring的設(shè)計(jì)使得應(yīng)用程序可以同時處理大量的I/O操作,從而提高系統(tǒng)的吞吐量和響應(yīng)速度。
2.io_uring實(shí)現(xiàn)原理
io_uring整體架構(gòu)如下:
圖片
2.1基礎(chǔ)概念
- SQE:提交隊(duì)列項(xiàng),表示IO請求。
- CQE:完成隊(duì)列項(xiàng),表示IO請求結(jié)果。
- SQ:Submission Queue,提交隊(duì)列,用于存儲SQE的數(shù)組。
- CQ:Completion Queue,完成隊(duì)列,用于存儲CQE的數(shù)組。
- SQ Ring:SQ環(huán)形緩沖區(qū),包含SQ,頭部索引(head),尾部索引(tail),隊(duì)列大小等信息。
- CQ Ring:CQ環(huán)形緩沖區(qū),包含SQ,頭部索引(head),尾部索引(tail),隊(duì)列大小等信息。
- SQ線程:內(nèi)核輔助線程,用于從SQ隊(duì)列獲取SQE,并提交給內(nèi)核處理,并將IO請求結(jié)果生成CQE存儲在CQ隊(duì)列。
2.2 io_uring系統(tǒng)調(diào)用
- io_uring_setup():用于初始化io_uring環(huán)境,創(chuàng)建io_uring實(shí)例。
- io_uring_enter():用于提交和等待io_uring操作的系統(tǒng)調(diào)用,可以指定提交的操作數(shù)量和等待的超時時間。
- io_uring_register():用于注冊文件描述符或事件文件描述符到io_uring實(shí)例中,以便進(jìn)行I/O操作。
2.3 liburing庫
liburing是一個用于Linux的用戶空間庫,用于利用io_uring接口進(jìn)行高性能的異步I/O操作,它提供了一組函數(shù)和數(shù)據(jù)結(jié)構(gòu),使開發(fā)者能夠更方便地使用io_uring接口。
- io_uring_queue_init:初始化一個io_uring隊(duì)列。
- io_uring_register:將文件描述符注冊到io_uring隊(duì)列中。
- io_uring_prep_read:準(zhǔn)備一個讀取操作。
- io_uring_prep_write:準(zhǔn)備一個寫入操作。
- io_uring_submit:提交一個或多個操作到io_uring隊(duì)列中。
- io_uring_wait_cqe:等待一個完成的操作。
- io_uring_cqe_seen:標(biāo)記一個完成的操作已經(jīng)被處理。
- io_uring_queue_exit:關(guān)閉并釋放io_uring隊(duì)列。
2.4 工作流程
- 創(chuàng)建io_uring對象:首先,需要創(chuàng)建一個io_uring對象,可以使用io_uring_setup()函數(shù)來完成。
- 準(zhǔn)備I/O請求:在進(jìn)行I/O操作之前,需要準(zhǔn)備相關(guān)的I/O請求??梢允褂胕o_uring_prep_XXX()系列函數(shù)來準(zhǔn)備不同類型的I/O請求,例如io_uring_prep_read()用于讀取數(shù)據(jù),io_uring_prep_write()用于寫入數(shù)據(jù)。
- 提交I/O請求:準(zhǔn)備好I/O請求后,可以使用io_uring_submit()函數(shù)將請求提交給內(nèi)核,內(nèi)核會將這些請求放入一個隊(duì)列中,等待執(zhí)行。
- 等待IO請求完成:可以使用io_uring_wait_cqe()函數(shù)來等待I/O請求的完成,一旦請求完成,內(nèi)核會將完成事件放入一個完成隊(duì)列中。
- 獲取IO請求結(jié)果:可以使用io_uring_peek_cqe()函數(shù)來獲取完成隊(duì)列中的完成事件。然后,可以通過事件的信息來處理完成的I/O請求,例如讀取數(shù)據(jù)或者處理錯誤。
- 釋放IO請求結(jié)果:獲取完IO請求結(jié)果,使用io_uring_cqe_seen()函數(shù)來釋放IO請求結(jié)果,以便內(nèi)核可以繼續(xù)使用。
- 重復(fù)執(zhí)行:可以重復(fù)執(zhí)行上述步驟,以處理更多的I/O請求。
3.內(nèi)核實(shí)現(xiàn)
3.1 創(chuàng)建io_uring對象
圖片
用戶程序通過io_uring_setup系統(tǒng)調(diào)用創(chuàng)建和初始化io_uring對象,io_uring對象對應(yīng)于struct io_ring_ctx結(jié)構(gòu)體對象。
io_uring_setup主要工作:
- 創(chuàng)建struct io_ring_ctx對象并初始化。
- 創(chuàng)建struct io_urings對象并初始化,注意此時已完成CQ和所有CQE創(chuàng)建。
- 創(chuàng)建SQ和所有SQE并初始化。
- 如果struct io_ring_ctx對象flags參數(shù)設(shè)置IORING_SETUP_SQPOLL,則創(chuàng)建SQ線程。
3.2 fd綁定io_uring對象
圖片
已創(chuàng)建的io_ring對象需要和fd進(jìn)行綁定, 以便能夠通過fd找到io_uring對象,創(chuàng)建一個新的file,file private_data成員指向io_ring對象,申請一個未使用的文件描述符fd,fd映射至file,并存儲在進(jìn)程已打開文件表中。
注意:mmap內(nèi)存映射需要用到該fd。
3.3 io_uring對象內(nèi)存映射
圖片
通過io_uring_setup系統(tǒng)調(diào)用創(chuàng)建完io_uring對象后,用戶程序還不能直接訪問io_uring對象,此時用戶程序需要通過mmap函數(shù)將io_uring對象SQ,CQ以及head和tail等相關(guān)內(nèi)存空間映射出來。
完成mmap內(nèi)存映射后,io_uring對象相關(guān)內(nèi)存空間成為用戶程序和內(nèi)核共享內(nèi)存空間,用戶程序可以直接訪問io_uring對象,不再需要通過執(zhí)行系統(tǒng)調(diào)用訪問,很大程度上提高了系統(tǒng)性能。
3.4 提交IO請求
圖片
SQ Ring中有兩個成員head(頭部索引)和tail(尾部索引),頭部索引指向SQ隊(duì)列第一個已提交IO請求,尾部索引指向SQ下一個空閑SQE。
提交IO請求,只需要將tail指向的SQE填充IO請求信息,并讓tail自增1,指向下一個空閑SQE。
注意:head和tail不是直接指向SQ數(shù)組,而是需要通過head&mask和tail &mask操作指向SQ數(shù)組,mask數(shù)組為數(shù)組長度減1,因?yàn)閿?shù)組有固定大小,所以需要通過&mask方式防止越界訪問數(shù)組,這種方式可以讓數(shù)組形成一個環(huán)形緩沖區(qū)。
3.5 等待IO請求完成
圖片
IO請求的處理有兩種方式:
- 方式1:SQ線程從SQ隊(duì)列中獲取SQE(已提交IO請求),并發(fā)送給內(nèi)核處理。
- 方式2:用戶程序通過io_uring_enter系統(tǒng)調(diào)用從SQ隊(duì)列中獲取SQE(已提交IO請求),并發(fā)送給內(nèi)核處理。
從SQ隊(duì)列獲取SQE只需要獲取SQ Ring head指向的SQE,并讓head自增指向下一個SQE即可。
圖片
內(nèi)核處理完IO請求后,SQ線程會申請CQ Ring tail指向的CQE存儲IO請求結(jié)果,tail自增1指向下一個空閑CQE。
3.6 獲取IO請求結(jié)果
圖片
用戶程序通過判斷CQ Ring head和tail之間的差值,可以檢測到是否有已完成IO請求,如果有已完成IO請求(CQE),獲取CQ Ring head指向CQE,獲取IO請求結(jié)果。
3.7 釋放已完成IO請求
釋放已完成IO請求只需要將CQ Ring head指針自增1指向下一個CQE即可,這樣做的目的是防止重復(fù)獲取IO請求結(jié)果。
io_uring為什么高效?
核心原因:io_uring通過mmap內(nèi)存映射大大減少了系統(tǒng)調(diào)用,在高并發(fā)場景下,系統(tǒng)調(diào)用非常損耗系統(tǒng)性能。
其他原因:
- 減少拷貝:io_uring通過共享內(nèi)存減少用戶程序和內(nèi)核數(shù)據(jù)拷貝。
- 批量操作:io_uring支持批量操作,一次性可以提交多個I/O請求,減少系統(tǒng)調(diào)用的次數(shù),提高系統(tǒng)效率。
- 無鎖環(huán)形隊(duì)列:io_uring采用無鎖隊(duì)列實(shí)現(xiàn)用戶程序與內(nèi)核對共享內(nèi)存的高效訪問。