深入解剖io_uring:Linux異步IO的終極武器
在 Linux 系統(tǒng)的世界里,I/O 操作的效率,始終是左右系統(tǒng)性能的關鍵因素。從一開始簡單的阻塞式 I/O,到后來的非阻塞 I/O,再到 I/O 多路復用技術的誕生,每一次技術變革,都在不斷突破 I/O 性能的瓶頸。在眾多 I/O 技術中,epoll 曾是高性能 I/O 的代表,它以事件驅(qū)動模式,高效地處理著大量并發(fā)連接,在 Nginx、Redis 等眾多知名項目中扮演著重要角色。但隨著數(shù)據(jù)量的爆炸式增長,以及應用場景日益復雜,epoll 在面對某些極端高并發(fā)場景時,也漸漸顯露出局限性。
就在這時候,io_uring 強勢登場。它作為 Linux 內(nèi)核異步 I/O 領域的革新者,一出現(xiàn)便備受關注。io_uring 的目標,是打破傳統(tǒng)異步 I/O 模型的性能束縛,以顛覆性的設計理念,為 Linux 異步 I/O 領域帶來全新變革。
那么,io_uring 究竟有著怎樣的獨特之處,能讓開發(fā)者們對它寄予厚望?它與 epoll 相比,又在哪些方面實現(xiàn)了重大突破?現(xiàn)在,就讓我們一起深入探究,開啟這場從 epoll 到 io_uring 的技術探索之旅 。
Part1.Linux I/O 的前世今生
在 Linux 系統(tǒng)的發(fā)展歷程中,I/O 模型的演進是提升系統(tǒng)性能和效率的關鍵因素。從早期簡單的阻塞式 I/O 到如今復雜高效的 io_uring,每一次變革都解決了特定場景下的性能瓶頸問題。
1.1阻塞式 I/O(Blocking I/O)
阻塞式 I/O 是最基礎、最直觀的 I/O 模型。在這種模型下,當應用程序執(zhí)行 I/O 操作(如 read 或 write)時,進程會被阻塞,直到 I/O 操作完成。例如,當從文件中讀取數(shù)據(jù)時,如果數(shù)據(jù)尚未準備好,進程就會一直等待,期間無法執(zhí)行其他任務。這就好比你去餐廳點餐,然后一直在餐桌旁等待食物上桌,在等待的過程中什么也做不了。
阻塞式 I/O 的優(yōu)點是編程簡單,邏輯清晰,但是在處理大量并發(fā)請求時,由于每個請求都可能阻塞進程,導致系統(tǒng)的并發(fā)處理能力極低,資源利用率也不高。在高并發(fā)的 Web 服務器場景中,如果使用阻塞式 I/O,每一個客戶端連接都需要一個獨立的線程來處理,當并發(fā)連接數(shù)增多時,線程資源將被大量消耗,系統(tǒng)性能會急劇下降。
1.2非阻塞式 I/O(Non - blocking I/O)
為了解決阻塞式 I/O 的問題,非阻塞式 I/O 應運而生。在非阻塞式 I/O 模型中,當應用程序執(zhí)行 I/O 操作時,如果數(shù)據(jù)尚未準備好,系統(tǒng)不會阻塞進程,而是立即返回一個錯誤(如 EWOULDBLOCK 或 EAGAIN)。應用程序可以繼續(xù)執(zhí)行其他任務,然后通過輪詢的方式再次嘗試 I/O 操作,直到數(shù)據(jù)準備好。
這就像你在餐廳點餐時,服務員告訴你需要等待一段時間,你可以先去做其他事情,然后時不時回來詢問食物是否準備好了。非阻塞式 I/O 提高了系統(tǒng)的并發(fā)處理能力,進程在等待 I/O 操作的過程中可以執(zhí)行其他任務,但是頻繁的輪詢會消耗大量的 CPU 資源,增加了系統(tǒng)的開銷。而且非阻塞式 I/O 的編程復雜度較高,需要處理更多的錯誤和狀態(tài)判斷。
1.3 I/O 多路復用(I/O Multiplexing)
I/O 多路復用是在非阻塞式 I/O 的基礎上進一步發(fā)展而來的,它允許一個進程同時監(jiān)視多個 I/O 描述符(如文件描述符、套接字等),當其中任何一個描述符就緒(即有數(shù)據(jù)可讀或可寫)時,進程就可以對其進行處理。常見的 I/O 多路復用技術有 select、poll 和 epoll。以 select 為例,應用程序通過調(diào)用 select 函數(shù),將需要監(jiān)視的 I/O 描述符集合傳遞給內(nèi)核,內(nèi)核會監(jiān)視這些描述符的狀態(tài),當有描述符就緒時,select 函數(shù)返回,應用程序再對就緒的描述符進行 I/O 操作。
這就好比你在餐廳同時點了多道菜,你只需要等待服務員一次性通知你哪些菜已經(jīng)準備好了,然后去取相應的菜,而不需要每道菜都單獨詢問。I/O 多路復用大大提高了系統(tǒng)的并發(fā)處理能力,減少了線程資源的消耗,但是它也存在一些問題,如 select 和 poll 的性能會隨著監(jiān)視的描述符數(shù)量增加而下降,epoll 雖然性能較好,但在高并發(fā)場景下,大量的事件處理也可能成為性能瓶頸。
1.4傳統(tǒng) I/O 模型的局限性
傳統(tǒng)的 I/O 模型,無論是阻塞式 I/O、非阻塞式 I/O 還是 I/O 多路復用,在面對現(xiàn)代應用程序?qū)Ω咝阅?、高并發(fā)的需求時,都存在一定的局限性。它們的主要問題在于:
- 系統(tǒng)調(diào)用開銷大:每次 I/O 操作都需要進行系統(tǒng)調(diào)用,從用戶態(tài)切換到內(nèi)核態(tài),這會帶來一定的開銷。在高并發(fā)場景下,頻繁的系統(tǒng)調(diào)用會消耗大量的 CPU 資源。
- 數(shù)據(jù)拷貝次數(shù)多:在數(shù)據(jù)傳輸過程中,數(shù)據(jù)往往需要在用戶空間和內(nèi)核空間之間多次拷貝,這不僅增加了數(shù)據(jù)傳輸?shù)臅r間,也消耗了系統(tǒng)資源。
- 異步處理能力有限:雖然非阻塞式 I/O 和 I/O 多路復用在一定程度上實現(xiàn)了異步處理,但它們的異步程度還不夠徹底,仍然需要應用程序主動輪詢或等待事件通知,無法充分發(fā)揮硬件的性能。
為了突破這些局限性,Linux 內(nèi)核引入了 io_uring,它代表了一種全新的異步 I/O 模型,為開發(fā)者提供了更高效、更強大的 I/O 處理能力。
Part2.io_uring 是什么
2.1定義與起源
io_uring 是 Linux 內(nèi)核提供的高性能異步 I/O 框架,它在 Linux 5.1 版本中被引入,由 Jens Axboe 開發(fā) 。在 io_uring 出現(xiàn)之前,傳統(tǒng)的異步 I/O 模型,如 epoll 或者 POSIX AIO,在大規(guī)模 I/O 操作中效率較低,存在系統(tǒng)調(diào)用開銷大、數(shù)據(jù)拷貝次數(shù)多、異步處理能力有限等問題。為了解決這些問題,io_uring 應運而生,它的出現(xiàn)為 Linux 異步 I/O 領域帶來了新的解決方案,旨在提供更高效、更強大的 I/O 處理能力。
2.2設計目標與特點
統(tǒng)一網(wǎng)絡和磁盤異步 I/O:在 io_uring 之前,Linux 的網(wǎng)絡 I/O 和磁盤 I/O 使用不同的機制,這給開發(fā)者帶來了很大的不便。io_uring 的設計目標之一就是統(tǒng)一網(wǎng)絡和磁盤異步 I/O,使得開發(fā)者可以使用統(tǒng)一的接口來處理不同類型的 I/O 操作。這就像一個萬能的工具,無論你是處理網(wǎng)絡數(shù)據(jù)的傳輸,還是磁盤文件的讀寫,都可以使用 io_uring 這個工具,而不需要在不同的工具之間切換。
提供統(tǒng)一完善的異步 API:它提供了一套統(tǒng)一且完善的異步 API,簡化了異步 I/O 編程。在傳統(tǒng)的 I/O 模型中,開發(fā)者可能需要使用多個不同的函數(shù)和系統(tǒng)調(diào)用來實現(xiàn)異步 I/O,而且這些接口可能并不統(tǒng)一,容易出錯。io_uring 將這些復雜的操作封裝成了簡單易用的 API,開發(fā)者只需要調(diào)用這些 API,就可以輕松地實現(xiàn)異步 I/O 操作,降低了編程的難度和出錯的概率。
支持異步、輪詢、無鎖、零拷貝:io_uring 支持異步操作,應用程序在發(fā)起 I/O 請求后不必等待操作完成,可以繼續(xù)執(zhí)行其他任務,提高了系統(tǒng)的并發(fā)處理能力;它還支持輪詢模式,不依賴硬件的中斷,通過調(diào)用 IORING_ENTER_GETEVENTS 不斷輪詢收割完成事件,減少了中斷開銷;同時,io_uring 采用了無鎖設計,避免了鎖競爭帶來的性能損耗;在數(shù)據(jù)傳輸過程中,io_uring 支持零拷貝技術,減少了數(shù)據(jù)在用戶空間和內(nèi)核空間之間的拷貝次數(shù),提高了數(shù)據(jù)傳輸?shù)男?。例如,在一個文件傳輸?shù)膱鼍爸?,使?io_uring 可以大大減少數(shù)據(jù)拷貝的時間,提高文件傳輸?shù)乃俣取?/span>
2.3io_uring設計思路
1.解決“系統(tǒng)調(diào)用開銷大”的問題?
針對這個問題,考慮是否每次都需要系統(tǒng)調(diào)用。如果能將多次系統(tǒng)調(diào)用中的邏輯放到有限次數(shù)中來,就能將消耗降為常數(shù)時間復雜度。
2.解決“拷貝開銷大”的問題?
之所以在提交和完成事件中存在大量的內(nèi)存拷貝,是因為應用程序和內(nèi)核之間的通信需要拷貝數(shù)據(jù),所以為了避免這個問題,需要重新考量應用與內(nèi)核間的通信方式。我們發(fā)現(xiàn),兩者通信,不是必須要拷貝,通過現(xiàn)有技術,可以讓應用與內(nèi)核共享內(nèi)存。
要實現(xiàn)核外與內(nèi)核的零拷貝,最佳方式就是實現(xiàn)一塊內(nèi)存映射區(qū)域,兩者共享一段內(nèi)存,核外往這段內(nèi)存寫數(shù)據(jù),然后通知內(nèi)核使用這段內(nèi)存數(shù)據(jù),或者內(nèi)核填寫這段數(shù)據(jù),核外使用這部分數(shù)據(jù)。因此,需要一對共享的ring buffer用于應用程序和內(nèi)核之間的通信。
- 一塊用于核外傳遞數(shù)據(jù)給內(nèi)核,一塊是內(nèi)核傳遞數(shù)據(jù)給核外,一方只讀,一方只寫。
- 提交隊列SQ(submission queue)中,應用是IO提交的生產(chǎn)者,內(nèi)核是消費者。
- 完成隊列CQ(completion queue)中,內(nèi)核是IO完成的生產(chǎn)者,應用是消費者。
- 內(nèi)核控制SQ ring的head和CQ ring的tail,應用程序控制SQ ring的tail和CQ ring的head
3.解決“API不友好”的問題?
問題在于需要多個系統(tǒng)調(diào)用才能完成,考慮是否可以把多個系統(tǒng)調(diào)用合而為一。有時候,將多個類似的函數(shù)合并并通過參數(shù)區(qū)分不同的行為是更好的選擇,而有時候可能需要將復雜的函數(shù)分解為更簡單的部分來進行重構。
如果發(fā)現(xiàn)函數(shù)中的某一部分代碼可以獨立出來成為一個單獨的函數(shù),可以先進行這樣的提煉,然后再考慮是否需要進一步使用參數(shù)化方法重構。
Part3.io_uring原理剖析
3.1核心概念
1.環(huán)形緩沖區(qū)
io_uring 的核心是兩個環(huán)形緩沖區(qū):提交隊列(Submission Queue,SQ)和完成隊列(Completion Queue,CQ)。這兩個隊列在內(nèi)核態(tài)和用戶態(tài)之間共享,通過內(nèi)存映射(mmap)的方式實現(xiàn)。
提交隊列(SQ)用于存放用戶程序提交的 I/O 請求。當用戶程序需要進行 I/O 操作時,它會創(chuàng)建一個提交隊列條目(Submission Queue Entry,SQE),并將其放入 SQ 中。每個 SQE 包含了 I/O 操作的詳細信息,如操作類型(讀、寫等)、文件描述符、緩沖區(qū)地址、數(shù)據(jù)長度等。
完成隊列(CQ)用于存放內(nèi)核完成 I/O 操作后的結(jié)果。當內(nèi)核完成一個 I/O 操作后,會將對應的完成隊列條目(Completion Queue Entry,CQE)放入 CQ 中。CQE 包含了 I/O 操作的返回值(如讀取或?qū)懭氲淖止?jié)數(shù)、錯誤碼等)以及用戶在 SQE 中設置的用戶數(shù)據(jù)。
環(huán)形緩沖區(qū)的工作方式基于生產(chǎn)者 - 消費者模型。用戶程序是 SQ 的生產(chǎn)者,內(nèi)核是 SQ 的消費者;內(nèi)核是 CQ 的生產(chǎn)者,用戶程序是 CQ 的消費者。通過這種方式,io_uring 實現(xiàn)了用戶態(tài)和內(nèi)核態(tài)之間高效的通信,減少了系統(tǒng)調(diào)用的次數(shù)和數(shù)據(jù)拷貝的開銷。例如,在傳統(tǒng)的 I/O 模型中,每次 I/O 操作都需要進行系統(tǒng)調(diào)用,從用戶態(tài)切換到內(nèi)核態(tài),而 io_uring 通過共享的環(huán)形緩沖區(qū),用戶程序可以直接將 I/O 請求放入 SQ,內(nèi)核從 SQ 中獲取請求并處理,處理完成后將結(jié)果放入 CQ,用戶程序再從 CQ 中獲取結(jié)果,避免了頻繁的系統(tǒng)調(diào)用和上下文切換。
2.異步 I/O 操作
io_uring 的異步 I/O 操作機制是其高性能的關鍵之一。在傳統(tǒng)的 I/O 模型中,當應用程序發(fā)起 I/O 請求后,通常需要等待 I/O 操作完成才能繼續(xù)執(zhí)行其他任務,這期間應用程序會被阻塞。而在 io_uring 中,用戶程序提交 I/O 請求后,無需等待操作完成,就可以繼續(xù)執(zhí)行其他任務。
當用戶程序?qū)?I/O 請求寫入提交隊列(SQ)后,內(nèi)核會異步地處理這些請求。內(nèi)核會根據(jù)請求的類型和參數(shù),執(zhí)行相應的 I/O 操作,如從磁盤讀取數(shù)據(jù)或向網(wǎng)絡發(fā)送數(shù)據(jù)。在 I/O 操作執(zhí)行過程中,用戶程序可以繼續(xù)執(zhí)行其他代碼,不會被阻塞。當 I/O 操作完成后,內(nèi)核會將操作結(jié)果寫入完成隊列(CQ),并通過事件通知機制(如 epoll)通知用戶程序。用戶程序可以通過輪詢 CQ 或等待事件通知的方式,獲取 I/O 操作的結(jié)果,并進行后續(xù)處理。這種異步操作方式使得應用程序能夠充分利用 CPU 資源,提高了系統(tǒng)的并發(fā)處理能力。
例如,在一個文件服務器中,當有多個客戶端同時請求讀取文件時,使用 io_uring 可以讓服務器在處理一個客戶端的 I/O 請求時,同時處理其他客戶端的請求,而不需要等待每個 I/O 請求都完成后再處理下一個,大大提高了服務器的響應速度和吞吐量。
3.批量操作與更多操作支持
io_uring 支持批量提交和處理 I/O 請求,這進一步提升了其性能。用戶程序可以一次性將多個 I/O 請求寫入提交隊列(SQ),然后通過一次系統(tǒng)調(diào)用(如 io_uring_enter)通知內(nèi)核處理這些請求。內(nèi)核會批量處理這些請求,并將結(jié)果批量寫入完成隊列(CQ)。這種批量操作方式減少了系統(tǒng)調(diào)用的次數(shù)和上下文切換的開銷,提高了 I/O 操作的效率。例如,在處理大量文件讀寫操作時,使用批量操作可以顯著減少系統(tǒng)調(diào)用的開銷,提高文件讀寫的速度。
此外,io_uring 支持的操作類型非常豐富,不僅包括傳統(tǒng)的文件 I/O 操作(如 read、write、open、close 等),還支持網(wǎng)絡相關的系統(tǒng)調(diào)用,如 send、recv、accept、connect 等。這使得開發(fā)者可以使用 io_uring 來構建高性能的網(wǎng)絡服務器和應用程序。在開發(fā)一個高并發(fā)的 Web 服務器時,可以使用 io_uring 來處理客戶端的連接請求、數(shù)據(jù)接收和發(fā)送等操作,充分發(fā)揮其高性能和異步處理的優(yōu)勢。io_uring 還支持一些其他的系統(tǒng)調(diào)用,如文件系統(tǒng)的操作(如 fsync、fdatasync 等),為開發(fā)者提供了更強大的功能和更靈活的編程方式。
3.2工作原理
1.提交隊列(SQ)工作流程
用戶程序在進行 I/O 操作時,首先會與 io_uring 進行交互,將 I/O 請求寫入提交隊列(SQ)。具體步驟如下:
- 獲取空閑的 SQE:用戶程序通過調(diào)用 io_uring_get_sqe 函數(shù),從提交隊列中獲取一個空閑的提交隊列條目(SQE)。這個過程類似于從一個空閑資源池中獲取一個資源,每個 SQE 都可以看作是一個承載 I/O 請求的 “容器”。
- 設置請求參數(shù):獲取到 SQE 后,用戶程序會根據(jù) I/O 操作的具體需求,設置 SQE 的各個參數(shù)。這些參數(shù)包括操作碼(opcode),用于指定 I/O 操作的類型,如讀操作(IORING_OP_READ)或?qū)懖僮鳎↖ORING_OP_WRITE);文件描述符(fd),指向要進行 I/O 操作的文件或套接字;偏移量(off),指定從文件或套接字的哪個位置開始執(zhí)行 I/O 操作;緩沖區(qū)地址(addr),指向用戶空間中用于存放讀取數(shù)據(jù)或提供要寫入數(shù)據(jù)的緩沖區(qū);數(shù)據(jù)長度(len),指定要讀取或?qū)懭氲臄?shù)據(jù)量等。還可以設置一些其他的標志位和用戶自定義數(shù)據(jù),以便在 I/O 操作完成后進行相關的處理。
- 將 SQE 索引放入 SQ:設置好 SQE 的參數(shù)后,用戶程序會將該 SQE 的索引放入提交隊列(SQ)中,并更新 SQ 的尾指針(tail)。這就像是將一個裝滿請求信息的 “包裹” 放入一個環(huán)形的傳送帶上,尾指針則表示傳送帶上最后一個 “包裹” 的位置。通過這種方式,用戶程序向內(nèi)核表明有新的 I/O 請求需要處理。
2.完成隊列(CQ)工作流程
當內(nèi)核完成 I/O 操作后,會將操作結(jié)果寫入完成隊列(CQ),用戶程序從 CQ 中獲取結(jié)果并進行處理,具體流程如下:
- 內(nèi)核寫入 CQE:內(nèi)核在完成 I/O 操作后,會創(chuàng)建一個完成隊列條目(CQE),并將 I/O 操作的結(jié)果信息填充到 CQE 中。這些結(jié)果信息包括操作的返回值(res),如果操作成功,res 表示實際傳輸?shù)淖止?jié)數(shù);如果操作失敗,res 表示錯誤碼(通常是一個負值,其絕對值對應具體的錯誤類型)。CQE 中還包含用戶在提交 I/O 請求時設置的用戶數(shù)據(jù)(user_data),以便用戶程序在獲取結(jié)果時能夠識別該結(jié)果對應的是哪個 I/O 請求。內(nèi)核將 CQE 放入完成隊列(CQ)中,并更新 CQ 的尾指針(tail),表示有新的完成事件可供用戶程序處理。
- 用戶程序獲取 CQE:用戶程序可以通過調(diào)用 io_uring_wait_cqe 函數(shù)來阻塞等待,直到 CQ 中有新的 CQE 可供處理;也可以通過調(diào)用 io_uring_peek_cqe 函數(shù)進行非阻塞地檢查,看是否有新的 CQE。當獲取到 CQE 后,用戶程序可以根據(jù) CQE 中的結(jié)果信息進行相應的處理。如果 I/O 操作成功,用戶程序可以處理讀取到的數(shù)據(jù)或確認寫入操作已成功完成;如果 I/O 操作失敗,用戶程序可以根據(jù)錯誤碼進行錯誤處理,如重試操作或向用戶報告錯誤信息。
- 標記 CQE 為已處理:用戶程序處理完 CQE 后,需要調(diào)用 io_uring_cqe_seen 函數(shù),將 CQ 的尾指針向前移動,標記該 CQE 已被處理,以便后續(xù)可以接收新的完成事件。這就像是在處理完一個任務后,將任務標記為已完成,以便系統(tǒng)可以繼續(xù)處理其他新的任務。
3.內(nèi)核與用戶態(tài)交互
內(nèi)核和用戶態(tài)之間通過共享內(nèi)存的環(huán)形緩沖區(qū)(即提交隊列 SQ 和完成隊列 CQ)進行交互,這種交互方式極大地減少了系統(tǒng)調(diào)用和上下文切換的開銷,提高了 I/O 操作的效率,其原理如下:
- 共享內(nèi)存映射:在初始化 io_uring 時,通過內(nèi)存映射(mmap)機制,將提交隊列(SQ)和完成隊列(CQ)映射到用戶空間和內(nèi)核空間。這樣,用戶程序和內(nèi)核都可以直接訪問這兩個隊列,而不需要通過傳統(tǒng)的系統(tǒng)調(diào)用方式進行數(shù)據(jù)傳遞。這就好比在用戶空間和內(nèi)核空間之間建立了一條 “高速公路”,數(shù)據(jù)可以直接在兩者之間快速傳輸,而不需要經(jīng)過復雜的 “收費站”(系統(tǒng)調(diào)用)。
- 減少系統(tǒng)調(diào)用:在傳統(tǒng)的 I/O 模型中,每次 I/O 操作都需要進行多次系統(tǒng)調(diào)用,從用戶態(tài)切換到內(nèi)核態(tài),然后再切換回用戶態(tài)。而在 io_uring 中,用戶程序?qū)?I/O 請求寫入 SQ 和從 CQ 獲取結(jié)果,都可以在用戶態(tài)完成,不需要頻繁地進行系統(tǒng)調(diào)用。只有在需要通知內(nèi)核處理 SQ 中的請求時(如調(diào)用 io_uring_enter 函數(shù)),才會進行一次系統(tǒng)調(diào)用,大大減少了系統(tǒng)調(diào)用的次數(shù)。
- 減少上下文切換:上下文切換是指當操作系統(tǒng)從一個進程或線程切換到另一個進程或線程時,需要保存當前進程或線程的狀態(tài)信息,并恢復下一個進程或線程的狀態(tài)信息。在傳統(tǒng) I/O 模型中,頻繁的系統(tǒng)調(diào)用會導致大量的上下文切換,消耗 CPU 資源。而 io_uring 通過共享內(nèi)存的方式,減少了系統(tǒng)調(diào)用的次數(shù),也就相應地減少了上下文切換的次數(shù),使得 CPU 可以更專注于執(zhí)行實際的 I/O 操作和用戶程序的邏輯代碼。
3.3系統(tǒng)調(diào)用詳解
io_uring的實現(xiàn)僅僅使用了三個syscall:io_uring_setup, io_uring_enter和io_uring_register。
這幾個系統(tǒng)調(diào)用接口都在io_uring.c文件中:
1.io_uring_setup
io_uring_setup 是用于初始化 io_uring 環(huán)境的系統(tǒng)調(diào)用。在使用 io_uring 進行異步 I/O 操作之前,首先需要調(diào)用 io_uring_setup 來創(chuàng)建一個 io_uring 實例。它接受兩個參數(shù),第一個參數(shù)是期望的提交隊列(SQ)的大小,即隊列中可以容納的 I/O 請求數(shù)量;第二個參數(shù)是一個指向 io_uring_params 結(jié)構體的指針,該結(jié)構體用于返回 io_uring 實例的相關參數(shù),如實際分配的 SQ 和完成隊列(CQ)的大小、隊列的偏移量等信息。
在調(diào)用 io_uring_setup 時,內(nèi)核會為 io_uring 實例分配所需的內(nèi)存空間,包括 SQ、CQ 以及相關的控制結(jié)構。同時,內(nèi)核還會創(chuàng)建一些內(nèi)部數(shù)據(jù)結(jié)構,用于管理和調(diào)度 I/O 請求。如果初始化成功,io_uring_setup 會返回一個文件描述符,這個文件描述符用于標識創(chuàng)建的 io_uring 實例,后續(xù)的 io_uring 系統(tǒng)調(diào)用(如 io_uring_enter、io_uring_register)將通過這個文件描述符來操作該 io_uring 實例。若初始化失敗,函數(shù)將返回一個負數(shù),表示相應的錯誤代碼。
io_uring_setup():
SYSCALL_DEFINE2(io_uring_setup, u32, entries,
struct io_uring_params __user *, params)
{
return io_uring_setup(entries, params);
}- 功能:用于初始化和配置 io_uring 。
- 應用用途:在使用 io_uring 之前,首先需要調(diào)用此接口初始化一個 io_uring 環(huán),并設置其參數(shù)。
2.io_uring_enter
io_uring_enter 是用于提交和等待 I/O 操作的系統(tǒng)調(diào)用。它的主要作用是將應用程序準備好的 I/O 請求提交給內(nèi)核,并可以選擇等待這些操作完成。io_uring_enter 接受多個參數(shù),其中包括 io_uring_setup 返回的文件描述符,用于指定要操作的 io_uring 實例;to_submit 參數(shù)表示要提交的 I/O 請求的數(shù)量,即從提交隊列(SQ)中取出并提交給內(nèi)核的 SQE 的數(shù)量;min_complete 參數(shù)指定了內(nèi)核在返回之前必須等待完成的 I/O 操作的最小數(shù)量;flags 參數(shù)則用于控制 io_uring_enter 的行為,例如可以設置是否等待 I/O 操作完成、是否獲取完成的 I/O 事件等。當調(diào)用 io_uring_enter 時,如果 to_submit 參數(shù)大于 0,內(nèi)核會從 SQ 中取出相應數(shù)量的 SQE,并將這些 I/O 請求提交到內(nèi)核中進行處理。
同時,如果設置了等待 I/O 操作完成的標志,內(nèi)核會阻塞等待,直到至少有 min_complete 個 I/O 操作完成,然后將這些完成的操作結(jié)果放入完成隊列(CQ)中。應用程序可以通過檢查 CQ 來獲取這些完成的 I/O 請求的結(jié)果。通過 io_uring_enter,應用程序可以靈活地控制 I/O 請求的提交和等待策略,提高 I/O 操作的效率和靈活性。
io_uring_enter():
SYSCALL_DEFINE6(io_uring_enter, unsigned int, fd, u32, to_submit,
u32, min_complete, u32, flags, const void __user *, argp,
size_t, argsz)- 功能:用于提交和處理異步 I/O 操作。
- 應用用途:在向 io_uring 環(huán)中提交 I/O 操作后,通過調(diào)用此接口觸發(fā)內(nèi)核處理這些操作,并獲取完成的操作結(jié)果。
3.io_uring_register
io_uring_register 用于注冊文件描述符或事件文件描述符到 io_uring 實例中,以便在后續(xù)的 I/O 操作中使用。它接受四個參數(shù),第一個參數(shù)是 io_uring_setup 返回的文件描述符,用于指定要注冊到的 io_uring 實例;第二個參數(shù) opcode 表示注冊的類型,例如可以是 IORING_REGISTER_FILES(注冊文件描述符集合)、IORING_REGISTER_BUFFERS(注冊內(nèi)存緩沖區(qū))、IORING_REGISTER_EVENTFD(注冊 eventfd 用于通知完成事件)等;
第三個參數(shù) arg 是一個指針,根據(jù) opcode 的類型不同,它指向不同的內(nèi)容,如注冊文件描述符時,arg 指向一個包含文件描述符的數(shù)組;注冊緩沖區(qū)時,arg 指向一個描述緩沖區(qū)的結(jié)構體數(shù)組;第四個參數(shù) nr_args 表示 arg 所指向的數(shù)組的長度。通過 io_uring_register 注冊文件描述符或緩沖區(qū)等資源后,內(nèi)核在處理 I/O 請求時,可以直接訪問這些預先注冊的資源,而無需每次都重新設置相關信息,從而提高了 I/O 操作的效率。例如,在進行大量文件讀寫操作時,預先注冊文件描述符可以避免每次提交 I/O 請求時都進行文件描述符的查找和驗證,減少了系統(tǒng)開銷,提升了 I/O 性能。
io_uring_register():
SYSCALL_DEFINE4(io_uring_register, unsigned int, fd, unsigned int, opcode,
void __user *, arg, unsigned int, nr_args)- 功能:用于注冊文件描述符、緩沖區(qū)、事件文件描述符等資源到 io_uring 環(huán)中。
- 應用用途:在進行 I/O 操作之前,需要將相關的資源注冊到 io_uring 環(huán)中,以便進行后續(xù)的異步 I/O 操作。
3.4工作流程深度剖析
1.創(chuàng)建 io_uring 對象
使用 io_uring 進行異步 I/O 操作的第一步是創(chuàng)建 io_uring 對象。內(nèi)核提供了io_uring_setup系統(tǒng)調(diào)用來初始化一個io_uring實例,創(chuàng)建SQ、CQ和SQ Array,entries參數(shù)表示的是SQ和SQArray的大小,CQ的大小默認是2 * entries。params參數(shù)既是輸入?yún)?shù),也是輸出參數(shù)。
該函數(shù)返回一個file descriptor,并將io_uring支持的功能、以及各個數(shù)據(jù)結(jié)構在fd中的偏移量存入params。用戶根據(jù)偏移量將fd通過mmap內(nèi)存映射得到一塊內(nèi)核用戶共享的內(nèi)存區(qū)域。這塊內(nèi)存區(qū)域中,有io_uring的上下文信息:SQ信息、CQ信息和SQ Array信息。
int io_uring_setup(int entries, struct io_uring_params *params);這通過調(diào)用 io_uring_setup 系統(tǒng)調(diào)用來完成。在調(diào)用 io_uring_setup 時,用戶需要指定提交隊列(SQ)的大小,即期望的 I/O 請求隊列長度。內(nèi)核會根據(jù)這個請求,為 io_uring 對象分配必要的內(nèi)存空間,包括提交隊列(SQ)、完成隊列(CQ)以及相關的控制結(jié)構。內(nèi)核會創(chuàng)建一個 io_ring_ctx 結(jié)構體對象,用于管理 io_uring 的上下文信息。
同時,還會創(chuàng)建一個 io_urings 結(jié)構體對象,該對象包含了 SQ 和 CQ 的具體實現(xiàn),如隊列的頭部索引(head)、尾部索引(tail)、隊列大小等信息。在創(chuàng)建過程中,內(nèi)核會初始化 SQ 和 CQ 的所有隊列項(SQE 和 CQE),并設置好相關的指針和標志位。如果用戶在調(diào)用 io_uring_setup 時設置了 IORING_SETUP_SQPOLL 標志位,內(nèi)核還會創(chuàng)建一個 SQ 線程,用于從 SQ 隊列中獲取 I/O 請求并提交給內(nèi)核處理。
創(chuàng)建完成后,io_uring_setup 會返回一個文件描述符,這個文件描述符是后續(xù)操作 io_uring 對象的關鍵標識,通過它可以進行 I/O 請求的提交、注冊文件描述符等操作。
2.準備 I/O 請求
在創(chuàng)建 io_uring 對象后,需要準備具體的 I/O 請求。這通常通過 io_uring_prep_XXX 系列函數(shù)來完成,這些函數(shù)用于準備不同類型的 I/O 請求,如 io_uring_prep_read 用于準備讀取操作,io_uring_prep_write 用于準備寫入操作,io_uring_prep_accept 用于準備異步接受連接操作等。
以 io_uring_prep_read 為例,它接受多個參數(shù),包括指向提交隊列項(SQE)的指針、目標文件描述符、讀取數(shù)據(jù)的緩沖區(qū)地址、讀取的字節(jié)數(shù)以及文件中的偏移量等。函數(shù)會根據(jù)這些參數(shù),將 I/O 請求的相關信息填充到 SQE 結(jié)構體中,包括設置操作類型(如 IORING_OP_READ)、目標文件描述符、緩沖區(qū)地址、數(shù)據(jù)長度、偏移量等字段。
除了基本的 I/O 操作參數(shù)外,還可以設置一些額外的標志位和選項,如 I/O 操作的優(yōu)先級、是否使用直接 I/O 等,以滿足不同的應用需求。通過這些函數(shù),應用程序可以靈活地構建各種類型的 I/O 請求,并將其準備好以便提交到內(nèi)核中進行處理。
3.提交 I/O 請求
當 I/O 請求準備好后,需要將其提交到內(nèi)核中執(zhí)行。這通過調(diào)用 io_uring_submit 函數(shù)(內(nèi)部調(diào)用 io_uring_enter 系統(tǒng)調(diào)用)來實現(xiàn)。在提交 I/O 請求時,首先應用程序會將準備好的 SQE 添加到提交隊列(SQ)中。SQ 是一個環(huán)形緩沖區(qū),應用程序通過操作 SQ Ring 中的 tail 指針來將 SQE 放入隊列。具體來說,應用程序會將 tail 指向的 SQE 填充為準備好的 I/O 請求信息,然后將 tail 指針遞增,指向下一個空閑的 SQE 位置。在填充 SQE 時,需要注意按照 SQE 結(jié)構體的定義,正確設置各項字段,確保 I/O 請求的信息準確無誤。
默認情況下,使用 io_uring 提交 I/O 請求需要:
- 從SQ Arrary中找到一個空閑的SQE;
- 根據(jù)具體的I/O請求設置該SQE;
- 將SQE的數(shù)組索引放到SQ中;
- 調(diào)用系統(tǒng)調(diào)用io_uring_enter提交SQ中的I/O請求。

當所有要提交的 I/O 請求都添加到 SQ 中后,調(diào)用 io_uring_submit 函數(shù),該函數(shù)會觸發(fā) io_uring_enter 系統(tǒng)調(diào)用,將 SQ 中的 I/O 請求提交給內(nèi)核。內(nèi)核接收到請求后,會從 SQ 中獲取 SQE,并根據(jù) SQE 中的信息執(zhí)行相應的 I/O 操作。在這個過程中,由于 SQ 是用戶態(tài)和內(nèi)核態(tài)共享的內(nèi)存區(qū)域,避免了數(shù)據(jù)的多次拷貝和額外的系統(tǒng)調(diào)用開銷,提高了 I/O 請求提交的效率。
4.等待 IO 請求完成
提交 I/O 請求后,應用程序可以選擇等待請求完成。等待 I/O 請求完成有兩種主要方式。一種是使用 io_uring_wait_cqe 函數(shù),該函數(shù)會阻塞調(diào)用線程,直到至少有一個 I/O 請求完成,并返回完成的完成隊列項(CQE)。當調(diào)用 io_uring_wait_cqe 時,它會檢查完成隊列(CQ)中是否有新完成的 I/O 請求。如果沒有,線程會進入阻塞狀態(tài),直到內(nèi)核將完成的 I/O 請求結(jié)果放入 CQ 中。一旦有新的 CQE 可用,io_uring_wait_cqe 會返回該 CQE,應用程序可以通過 CQE 獲取 I/O 操作的結(jié)果。
另一種方式是使用 io_uring_peek_batch_cqe 函數(shù),它是非阻塞的,用于檢查 CQ 中是否有已經(jīng)完成的 I/O 請求。如果有,它會返回已完成的 CQE 列表,應用程序可以根據(jù)返回的 CQE 進行相應的處理;如果沒有完成的請求,函數(shù)會立即返回,應用程序可以繼續(xù)執(zhí)行其他任務,然后在適當?shù)臅r候再次調(diào)用該函數(shù)檢查 CQ。這兩種方式為應用程序提供了靈活的等待策略,使其可以根據(jù)自身的業(yè)務需求和性能要求,選擇合適的方式來處理 I/O 請求的完成事件。
5.獲取 IO 請求結(jié)果
當 I/O 請求完成后,應用程序需要從完成隊列(CQ)中獲取結(jié)果。這可以通過 io_uring_peek_cqe 函數(shù)來實現(xiàn)。io_uring_peek_cqe 函數(shù)用于從 CQ 中獲取一個完成的 CQE,而不將其從隊列中移除。應用程序獲取到 CQE 后,可以根據(jù) CQE 中的信息來處理完成的 I/O 請求。CQE 中包含了豐富的信息,如 I/O 操作的返回值、狀態(tài)碼、用戶自定義數(shù)據(jù)等。例如,對于文件讀取操作,CQE 中的返回值表示實際讀取的字節(jié)數(shù),狀態(tài)碼用于指示操作是否成功,若操作失敗,狀態(tài)碼會包含具體的錯誤信息。
應用程序可以根據(jù)這些信息進行相應的處理,如讀取數(shù)據(jù)并進行后續(xù)的業(yè)務邏輯處理,或者在操作失敗時進行錯誤處理,如記錄錯誤日志、重新嘗試 I/O 操作等。在獲取 CQE 后,應用程序通常會根據(jù) I/O 操作的類型和結(jié)果,執(zhí)行相應的業(yè)務邏輯,以實現(xiàn)應用程序的功能需求。
6.釋放 IO 請求結(jié)果
在獲取并處理完 IO 請求結(jié)果后,需要釋放該結(jié)果,以便內(nèi)核可以繼續(xù)使用完成隊列(CQ)。這通過調(diào)用 io_uring_cqe_seen 函數(shù)來實現(xiàn)。io_uring_cqe_seen 函數(shù)的作用是標記一個完成的 CQE 已經(jīng)被處理,它會將 CQ Ring 中的 head 指針遞增,指向下一個未處理的 CQE。通過這種方式,內(nèi)核可以知道哪些 CQE 已經(jīng)被應用程序處理,從而可以繼續(xù)向 CQ 中放入新的完成結(jié)果。
在釋放 IO 請求結(jié)果時,需要注意確保已經(jīng)完成了對 CQE 中信息的處理,避免在釋放后再次訪問已釋放的 CQE。同時,及時釋放 CQE 也有助于提高系統(tǒng)的性能和資源利用率,避免 CQ 隊列被占用過多而影響后續(xù) I/O 請求結(jié)果的存儲和處理。通過正確地釋放 IO 請求結(jié)果,保證了 io_uring 的工作流程能夠持續(xù)高效地運行,為應用程序提供穩(wěn)定的異步 I/O 服務。
Part4.io_uring 應用實例
4.1 io_uring應用場景
1.在高性能網(wǎng)絡服務中的應用
在高性能網(wǎng)絡服務領域,io_uring 展現(xiàn)出了強大的優(yōu)勢,能夠顯著提升網(wǎng)絡服務的并發(fā)處理能力和性能。以 Web 服務器和代理服務器為例,它們通常需要處理大量的并發(fā)連接和數(shù)據(jù)傳輸。
在傳統(tǒng)的 Web 服務器中,如使用基于 epoll 的 I/O 模型,雖然可以通過事件驅(qū)動的方式處理多個連接,但在高并發(fā)情況下,仍然存在一定的局限性。例如,當有大量客戶端同時請求訪問網(wǎng)頁時,epoll 需要不斷地輪詢文件描述符,檢查是否有新的事件發(fā)生,這會消耗大量的 CPU 資源。而且,每次數(shù)據(jù)傳輸都可能涉及多次系統(tǒng)調(diào)用和數(shù)據(jù)拷貝,導致效率低下。
而引入 io_uring 后,Web 服務器的性能得到了大幅提升。io_uring 的異步 I/O 特性使得服務器在處理 I/O 請求時,無需阻塞等待操作完成,可以立即處理其他請求,從而大大提高了并發(fā)處理能力。在處理靜態(tài)文件請求時,服務器可以使用 io_uring 一次性提交多個文件讀取請求,內(nèi)核在后臺異步地處理這些請求,并將結(jié)果放入完成隊列。服務器從完成隊列中獲取結(jié)果后,直接將數(shù)據(jù)發(fā)送給客戶端,減少了數(shù)據(jù)傳輸?shù)难舆t。io_uring 支持的零拷貝技術也減少了數(shù)據(jù)在用戶空間和內(nèi)核空間之間的拷貝次數(shù),提高了數(shù)據(jù)傳輸?shù)男省?/span>
對于代理服務器來說,io_uring 同樣具有重要意義。代理服務器需要在客戶端和目標服務器之間轉(zhuǎn)發(fā)數(shù)據(jù),對數(shù)據(jù)傳輸?shù)男屎筒l(fā)處理能力要求極高。使用 io_uring,代理服務器可以更高效地處理大量的并發(fā)連接,減少數(shù)據(jù)轉(zhuǎn)發(fā)的延遲。在處理 HTTP 代理請求時,代理服務器可以利用 io_uring 的異步 I/O 和批量操作特性,同時處理多個客戶端的請求,快速地從目標服務器獲取數(shù)據(jù)并轉(zhuǎn)發(fā)給客戶端,提升了代理服務的性能和響應速度。
2.在數(shù)據(jù)庫系統(tǒng)中的應用
在數(shù)據(jù)庫系統(tǒng)中,I/O 操作是影響性能的關鍵因素之一。數(shù)據(jù)庫系統(tǒng)需要頻繁地進行數(shù)據(jù)的讀寫、索引的更新等操作,這些操作都涉及大量的 I/O。io_uring 為數(shù)據(jù)庫系統(tǒng)提供了高效的 I/O 支持,對提升數(shù)據(jù)庫性能起到了重要作用。
以關系型數(shù)據(jù)庫 PostgreSQL 為例,在傳統(tǒng)的 I/O 模型下,當進行數(shù)據(jù)寫入操作時,需要將數(shù)據(jù)從用戶空間拷貝到內(nèi)核空間,然后再寫入磁盤。這個過程涉及多次系統(tǒng)調(diào)用和數(shù)據(jù)拷貝,會消耗大量的時間和資源。而且,在高并發(fā)寫入的情況下,由于鎖競爭等問題,會導致寫入性能下降。
而采用 io_uring 后,PostgreSQL 可以利用其異步 I/O 和批量操作特性,提高數(shù)據(jù)寫入的效率。數(shù)據(jù)庫可以一次性提交多個寫入請求,內(nèi)核異步地處理這些請求,減少了等待時間。io_uring 的零拷貝技術也減少了數(shù)據(jù)拷貝的開銷,提高了寫入性能。在數(shù)據(jù)讀取方面,io_uring 同樣可以提高效率。當查詢數(shù)據(jù)時,數(shù)據(jù)庫可以通過 io_uring 異步地從磁盤讀取數(shù)據(jù),在讀取數(shù)據(jù)的同時,數(shù)據(jù)庫可以繼續(xù)處理其他任務,如解析查詢語句、優(yōu)化查詢計劃等,提高了數(shù)據(jù)庫的整體響應速度。
對于一些新興的分布式數(shù)據(jù)庫,如 TiDB,io_uring 的優(yōu)勢更加明顯。分布式數(shù)據(jù)庫需要處理大量的分布式存儲節(jié)點之間的數(shù)據(jù)傳輸和同步,對 I/O 的性能和可靠性要求極高。io_uring 的高效異步 I/O 和批量操作能力,可以幫助分布式數(shù)據(jù)庫更好地處理這些復雜的 I/O 操作,提高系統(tǒng)的擴展性和性能。在數(shù)據(jù)同步過程中,io_uring 可以實現(xiàn)高效的數(shù)據(jù)傳輸,減少數(shù)據(jù)同步的延遲,保證分布式數(shù)據(jù)庫的數(shù)據(jù)一致性和可用性。
3.在大規(guī)模文件系統(tǒng)操作中的應用
在大規(guī)模文件系統(tǒng)操作場景中,如存儲服務和分布式文件系統(tǒng),io_uring 也展現(xiàn)出了獨特的優(yōu)勢。這些場景通常需要處理大量的文件讀寫、存儲和管理操作,對 I/O 性能的要求非常高。
以存儲服務為例,無論是對象存儲還是塊存儲,都需要頻繁地進行文件的讀寫操作。在傳統(tǒng)的 I/O 模型下,當處理大量文件請求時,系統(tǒng)調(diào)用開銷和數(shù)據(jù)拷貝開銷會成為性能瓶頸。例如,在對象存儲服務中,當用戶上傳或下載大量文件時,傳統(tǒng)的 I/O 模型可能會導致響應時間過長,用戶體驗不佳。
而引入 io_uring 后,存儲服務可以利用其異步 I/O 和批量操作特性,大大提高文件處理的效率。在處理文件上傳時,存儲服務可以使用 io_uring 一次性提交多個寫入請求,內(nèi)核異步地將數(shù)據(jù)寫入存儲設備,減少了用戶等待時間。在文件下載時,io_uring 可以實現(xiàn)高效的文件讀取,快速地將數(shù)據(jù)傳輸給用戶。io_uring 的零拷貝技術也減少了數(shù)據(jù)傳輸過程中的開銷,提高了存儲服務的性能和吞吐量。
對于分布式文件系統(tǒng),如 Ceph,io_uring 的應用可以提升整個文件系統(tǒng)的性能和可靠性。分布式文件系統(tǒng)需要處理多個存儲節(jié)點之間的數(shù)據(jù)分布和讀寫操作,對 I/O 的并發(fā)處理能力和數(shù)據(jù)一致性要求很高。io_uring 的異步 I/O 和批量操作能力,可以幫助分布式文件系統(tǒng)更好地管理和調(diào)度 I/O 請求,提高數(shù)據(jù)讀寫的效率。在處理大規(guī)模文件的讀寫時,io_uring 可以實現(xiàn)高效的數(shù)據(jù)傳輸和并行處理,減少文件操作的延遲,提升分布式文件系統(tǒng)的整體性能。
4.2 io_uring案例分析
1.簡單文件讀寫案例
⑴代碼實現(xiàn)
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <linux/io_uring.h>
int main() {
struct io_uring ring;
struct io_uring_sqe *sqe;
struct io_uring_cqe *cqe;
int fd, ret;
// 打開文件
fd = open("example.txt", O_RDONLY);
if (fd < 0) {
perror("Failed to open file");
return 1;
}
// 初始化io_uring
io_uring_queue_init(8, &ring, 0);
// 獲取一個提交隊列條目
sqe = io_uring_get_sqe(&ring);
if (!sqe) {
fprintf(stderr, "Could not get sqe\n");
return 1;
}
// 準備異步讀操作
char *buf = malloc(1024);
io_uring_prep_read(sqe, fd, buf, 1024, 0);
// 提交請求
io_uring_submit(&ring);
// 等待完成
ret = io_uring_wait_cqe(&ring, &cqe);
if (ret < 0) {
perror("io_uring_wait_cqe");
return 1;
}
// 檢查結(jié)果
if (cqe->res < 0) {
fprintf(stderr, "Async read failed: %s\n", strerror(-cqe->res));
} else {
printf("Read %d bytes: %s\n", cqe->res, buf);
}
// 釋放資源
io_uring_cqe_seen(&ring, cqe);
io_uring_queue_exit(&ring);
close(fd);
free(buf);
return 0;
}代碼解讀
文件打開:fd = open("example.txt", O_RDONLY); 這行代碼使用 open 函數(shù)打開名為 example.txt 的文件,以只讀模式(O_RDONLY)打開。如果打開失敗,open 函數(shù)會返回一個負數(shù),并通過 perror 函數(shù)打印錯誤信息,然后程序返回錯誤代碼 1。
io_uring 初始化:io_uring_queue_init(8, &ring, 0); 這行代碼用于初始化 io_uring 實例。其中,第一個參數(shù) 8 表示提交隊列(SQ)和完成隊列(CQ)的大小,即隊列中可以容納的 I/O 請求數(shù)量;第二個參數(shù) &ring 是指向 io_uring 結(jié)構體的指針,用于存儲初始化后的 io_uring 實例;第三個參數(shù) 0 表示使用默認的初始化標志。
獲取提交隊列條目:sqe = io_uring_get_sqe(&ring); 從 io_uring 的提交隊列中獲取一個提交隊列項(SQE)。如果獲取失敗,io_uring_get_sqe 函數(shù)會返回 NULL,程序會打印錯誤信息并返回錯誤代碼 1。
準備異步讀操作:
char *buf = malloc(1024); //分配 1024 字節(jié)的內(nèi)存空間,用于存儲讀取的文件數(shù)據(jù)。io_uring_prep_read(sqe, fd, buf, 1024, 0); 使用 io_uring_prep_read 函數(shù)準備一個異步讀操作。它接受五個參數(shù),第一個參數(shù) sqe 是之前獲取的提交隊列項;第二個參數(shù) fd 是要讀取的文件描述符;第三個參數(shù) buf 是用于存儲讀取數(shù)據(jù)的緩沖區(qū);第四個參數(shù) 1024 表示要讀取的字節(jié)數(shù);第五個參數(shù) 0 表示從文件的起始位置開始讀取。
提交請求:io_uring_submit(&ring); 將準備好的 I/O 請求提交到內(nèi)核中執(zhí)行。這個函數(shù)會觸發(fā) io_uring_enter 系統(tǒng)調(diào)用,將提交隊列中的請求提交給內(nèi)核。
等待完成:ret = io_uring_wait_cqe(&ring, &cqe); 等待 I/O 操作完成。這個函數(shù)會阻塞調(diào)用線程,直到至少有一個 I/O 請求完成,并返回完成的完成隊列項(CQE)。如果等待過程中出現(xiàn)錯誤,io_uring_wait_cqe 函數(shù)會返回一個負數(shù),程序會通過 perror 函數(shù)打印錯誤信息并返回錯誤代碼 1。
檢查結(jié)果:
if (cqe->res < 0) 檢查 I/O 操作的結(jié)果。如果 cqe->res 小于 0,表示操作失敗,通過 fprintf 函數(shù)打印錯誤信息。else 分支表示操作成功,打印實際讀取的字節(jié)數(shù)和讀取到的數(shù)據(jù)。
釋放資源:
io_uring_cqe_seen(&ring, cqe); /* 知內(nèi)核已經(jīng)處理完一個完成事件,
釋放相關資源。這通過將完成隊列的頭部指針遞增來實現(xiàn),以便內(nèi)核可以繼續(xù)使用完成隊列。*/io_uring_queue_exit(&ring); 釋放 io_uring 實例所占用的資源,包括提交隊列和完成隊列等。
close(fd); 關閉之前打開的文件。
free(buf); 釋放之前分配的內(nèi)存緩沖區(qū)。
2.網(wǎng)絡編程案例(TCP 服務器)
⑴代碼實現(xiàn)
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <liburing.h>
#define ENTRIES_LENGTH 4096
#define MAX_CONNECTIONS 1024
#define BUFFER_LENGTH 1024
char buf_table[MAX_CONNECTIONS][BUFFER_LENGTH] = {0};
enum {
READ,
WRITE,
ACCEPT,
};
struct conninfo {
int connfd;
int type;
};
void set_read_event(struct io_uring *ring, int fd, void *buf, size_t len, int flags) {
struct io_uring_sqe *sqe = io_uring_get_sqe(ring);
io_uring_prep_recv(sqe, fd, buf, len, flags);
struct conninfo ci = {.connfd = fd,.type = READ};
memcpy(&sqe->user_data, &ci, sizeof(struct conninfo));
}
void set_write_event(struct io_uring *ring, int fd, const void *buf, size_t len, int flags) {
struct io_uring_sqe *sqe = io_uring_get_sqe(ring);
io_uring_prep_send(sqe, fd, buf, len, flags);
struct conninfo ci = {.connfd = fd,.type = WRITE};
memcpy(&sqe->user_data, &ci, sizeof(struct conninfo));
}
void set_accept_event(struct io_uring *ring, int fd, struct sockaddr *cliaddr, socklen_t *clilen, unsigned flags) {
struct io_uring_sqe *sqe = io_uring_get_sqe(ring);
io_uring_prep_accept(sqe, fd, cliaddr, clilen, flags);
struct conninfo ci = {.connfd = fd,.type = ACCEPT};
memcpy(&sqe->user_data, &ci, sizeof(struct conninfo));
}
int main() {
int listenfd = socket(AF_INET, SOCK_STREAM, 0);
if (listenfd == -1) return -1;
struct sockaddr_in servaddr, clientaddr;
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(9999);
if (-1 == bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr))) {
return -2;
}
listen(listenfd, 10);
struct io_uring_params params;
memset(?ms, 0, sizeof(params));
struct io_uring ring;
memset(&ring, 0, sizeof(ring));
/*初始化params 和 ring*/
io_uring_queue_init_params(ENTRIES_LENGTH, &ring, ?ms);
socklen_t clilen = sizeof(clientaddr);
set_accept_event(&ring, listenfd, (struct sockaddr *)&clientaddr, &clilen, 0);
while (1) {
struct io_uring_cqe *cqe;
io_uring_submit(&ring);
int ret = io_uring_wait_cqe(&ring, &cqe);
struct io_uring_cqe *cqes[10];
int cqecount = io_uring_peek_batch_cqe(&ring, cqes, 10);
unsigned count = 0;
for (int i = 0; i < cqecount; i++) {
cqe = cqes[i];
count++;
struct conninfo ci;
memcpy(&ci, &cqe->user_data, sizeof(ci));
if (ci.type == ACCEPT) {
int connfd = cqe->res;
char *buffer = buf_table[connfd];
set_read_event(&ring, connfd, buffer, 1024, 0);
set_accept_event(&ring, listenfd, (struct sockaddr *)&clientaddr, &clilen, 0);
} else if (ci.type == READ) {
int bytes_read = cqe->res;
if (bytes_read == 0) {
close(ci.connfd);
} else if (bytes_read < 0) {
close(ci.connfd);
printf("client %d disconnected!\n", ci.connfd);
} else {
char *buffer = buf_table[ci.connfd];
set_write_event(&ring, ci.connfd, buffer, bytes_read, 0);
}
} else if (ci.type == WRITE) {
char *buffer = buf_table[ci.connfd];
set_read_event(&ring, ci.connfd, buffer, 1024, 0);
}
}
io_uring_cq_advance(&ring, count);
}
return 0;
}⑵代碼解讀
創(chuàng)建監(jiān)聽套接字:int listenfd = socket(AF_INET, SOCK_STREAM, 0); 使用 socket 函數(shù)創(chuàng)建一個 TCP 套接字,AF_INET 表示使用 IPv4 協(xié)議,SOCK_STREAM 表示使用流式套接字(即 TCP 協(xié)議),0 表示默認協(xié)議。如果創(chuàng)建失敗,socket 函數(shù)會返回 -1,程序返回 -1。
綁定地址和端口:
填充服務器地址結(jié)構體 servaddr,包括地址族(AF_INET)、IP 地址(INADDR_ANY 表示綁定到所有可用的網(wǎng)絡接口)和端口號(9999)。
if (-1 == bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr))) 使用 bind 函數(shù)將創(chuàng)建的套接字綁定到指定的地址和端口。如果綁定失敗,bind 函數(shù)返回 -1,程序返回 -2。
監(jiān)聽連接:listen(listenfd, 10); 使用 listen 函數(shù)開始監(jiān)聽套接字,第二個參數(shù) 10 表示最大連接數(shù),即允許同時存在的未處理連接請求的最大數(shù)量。
初始化 io_uring:
struct io_uring_params params; 和 struct io_uring ring; 分別定義了 io_uring 的參數(shù)結(jié)構體和實例結(jié)構體。
memset(?ms, 0, sizeof(params)); 和 memset(&ring, 0, sizeof(ring)); 初始化這兩個結(jié)構體的內(nèi)容為 0。io_uring_queue_init_params(ENTRIES_LENGTH, &ring, ¶ms); 使用 io_uring_queue_init_params 函數(shù)初始化 io_uring 實例,ENTRIES_LENGTH 表示提交隊列和完成隊列的大小,&ring 是指向 io_uring 實例的指針,¶ms 是指向參數(shù)結(jié)構體的指針。
設置接受連接事件:set_accept_event(&ring, listenfd, (struct sockaddr *)&clientaddr, &clilen, 0); 調(diào)用 set_accept_event 函數(shù)設置一個接受連接的異步事件。在這個函數(shù)中,首先獲取一個提交隊列項(SQE),然后使用 io_uring_prep_accept 函數(shù)準備接受連接的請求,將相關信息(如監(jiān)聽套接字、客戶端地址、地址長度等)填充到 SQE 中,并將自定義的連接信息結(jié)構體 conninfo 復制到 SQE 的用戶數(shù)據(jù)區(qū)域,用于標識該請求的類型和相關連接信息。
事件循環(huán)處理:
- while (1) 進入一個無限循環(huán),用于持續(xù)處理 I/O 事件。
- io_uring_submit(&ring); 提交準備好的 I/O 請求到內(nèi)核。
- int ret = io_uring_wait_cqe(&ring, &cqe); 等待 I/O 操作完成,獲取完成的完成隊列項(CQE)。
- struct io_uring_cqe *cqes[10]; 和 int cqecount = io_uring_peek_batch_cqe(&ring, cqes, 10); 使用 io_uring_peek_batch_cqe 函數(shù)嘗試批量獲取完成的 CQE,最多獲取 10 個。
遍歷獲取到的 CQE:
struct conninfo ci; 和 memcpy(&ci, &cqe->user_data, sizeof(ci)); 從 CQE 的用戶數(shù)據(jù)區(qū)域復制之前設置的連接信息結(jié)構體 conninfo。
根據(jù)連接信息中的類型(ci.type)進行不同的處理:
如果是 ACCEPT 類型,表示有新的連接請求被接受。獲取新的連接描述符 connfd,設置讀取事件,準備從新連接中讀取數(shù)據(jù),并再次設置接受連接事件,以便繼續(xù)接受新的連接請求。如果是 READ 類型,表示有數(shù)據(jù)可讀。根據(jù)讀取的字節(jié)數(shù)進行處理,如果讀取到的字節(jié)數(shù)為 0,表示客戶端斷開連接,關閉連接;如果讀取失敗(字節(jié)數(shù)小于 0),也關閉連接并打印斷開連接的信息;如果讀取成功,設置寫入事件,將讀取到的數(shù)據(jù)回顯給客戶端。如果是 WRITE 類型,表示數(shù)據(jù)寫入完成,設置讀取事件,準備從客戶端讀取下一次的數(shù)據(jù)。
io_uring_cq_advance(&ring, count); 告知內(nèi)核已經(jīng)處理完 count 個完成事件,通過將完成隊列的頭部指針遞增 count 個位置,以便內(nèi)核可以繼續(xù)使用完成隊列。
4.3性能對比測試
1.測試環(huán)境與方法
測試環(huán)境搭建:在一臺配備 Intel (R) Xeon (R) CPU E5 - 2682 v4 @ 2.50GHz 處理器、16GB 內(nèi)存、運行 Linux 5.10 內(nèi)核的服務器上進行測試。使用的存儲設備為 NVMe SSD,以確保 I/O 性能不受磁盤性能的過多限制。測試機器的網(wǎng)絡配置為千兆以太網(wǎng),以保證網(wǎng)絡傳輸?shù)姆€(wěn)定性。
2.測試方法設計
針對文件讀寫場景,使用 fio 工具進行測試。分別設置不同的 I/O 模式,包括阻塞 I/O、非阻塞 I/O、epoll 以及 io_uring。對于每種模式,進行多次測試,每次測試設置不同的文件大?。ㄈ?1MB、10MB、100MB)和 I/O 操作類型(如隨機讀、順序讀、隨機寫、順序?qū)懀?。在每次測試中,fio 工具會按照設定的參數(shù)進行 I/O 操作,并記錄操作的時間、吞吐量等性能指標。例如,在隨機讀測試中,fio 會隨機讀取文件中的數(shù)據(jù)塊,并統(tǒng)計單位時間內(nèi)讀取的數(shù)據(jù)量。
在網(wǎng)絡編程場景下,搭建一個簡單的 echo 服務器模型,分別使用 epoll 和 io_uring 實現(xiàn)??蛻舳送ㄟ^多線程模擬大量并發(fā)連接,向服務器發(fā)送數(shù)據(jù)并接收服務器回顯的數(shù)據(jù)。在測試過程中,逐漸增加并發(fā)連接數(shù),從 100 個連接開始,每次增加 100 個,直到達到 1000 個連接。使用 iperf 等工具測量不同并發(fā)連接數(shù)下的 QPS(每秒查詢率)、延遲等性能指標。iperf 工具會在客戶端和服務器之間建立 TCP 連接,發(fā)送一定量的數(shù)據(jù),并記錄數(shù)據(jù)傳輸?shù)乃俾?、延遲等信息。
3.測試結(jié)果分析
文件讀寫性能:在小文件(1MB)讀寫測試中,阻塞 I/O 由于線程阻塞等待 I/O 操作完成,導致其吞吐量最低,平均吞吐量約為 50MB/s。非阻塞 I/O 雖然避免了線程阻塞,但頻繁的輪詢使得 CPU 利用率較高,且由于 I/O 操作的碎片化,其吞吐量也不高,平均約為 80MB/s。epoll 在處理多個文件描述符的 I/O 事件時,通過高效的事件通知機制,提高了 I/O 操作的效率,平均吞吐量達到 120MB/s。
Part5.io_uring與其他 I/O 模型對比
5.1與阻塞 I/O 對比
阻塞 I/O 是最基礎的 I/O 模型,當應用程序執(zhí)行 I/O 操作(如 read 或 write)時,線程會被阻塞,直到 I/O 操作完成。在從磁盤讀取文件時,若數(shù)據(jù)尚未準備好,線程就會一直等待,期間無法執(zhí)行其他任務。這就好比一個人在餐廳點餐,必須坐在餐桌旁等待食物上桌,期間什么其他事情都做不了。阻塞 I/O 的優(yōu)點是編程簡單,邏輯清晰,但在高并發(fā)場景下,由于每個 I/O 請求都可能阻塞線程,導致系統(tǒng)的并發(fā)處理能力極低,資源利用率也不高。
而 io_uring 采用異步 I/O 機制,用戶程序提交 I/O 請求后,無需等待操作完成,就可以繼續(xù)執(zhí)行其他任務。當 I/O 操作完成后,內(nèi)核會將結(jié)果放入完成隊列(CQ),并通過事件通知機制通知用戶程序。這種方式大大提高了系統(tǒng)的并發(fā)處理能力,線程在等待 I/O 操作的過程中可以充分利用 CPU 資源執(zhí)行其他任務。在一個高并發(fā)的 Web 服務器中,使用阻塞 I/O 時,每個客戶端連接都需要一個獨立的線程來處理,當并發(fā)連接數(shù)增多時,線程資源將被大量消耗,系統(tǒng)性能會急劇下降;而使用 io_uring,服務器可以同時處理多個客戶端的 I/O 請求,無需為每個請求創(chuàng)建單獨的線程,提高了資源利用率和系統(tǒng)的并發(fā)處理能力。
5.2與非阻塞 I/O 對比
非阻塞 I/O 允許應用程序在 I/O 操作未完成時立即返回,線程不會被阻塞。當應用程序執(zhí)行 I/O 操作時,如果數(shù)據(jù)尚未準備好,系統(tǒng)會立即返回一個錯誤(如 EWOULDBLOCK 或 EAGAIN),應用程序可以繼續(xù)執(zhí)行其他任務,然后通過輪詢的方式再次嘗試 I/O 操作,直到數(shù)據(jù)準備好。這就像在餐廳點餐時,服務員告知需要等待一段時間,你可以先去做其他事情,然后時不時回來詢問食物是否準備好了。非阻塞 I/O 提高了系統(tǒng)的并發(fā)處理能力,但頻繁的輪詢會消耗大量的 CPU 資源,增加了系統(tǒng)的開銷,而且編程復雜度較高,需要處理更多的錯誤和狀態(tài)判斷。
io_uring 雖然也是異步 I/O 模型,但與非阻塞 I/O 有很大的不同。io_uring 通過提交隊列(SQ)和完成隊列(CQ)實現(xiàn)了高效的異步 I/O 操作,減少了系統(tǒng)調(diào)用和上下文切換的開銷。用戶程序只需將 I/O 請求寫入 SQ,內(nèi)核會異步處理這些請求,并將結(jié)果寫入 CQ,用戶程序從 CQ 中獲取結(jié)果,無需頻繁輪詢。在處理大量 I/O 請求時,非阻塞 I/O 的輪詢操作會導致 CPU 使用率急劇上升,而 io_uring 通過異步機制和事件通知,大大減少了 CPU 的消耗,提高了系統(tǒng)的性能和效率。io_uring 還支持批量操作和更多的系統(tǒng)調(diào)用,功能更加豐富和強大。
5.3與 epoll 對比
epoll 是 Linux 下常用的 I/O 多路復用技術,它允許一個進程同時監(jiān)視多個 I/O 描述符,當其中任何一個描述符就緒(即有數(shù)據(jù)可讀或可寫)時,進程就可以對其進行處理。epoll 采用事件驅(qū)動模式,使用紅黑樹管理需要監(jiān)聽的文件描述符,用一個事件隊列存放 I/O 就緒事件。調(diào)用 epoll_wait 時,內(nèi)核將已就緒的事件從內(nèi)核空間拷貝到用戶空間,用戶程序依次處理這些事件。若有大量事件就緒,需多次系統(tǒng)調(diào)用處理。
io_uring 和 epoll 在設計理念、實現(xiàn)機制和適用場景等方面都存在差異。從設計理念上看,epoll 主要用于 I/O 多路復用,解決一個進程監(jiān)視多個文件描述符的問題;而 io_uring 是更廣泛的異步I/O 框架,不僅用于事件通知,還能直接執(zhí)行 I/O 操作,旨在提高大規(guī)模并發(fā)I/O 操作性能。在實現(xiàn)機制上,epoll 通過內(nèi)核和用戶空間的數(shù)據(jù)拷貝來傳遞事件信息,而 io_uring 基于兩個共享環(huán)形緩沖區(qū)(SQ 和 CQ),用戶程序?qū)?I/O 請求寫入SQ,內(nèi)核處理完 I/O 操作后把結(jié)果寫入 CQ,減少了用戶態(tài)到內(nèi)核態(tài)的上下文切換次數(shù),且支持批量提交和處理 I/O 請求 。
在性能方面,在高并發(fā)場景下,io_uring 性能優(yōu)勢明顯,能極大減少用戶態(tài)到內(nèi)核態(tài)的切換次數(shù),測試顯示連接數(shù) 1000 及以上時,io_uring 性能開始超越 epoll,其極限性能單 core 在 24 萬 QPS 左右,而 epoll 單 core 只能達到 20 萬 QPS 左右 。在連接數(shù)超過 300 時,io_uring 的用戶態(tài)到內(nèi)核態(tài)的切換次數(shù)基本可忽略不計 。
不過在某些特殊場景(如 meltdown 和 spectre 漏洞未修復時 ),io_uring 相對 epoll 的性能提升不明顯甚至略有下降 。在適用場景上,epoll 適用于事件驅(qū)動的網(wǎng)絡編程場景,如監(jiān)視多個客戶端連接的服務器,像 Nginx、Redis 等都基于 epoll 構建;io_uring 則更適合處理網(wǎng)絡 I/O、文件 I/O、內(nèi)存映射等多種場景,目標是實現(xiàn) Linux 下一切基于文件概念的異步編程 。
io_uring 的編程復雜度相對較高,需要深入理解提交隊列和完成隊列的工作機制,手動管理 I/O 請求的提交、結(jié)果獲取,以及處理隊列初始化、事件提交與回收等操作,而 epoll 的編程相對簡單,開發(fā)者只需關注文件描述符的事件注冊(epoll_ctl)和事件處理(epoll_wait 返回后的邏輯) 。
Part6.使用 io_uring 的注意事項與挑戰(zhàn)
6.1內(nèi)核版本要求
io_uring 是 Linux 內(nèi)核提供的特性,對內(nèi)核版本有一定的要求。要充分利用 io_uring 的全部功能,建議使用 Linux 5.10 及以上版本的內(nèi)核 。在較低版本的內(nèi)核中,可能不支持 io_uring,或者雖然支持但存在功能缺陷和性能問題。在 Linux 5.4 版本之前,io_uring 的某些功能可能不夠穩(wěn)定,在處理某些復雜的 I/O 操作時可能會出現(xiàn)錯誤。如果你的系統(tǒng)內(nèi)核版本較低,在考慮使用 io_uring 之前,需要先升級內(nèi)核。內(nèi)核升級過程可能會涉及到系統(tǒng)兼容性、驅(qū)動程序等一系列問題,需要謹慎操作。在升級內(nèi)核之前,最好備份重要的數(shù)據(jù),并在測試環(huán)境中進行充分的測試,確保升級后的系統(tǒng)能夠正常運行。
6.2編程復雜度
雖然 io_uring 提供了強大的功能,但直接使用 io_uring 的系統(tǒng)調(diào)用進行編程是比較復雜的。它涉及到對提交隊列(SQ)和完成隊列(CQ)的詳細操作,以及對各種 I/O 請求參數(shù)的設置。開發(fā)者需要深入理解 io_uring 的工作原理和機制,才能正確地使用它。例如,在設置提交隊列條目(SQE)時,需要準確地設置操作碼、文件描述符、緩沖區(qū)地址、數(shù)據(jù)長度等參數(shù),任何一個參數(shù)設置錯誤都可能導致 I/O 操作失敗。在處理完成隊列條目(CQE)時,也需要正確地解析操作結(jié)果和錯誤碼,進行相應的處理。
為了簡化 io_uring 的使用,開發(fā)者可以借助 liburing 庫。liburing 庫是對 io_uring 系統(tǒng)調(diào)用的封裝,提供了更高級、更易用的 API。通過 liburing 庫,開發(fā)者可以更方便地初始化 io_uring 實例、提交 I/O 請求、獲取完成事件等。使用 liburing 庫中的 io_uring_queue_init 函數(shù)可以方便地初始化 io_uring 實例,使用 io_uring_get_sqe 函數(shù)可以從提交隊列中獲取一個空閑的 SQE,使用 io_uring_submit 函數(shù)可以提交 I/O 請求等。借助 liburing 庫,開發(fā)者可以降低編程的復雜度,提高開發(fā)效率,但同時也需要了解 liburing 庫的使用方法和相關的函數(shù)接口。
6.3應用適配難度
將現(xiàn)有的應用程序遷移到 io_uring 可能需要對代碼進行較大的修改,存在一定的適配難度。因為 io_uring 的編程模型與傳統(tǒng)的 I/O 模型有很大的不同,現(xiàn)有的應用程序可能是基于阻塞 I/O、非阻塞 I/O 或 I/O 多路復用等模型開發(fā)的,要遷移到 io_uring,需要重新設計和實現(xiàn) I/O 相關的部分代碼。
在一個基于 epoll 的 Web 服務器中,要將其遷移到 io_uring,需要重新編寫事件處理邏輯、I/O 請求的提交和處理流程等。這不僅需要對 io_uring 有深入的理解,還需要對現(xiàn)有的應用程序架構有清晰的認識,確保遷移過程中不會影響應用程序的功能和穩(wěn)定性。在遷移過程中,還可能會遇到一些兼容性問題,如與其他庫或組件的兼容性等,需要進行仔細的測試和調(diào)試。

























