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