日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 运维知识 > linux >内容正文

linux

深入理解Linux异步I/O框架 io_uring

發布時間:2024/4/11 linux 74 豆豆
生活随笔 收集整理的這篇文章主要介紹了 深入理解Linux异步I/O框架 io_uring 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

、

來源:云原生實驗室

hi,大家好,今天分享一篇Linux異步IO編程框架文章,對比IO復用的epoll框架,到底性能提高多少?讓我們看一看。

譯者序

?本文組合翻譯了以下兩篇文章的干貨部分,作為?io_uring?相關的入門參考:

  • How io_uring and eBPF Will Revolutionize Programming in Linux[1], ScyllaDB, 2020

  • An Introduction to the io_uring Asynchronous I/O Framework[2], Oracle, 2020

io_uring 是 2019 年 ?Linux 5.1 內核首次引入的高性能異步 I/O 框架,能顯著加速 I/O 密集型應用的性能。但如果你的應用 已經在使用 傳統 Linux AIO 了, 并且使用方式恰當, 那 io_uring ?并不會帶來太大的性能提升 —— 根據原文測試(以及我們 自己的復現),即便打開高級特性,也只有 5%。除非你真的需要這 5% 的額外性能,否則切換成 io_uring ?代價可能也挺大,因為要重寫應用來適配 io_uring(或者讓依賴的平臺或框架去適配,總之需要改代碼)。

既然性能跟傳統 AIO 差不多,那為什么還稱 io_uring 為革命性技術呢?

  • 它首先和最大的貢獻在于:統一了 Linux 異步 I/O 框架

    • Linux AIO ?只支持 direct I/O 模式的 存儲文件(storage file),而且主要用在 數據庫這一細分領域

    • io_uring 支持存儲文件和網絡文件(network sockets),也支持更多的異步系統調用 (accept/openat/stat/...),而非僅限于 read/write 系統調用。

    設計上是真正的異步 I/O,作為對比,Linux AIO 雖然也 是異步的,但仍然可能會阻塞,某些情況下的行為也無法預測;

    似乎之前 Windows 在這塊反而是領先的,更多參考:

    • 淺析開源項目之 io_uring[3],“分布式存儲”專欄,知乎

    • Is there really no asynchronous block I/O on Linux?[4],stackoverflow

    靈活性和可擴展性非常好,甚至能基于 io_uring 重寫所有系統調用,而 Linux AIO 設計時就沒考慮擴展性。

    eBPF 也算是異步框架(事件驅動),但與 io_uring 沒有本質聯系,二者屬于不同子系統, 并且在模型上有一個本質區別:

  • eBPF 對用戶是透明的,只需升級內核(到合適的版本), 應用程序無需任何改造

  • io_uring 提供了 新的系統調用和用戶空間 API,因此 需要應用程序做改造。

  • eBPF 作為動態跟蹤工具,能夠更方便地排查和觀測 io_uring 等模塊在執行層面的具體問題。

    本文介紹 Linux 異步 I/O 的發展歷史,io_uring 的原理和功能, 并給出了一些程序示例和性能壓測結果(我們在 5.10 內核做了類似測試,結論與原文差不多)。

    由于譯者水平有限,本文不免存在遺漏或錯誤之處。如有疑問,請查閱原文。

    以下是譯文。


    很多人可能還沒意識到,Linux 內核在過去幾年已經發生了一場革命。這場革命源于兩個激動人心的新接口的引入:eBPF 和 io_uring。我們認為,二者將會完全 改變應用與內核交互的方式,以及應用開發者思考和看待內核的方式。

    本文介紹 io_uring(我們在 ScyllaDB 中有 io_uring 的深入使用經驗),并略微提及一下 eBPF。

    1 Linux I/O 系統調用演進

    1.1 基于 fd 的阻塞式 I/O:read()/write()

    作為大家最熟悉的讀寫方式,Linux 內核提供了 基于文件描述符的系統調用, 這些描述符指向的可能是 存儲文件(storage file),也可能是 ?network sockets

    ssize_t?read(int?fd,?void?*buf,?size_t?count); ssize_t?write(int?fd,?const?void?*buf,?size_t?count);

    二者稱為 阻塞式系統調用(blocking system calls),因為程序調用 這些函數時會進入 sleep 狀態,然后被調度出去(讓出處理器),直到 I/O 操作完成:

    • 如果數據在文件中,并且文件內容 已經緩存在 page cache 中,調用會 立即返回

    • 如果數據在另一臺機器上,就需要通過網絡(例如 TCP)獲取,會阻塞一段時間;

    • 如果數據在硬盤上,也會阻塞一段時間。

    但很容易想到,隨著存儲 設備越來越快,程序越來越復雜, 阻塞式(blocking)已經這種最簡單的方式已經不適用了。

    1.2 非阻塞式 I/O:select()/poll()/epoll()

    阻塞式之后,出現了一些新的、非阻塞的系統調用,例如 select()、poll() 以及更新的 epoll()。應用程序在調用這些函數讀寫時不會阻塞,而是 立即返回,返回的是一個已經 ready 的文件描述符列表

    但這種方式存在一個致命缺點:只支持 network sockets 和 pipes ——epoll() 甚至連 storage files 都不支持。

    1.3 線程池方式

    對于 storage I/O,經典的解決思路是 thread pool[5]:主線程將 I/O 分發給 worker 線程,后者代替主線程進行阻塞式讀寫,主線程不會阻塞。

    這種方式的問題是 線程上下文切換開銷可能非常大,后面性能壓測會看到。

    1.4 Direct I/O(數據庫軟件):繞過 page cache

    隨后出現了更加靈活和強大的方式:數據庫軟件(database software) 有時 并不想使用操作系統的 page cache[6], 而是希望打開一個文件后, 直接從設備讀寫這個文件(direct access to the device)。這種方式稱為 直接訪問(direct access)或 直接 I/O(direct I/O),

    • 需要指定 ?O_DIRECT flag;

    • 需要 應用自己管理自己的緩存 —— 這正是數據庫軟件所希望的;

    • 是 ?zero-copy I/O,因為應用的緩沖數據直接發送到設備,或者直接從設備讀取。

    1.5 異步 IO(AIO)

    前面提到,隨著存儲設備越來越快,主線程和 worker 線性之間的上下文切換開銷占比越來越高?,F在市場上的一些設備,例如 Intel Optane[7]延遲已經低到和上下文切換一個量級(微秒 us)。換個方式描述, 更能讓我們感受到這種開銷:上下文每切換一次,我們就少一次 dispatch I/O 的機會。

    因此,Linux ?2.6 內核引入了異步 I/O(asynchronous I/O)接口, 方便起見,本文簡寫為 linux-aio。AIO ?**原理**是很簡單的:

    • 用戶通過 io_submit() 提交 I/O 請求,

    • 過一會再調用 io_getevents() 來檢查哪些 events 已經 ready 了。

    • 使程序員 能編寫完全異步的代碼。

    近期,Linux AIO 甚至支持了[8]epoll():也就是說 不僅能提交 storage I/O 請求,還能提交網絡 I/O 請求。照這樣發展下去,linux-aio似乎能成為一個王者。但由于它糟糕的演進之路,這個愿望幾乎不可能實現了。我們從 ?Linus 標志性的激烈言辭中就能略窺一斑

    Reply to: to support opening files asynchronously[9]

    So I think this is ridiculously ugly.

    AIO is a horrible ad-hoc design, with the main excuse being “other, less gifted people, made that design, and we are implementing it for compatibility because database people — who seldom have any shred of taste — actually use it”.

    — Linus Torvalds (on lwn.net)

    首先,作為數據庫從業人員,我們想借此機會為我們的沒品(lack of taste)向 Linus 道歉。但更重要的是,我們要進一步解釋一下 為什么 Linus 是對的:Linux AIO 確實問題纏身,

  • 只支持 O_DIRECT 文件,因此 對常規的非數據庫應用(normal, non-database applications) 幾乎是無用的

  • 接口在 設計時并未考慮擴展性。雖然可以擴展 —— 我們也確實這么做了 —— 但每加一個東西都相當復雜;

  • 雖然從 技術上說接口是非阻塞的,但實際上有很多可能的原因都會導致它阻塞[10],而且引發的方式難以預料。

  • 1.6 小結

    以上可以清晰地看出 Linux I/O 的演進:

    • 最開始是同步(阻塞式)系統調用;

    • 然后隨著 實際需求和具體場景,不斷加入新的異步接口,還要保持與老接口的兼容和協同工作。

    另外也看到,在非阻塞式讀寫的問題上 并沒有形成統一方案

  • Network socket 領域:添加一個異步接口,然后去輪詢(poll)請求是否完成(readiness);

  • Storage I/O 領域:只針對某一細分領域(數據庫)在某一特定時期的需求,添加了一個定制版的異步接口。

  • 這就是 Linux I/O 的演進歷史 —— 只著眼當前,出現一個問題就引入一種設計,而并沒有多少前瞻性 —— 直到 io_uring 的出現。

    2 io_uring

    io_uring 來自資深內核開發者 Jens Axboe 的想法,他在 Linux I/O stack 領域頗有研究。從最早的 patch aio: support for IO polling[11]可以看出,這項工作始于一個很簡單的觀察:隨著設備越來越快,中斷驅動(interrupt-driven)模式效率已經低于輪詢模式(polling for completions) —— 這也是高性能領域最常見的主題之一。

    • io_uring 的 基本邏輯與 linux-aio 是類似的:提供兩個接口,一個將 I/O 請求提交到內核,一個從內核接收完成事件。

    • 但隨著開發深入,它逐漸變成了一個完全不同的接口:設計者開始從源頭思考如何支持完全異步的操作

    2.1 與 Linux AIO 的不同

    io_uring 與 linux-aio 有著本質的不同:

  • 在設計上是真正異步的(truly asynchronous)。只要 設置了合適的 flag,它在 系統調用上下文中就只是將請求放入隊列, 不會做其他任何額外的事情, 保證了應用永遠不會阻塞。

  • 支持任何類型的 I/O:cached files、direct-access files 甚至 blocking sockets。

    由于設計上就是異步的(async-by-design nature),因此 無需 poll+read/write 來處理 sockets。只需提交一個阻塞式讀(blocking read),請求完成之后,就會出現在 completion ring。

  • 靈活、可擴展:基于 io_uring 甚至能重寫(re-implement)Linux 的每個系統調用。

  • 2.2 原理及核心數據結構:SQ/CQ/SQE/CQE

    每個 io_uring 實例都有 兩個環形隊列(ring),在內核和應用程序之間共享:

    • 提交隊列:submission queue (SQ)

    • 完成隊列:completion queue (CQ)

    這兩個隊列:

    • 都是 單生產者、單消費者,size 是 2 的冪次;

    • 提供 無鎖接口(lock-less access interface),內部使用 **內存屏障**做同步(coordinated with memory barriers)。

    使用方式

    • 請求

      • 應用創建 SQ entries (SQE),更新 SQ tail;

      • 內核消費 SQE,更新 SQ head。

    • 完成

      • 內核為完成的一個或多個請求創建 CQ entries (CQE),更新 CQ tail;

      • 應用消費 CQE,更新 CQ head。

      • 完成事件(completion events)可能以任意順序到達,到總是與特定的 SQE 相關聯的。

      • 消費 CQE 過程無需切換到內核態。

    2.3 帶來的好處

    io_uring 這種請求方式還有一個好處是:原來需要多次系統調用(讀或寫),現在變成批處理一次提交。

    還記得 Meltdown 漏洞嗎?當時我還寫了一篇文章[12]解釋為什么我們的 Scylla NoSQL 數據庫受影響很小:aio 已經將我們的 I/O 系統調用批處理化了。

    io_uring ?將這種批處理能力帶給了 storage I/O 系統調用之外的其他一些系統調用,包括:

    • read

    • write

    • send

    • recv

    • accept

    • openat

    • stat

    • 專用的一些系統調用,例如 fallocate

    此外,io_uring 使異步 I/O 的使用場景也不再僅限于數據庫應用, 普通的 非數據庫應用也能用。這一點值得重復一遍:

    雖然 io_uring 與 aio 有一些相似之處,但它的 擴展性和架構是革命性的:它 將異步操作的強大能力帶給了所有應用(及其開發者),而不再僅限于是數據庫應用這一細分領域

    我們的 CTO Avi Kivity 在 the Core C++ 2019 event 上 有一次關于 async 的分享[13]。核心點包括:從延遲上來說

  • 現代多核、多 CPU 設備,其內部本身就是一個基礎網絡;

  • **CPU 之間**是另一個網絡;

  • **CPU 和磁盤 I/O 之間**又是一個網絡。

  • 因此網絡編程采用異步是明智的,而現在開發自己的應用也應該考慮異步。這 從根本上改變了 Linux 應用的設計方式

    • 之前都是一段順序代碼流,需要系統調用時才執行系統調用,

    • 現在需要思考一個文件是否 ready,因而自然地引入 event-loop,不斷通過共享 buffer 提交請求和接收結果。

    2.4 三種工作模式

    io_uring 實例可工作在三種模式:

  • 中斷驅動模式(interrupt driven)

    默認模式??赏ㄟ^ io_uring_enter() 提交 I/O 請求,然后直接檢查 CQ 狀態判斷是否完成。

  • 輪詢模式(polled)

    Busy-waiting for an I/O completion,而不是通過異步 IRQ(Interrupt Request)接收通知。

    這種模式需要文件系統(如果有)和塊設備(block device)支持輪詢功能。相比中斷驅動方式,這種方式延遲更低(連系統調用都省了[14]), 但可能會消耗更多 CPU 資源。

    目前,只有指定了 O_DIRECT flag 打開的文件描述符,才能使用這種模式。當一個讀 或寫請求提交給輪詢上下文(polled context)之后,應用(application)必須調用io_uring_enter() 來輪詢 CQ 隊列,判斷請求是否已經完成。

    對一個 io_uring 實例來說, 不支持混合使用輪詢和非輪詢模式

  • 內核輪詢模式(kernel polled)

  • 這種模式中,會 ?創建一個內核線程(kernel thread)來執行 SQ 的輪詢工作。

    使用這種模式的 io_uring 實例, ?應用無需切到到內核態 就能觸發(issue)I/O 操作。通過 SQ 來提交 SQE,以及監控 CQ 的完成狀態,應用無需任何系統調用,就能提交和收割 I/O(submit and reap I/Os)。

    如果內核線程的空閑時間超過了用戶的配置值,它會通知應用,然后進入 idle 狀態。這種情況下,應用必須調用 io_uring_enter() 來喚醒內核線程。如果 I/O 一直很繁忙,內核線性是不會 sleep 的。

    2.5 io_uring 系統調用 API

    有三個:

    • io_uring_setup(2)

    • io_uring_register(2)

    • io_uring_enter(2)

    下面展開介紹。完整文檔見 manpage[15]

    2.5.1 io_uring_setup()

    執行異步 I/O 需要先 設置上下文

    int?io_uring_setup(u32?entries,?struct?io_uring_params?*p);

    這個系統調用

    • 創建一個 SQ 和一個 CQ

    • queue size 至少 entries 個元素,

    • 返回一個文件描述符,隨后用于在這個 io_uring 實例上執行操作。

    SQ 和 CQ 在應用和內核之間共享,避免了在初始化和完成 I/O 時(initiating and completing I/O)拷貝數據。

    參數 p:

    • 應用用來配置 io_uring,

    • 內核返回的 SQ/CQ 配置信息也通過它帶回來。

    io_uring_setup() 成功時返回一個文件描述符(fd)。應用隨后可以將這個 fd 傳給 mmap(2) 系統調用,來 map the submission and completion queues 或者傳給 to the io_uring_register() or io_uring_enter() system calls.

    2.5.2 io_uring_register()

    注冊用于異步 I/O 的 文件或用戶緩沖區(files or user buffers):

    int?io_uring_register(unsigned?int?fd,?unsigned?int?opcode,?void?*arg,?unsigned?int?nr_args);

    注冊文件或用戶緩沖區,使內核能 長時間持有對該文件在內核內部的數據結構引用(internal kernel data structures associated with the files), 或創建 應用內存的長期映射(long term mappings of application memory associated with the buffers), 這個操作只會在注冊時執行一次,而不是每個 I/O 請求都會處理,因此減少了 per-I/O overhead。

    注冊的緩沖區(buffer)性質
    • Registered buffers 將會 被鎖定在內存中(be locked in memory),并 計入用戶的 RLIMIT_MEMLOCK 資源限制。

    • 此外,每個 buffer 有 ?1GB 的大小限制

    • 當前,buffers 必須是 匿名、非文件后端的內存(anonymous, non-file-backed memory),例如 malloc(3) or mmap(2) with the MAP_ANONYMOUS flag set 返回的內存。

    • Huge pages 也是支持的。整個 huge page 都會被 pin 到內核,即使只用到了其中一部分。

    • 已經注冊的 buffer 無法調整大小,想調整只能先 unregister,再重新 register 一個新的。

    通過 eventfd() 訂閱 completion 事件

    可以用 eventfd(2) 訂閱 io_uring 實例的 completion events。將 eventfd 描述符通過這個系統調用注冊就行了。

    The credentials of the running application can be registered with io_uring which returns an id associated with those credentials. Applications wishing to share a ring between separate users/processes can pass in this credential id in the SQE personality field. If set, that particular SQE will be issued with these credentials.

    2.5.3 io_uring_enter()

    int?io_uring_enter(unsigned?int?fd,?unsigned?int?to_submit,?unsigned?int?min_complete,?unsigned?int?flags,?sigset_t?*sig);

    這個系統調用用于初始化和完成(initiate and complete)I/O,使用共享的 SQ 和 CQ。單次調用同時執行:

  • 提交新的 I/O 請求

  • 等待 I/O 完成

  • 參數:

  • fd 是 io_uring_setup() 返回的文件描述符;

  • to_submit 指定了 SQ 中提交的 I/O 數量;

  • 依據不同模式:

    • 默認模式,如果指定了 min_complete,會等待這個數量的 I/O 事件完成再返回;

    • 如果 io_uring 是 polling 模式,這個參數表示:

  • 0:要求內核返回當前以及完成的所有 events,無阻塞;

  • 非零:如果有事件完成,內核仍然立即返回;如果沒有完成事件,內核會 poll,等待指定的次數完成,或者這個進程的時間片用完。

  • 注意:對于 interrupt driven I/O, 應用無需進入內核就能檢查 CQ 的 event completions

    io_uring_enter() 支持很多操作,包括:

    • Open, close, and stat files

    • Read and write into multiple buffers or pre-mapped buffers

    • Socket I/O operations

    • Synchronize file state

    • Asynchronously monitor a set of file descriptors

    • Create a timeout linked to a specific operation in the ring

    • Attempt to cancel an operation that is currently in flight

    • Create I/O chains

    • Ordered execution within a chain

    • Parallel execution of multiple chains

    當這個系統調用返回時,表示一定數量的 SEQ 已經被消費和提交了,此時可以安全的重用隊列中的 SEQ。此時 IO 提交有可能還停留在異步上下文中,即實際上 SQE 可能還沒有被提交 —— 不過 用戶不用關心這些細節 —— 當隨后內核需要使用某個特定的 SQE 時,它已經復制了一份。

    2.6 高級特性

    io_uring 提供了一些用于特殊場景的高級特性:

  • File registration(文件注冊):每次發起一個指定文件描述的操 作,內核都需要 花費一些時鐘周期(cycles) 將文件描述符映射到內部表示。對于那些 **針對同一文件進行重復操作**的場景,io_uring 支持 提前注冊這些文件,后面直接查找就行了。

  • Buffer registration(緩沖區注冊):與 file registration 類 似,direct I/O 場景中,內核需要 map/unmap memory areas。io_uring 支持提前 注冊這些緩沖區(buffers)。

  • Poll ring(輪詢環形緩沖區):對于非??焓窃O備,處理中斷的開 銷是比較大的。io_uring 允許用戶關閉中斷,使用輪詢模式。前面“三種工作模式”小節 也介紹到了這一點。

  • Linked operations(鏈接操作):允許用戶發送串聯的請求。這兩 個請求同時提交,但后面的會等前面的處理完才開始執行。

  • 2.7 用戶空間庫 liburing

    `liburing`[16] 提供了一個簡單的高層 API, 可用于一些基本場景,應用程序避免了直接使用更底層的系統調用。此外,這個 API 還避免了一些重復操作的代碼,如設置 io_uring 實例。

    舉個例子,在 io_uring_setup() 的 manpage 描述中,調用這個系統調用獲得一個 ring 文 件描述符之后,應用必須調用 mmap() 來這樣的邏輯需要一段略長的代碼,而用liburing 的話,下面的函數已經將上述流程封裝好了:

    int?io_uring_queue_init(unsigned?entries,?struct?io_uring?*ring,?unsigned?flags);

    下一節來看兩個例子基于 liburing 的例子。

    3 基于 liburing 的示例應用

    編譯:

    $?git?clone?https://github.com/axboe/liburing.git $?git?co?-b?liburing-2.0?tags/liburing-2.0$?cd?liburing $?ls?examples/ io_uring-cp??io_uring-cp.c??io_uring-test??io_uring-test.c??link-cp??link-cp.c??Makefile??ucontext-cp??ucontext-cp.c$?make?-j4$?./examples/io_uring-test?<file> Submitted=4,?completed=4,?bytes=16384$?./examples/link-cp?<in-file>?<out-file>

    3.1 io_uring-test

    這個程序使用 4 個 SQE,從輸入文件中 讀取最多 16KB 數據。

    源碼及注釋

    為方便看清主要邏輯,忽略了一些錯誤處理代碼,完整代碼見io_uring-test.c[17]。

    /*?SPDX-License-Identifier:?MIT?*/ /**?Simple?app?that?demonstrates?how?to?setup?an?io_uring?interface,*?submit?and?complete?IO?against?it,?and?then?tear?it?down.**?gcc?-Wall?-O2?-D_GNU_SOURCE?-o?io_uring-test?io_uring-test.c?-luring*/ #include?"liburing.h"#define?QD????4?//?io_uring?隊列長度int?main(int?argc,?char?*argv[])?{int?i,?fd,?pending,?done;void?*buf;//?1.?初始化一個?io_uring?實例struct?io_uring?ring;ret?=?io_uring_queue_init(QD,????//?隊列長度&ring,?//?io_uring?實例0);????//?flags,0?表示默認配置,例如使用中斷驅動模式//?2.?打開輸入文件,注意這里指定了?O_DIRECT?flag,內核輪詢模式需要這個?flag,見前面介紹fd?=?open(argv[1],?O_RDONLY?|?O_DIRECT);struct?stat?sb;fstat(fd,?&sb);?//?獲取文件信息,例如文件長度,后面會用到//?3.?初始化?4?個讀緩沖區ssize_t?fsize?=?0;?????????????//?程序的最大讀取長度struct?iovec?*iovecs?=?calloc(QD,?sizeof(struct?iovec));for?(i?=?0;?i?<?QD;?i++)?{if?(posix_memalign(&buf,?4096,?4096))return?1;iovecs[i].iov_base?=?buf;??//?起始地址iovecs[i].iov_len?=?4096;??//?緩沖區大小fsize?+=?4096;}//?4.?依次準備?4?個?SQE?讀請求,指定將隨后讀入的數據寫入?iovecs?struct?io_uring_sqe?*sqe;offset?=?0;i?=?0;do?{sqe?=?io_uring_get_sqe(&ring);??//?獲取可用?SQEio_uring_prep_readv(sqe,????????//?用這個?SQE?準備一個待提交的?read?操作fd,?????????//?從?fd?打開的文件中讀取數據&iovecs[i],?//?iovec?地址,讀到的數據寫入?iovec?緩沖區1,??????????//?iovec?數量offset);????//?讀取操作的起始地址偏移量offset?+=?iovecs[i].iov_len;????//?更新偏移量,下次使用i++;if?(offset?>?sb.st_size)????????//?如果超出了文件大小,停止準備后面的?SQEbreak;}?while?(1);//?5.?提交?SQE?讀請求ret?=?io_uring_submit(&ring);???????//?4?個?SQE?一次提交,返回提交成功的?SQE?數量if?(ret?<?0)?{fprintf(stderr,?"io_uring_submit:?%s\n",?strerror(-ret));return?1;}?else?if?(ret?!=?i)?{fprintf(stderr,?"io_uring_submit?submitted?less?%d\n",?ret);return?1;}//?6.?等待讀請求完成(CQE)struct?io_uring_cqe?*cqe;done?=?0;pending?=?ret;fsize?=?0;for?(i?=?0;?i?<?pending;?i++)?{io_uring_wait_cqe(&ring,?&cqe);??//?等待系統返回一個讀完成事件done++;if?(cqe->res?!=?4096?&&?cqe->res?+?fsize?!=?sb.st_size)?{fprintf(stderr,?"ret=%d,?wanted?4096\n",?cqe->res);}fsize?+=?cqe->res;io_uring_cqe_seen(&ring,?cqe);???//?更新?io_uring?實例的完成隊列}//?7.?打印統計信息printf("Submitted=%d,?completed=%d,?bytes=%lu\n",?pending,?done,?(unsigned?long)?fsize);//?8.?清理工作close(fd);io_uring_queue_exit(&ring);return?0; }

    其他說明

    代碼中已經添加了注釋,這里再解釋幾點:

    • 每個 SQE 都執行一個 allocated buffer,后者是用 iovec 結構描述的;

    • 第 3 & 4 步:初始化所有 SQE,用于接下來的 IORING_OP_READV 操作,后者提供了 readv(2) 系統調用的異步接口。

    • 操作完成之后,這個 SQE iovec buffer 中存放的是相關 readv 操作的結果;

    • 接下來調用 io_uring_wait_cqe() 來 reap CQE,并通過 cqe->res 字段驗證讀取的字節數;

    • io_uring_cqe_seen() 通知內核這個 CQE 已經被消費了。

    3.2 link-cp

    link-cp 使用 io_uring 高級特性 SQE chaining 特性來復制文件。

    I/O chain

    io_uring 支持創建 I/O chain。一個 chain 內的 I/O 是順序執行的,多個 I/O chain 可以并行執行。

    io_uring_enter() manpage 中對 IOSQE_IO_LINK 有 詳細解釋[18]

    When this flag is specified, it forms a link with the next SQE in the submission ring. That next SQE will not be started before this one completes. This, in effect, forms a chain of SQEs, which can be arbitrarily long. The tail of the chain is denoted by the first SQE that does not have this flag set. This flag has no effect on previous SQE submissions, nor does it impact SQEs that are outside of the chain tail. This means that multiple chains can be executing in parallel, or chains and individual SQEs. Only members inside the chain are serialized. A chain of SQEs will be broken, if any request in that chain ends in error. io_uring considers any unexpected result an error. This means that, eg, a short read will also terminate the remainder of the chain. If a chain of SQE links is broken, the remaining unstarted part of the chain will be terminated and completed with -ECANCELED as the error code. Available since 5.3.

    為實現復制文件功能,link-cp 創建一個長度為 2 的 SQE chain。

    • 第一個 SQE 是一個讀請求,將數據從輸入文件讀到 buffer;

    • 第二個請求,與第一個請求是 linked,是一個寫請求,將數據從 buffer 寫入輸出文件。

    源碼及注釋

    /*?SPDX-License-Identifier:?MIT?*/ /**?Very?basic?proof-of-concept?for?doing?a?copy?with?linked?SQEs.?Needs?a*?bit?of?error?handling?and?short?read?love.*/ #include?"liburing.h"#define?QD????64?????????//?io_uring?隊列長度 #define?BS????(32*1024)struct?io_data?{size_t?offset;int?index;struct?iovec?iov; };static?int?infd,?outfd; static?unsigned?inflight;//?創建一個?read->write?SQE?chain static?void?queue_rw_pair(struct?io_uring?*ring,?off_t?size,?off_t?offset)?{struct?io_uring_sqe?*sqe;struct?io_data?*data;void?*ptr;ptr?=?malloc(size?+?sizeof(*data));data?=?ptr?+?size;data->index?=?0;data->offset?=?offset;data->iov.iov_base?=?ptr;data->iov.iov_len?=?size;sqe?=?io_uring_get_sqe(ring);????????????????????????????//?獲取可用?SQEio_uring_prep_readv(sqe,?infd,?&data->iov,?1,?offset);???//?準備?read?請求sqe->flags?|=?IOSQE_IO_LINK;?????????????????????????????//?設置為?LINK?模式io_uring_sqe_set_data(sqe,?data);????????????????????????//?設置?datasqe?=?io_uring_get_sqe(ring);????????????????????????????//?獲取另一個可用?SQEio_uring_prep_writev(sqe,?outfd,?&data->iov,?1,?offset);?//?準備?write?請求io_uring_sqe_set_data(sqe,?data);????????????????????????//?設置?data }//?處理完成(completion)事件:釋放 SQE 的內存緩沖區,通知內核已經消費了 CQE。 static?int?handle_cqe(struct?io_uring?*ring,?struct?io_uring_cqe?*cqe)?{struct?io_data?*data?=?io_uring_cqe_get_data(cqe);???????//?獲取?CQEdata->index++;if?(cqe->res?<?0)?{if?(cqe->res?==?-ECANCELED)?{queue_rw_pair(ring,?BS,?data->offset);inflight?+=?2;}?else?{printf("cqe?error:?%s\n",?strerror(cqe->res));ret?=?1;}}if?(data->index?==?2)?{????????//?read->write?chain?完成,釋放緩沖區內存void?*ptr?=?(void?*)?data?-?data->iov.iov_len;free(ptr);}io_uring_cqe_seen(ring,?cqe);??//?通知內核已經消費了?CQE?事件return?ret; }static?int?copy_file(struct?io_uring?*ring,?off_t?insize)?{struct?io_uring_cqe?*cqe;size_t?this_size;off_t?offset;offset?=?0;while?(insize)?{??????????????????????//?數據還沒處理完int?has_inflight?=?inflight;??????//?當前正在進行中的?SQE?數量int?depth;??//?SQE?閾值,當前進行中的?SQE?數量(inflight)超過這個值之后,需要阻塞等待?CQE?完成while?(insize?&&?inflight?<?QD)?{?//?數據還沒處理完,io_uring?隊列也還沒用完this_size?=?BS;if?(this_size?>?insize)???????//?最后一段數據不足?BS?大小this_size?=?insize;queue_rw_pair(ring,?this_size,?offset);?//?創建一個?read->write?chain,占用兩個?SQEoffset?+=?this_size;insize?-=?this_size;inflight?+=?2;????????????????//?正在進行中的?SQE?數量?+2}if?(has_inflight?!=?inflight)?????//?如果有新創建的?SQE,io_uring_submit(ring);????????//?就提交給內核if?(insize)???????????????????????//?如果還有?data?等待處理,depth?=?QD;???????????????????//?閾值設置 SQ 的隊列長度,即 SQ 隊列用完才開始阻塞等待 CQE;else??????????????????????????????//?data?處理已經全部提交,depth?=?1;????????????????????//?閾值設置為?1,即只要還有?SQE?未完成,就阻塞等待?CQE//?下面這個?while?只有?SQ?隊列用完或?data?全部提交之后才會執行到while?(inflight?>=?depth)?{???????//?如果所有?SQE?都已經用完,或者所有?data?read->write?請求都已經提交io_uring_wait_cqe(ring,?&cqe);//?等待內核?completion?事件handle_cqe(ring,?cqe);????????//?處理 completion 事件:釋放 SQE 內存緩沖區,通知內核 CQE 已消費inflight--;???????????????????//?正在進行中的?SQE?數量?-1}}return?0; }static?int?setup_context(unsigned?entries,?struct?io_uring?*ring)?{io_uring_queue_init(entries,?ring,?0);return?0; }static?int?get_file_size(int?fd,?off_t?*size)?{struct?stat?st;if?(fstat(fd,?&st)?<?0)return?-1;if?(S_ISREG(st.st_mode))?{*size?=?st.st_size;return?0;}?else?if?(S_ISBLK(st.st_mode))?{unsigned?long?long?bytes;if?(ioctl(fd,?BLKGETSIZE64,?&bytes)?!=?0)return?-1;*size?=?bytes;return?0;}return?-1; }int?main(int?argc,?char?*argv[])?{struct?io_uring?ring;off_t?insize;int?ret;infd?=?open(argv[1],?O_RDONLY);outfd?=?open(argv[2],?O_WRONLY?|?O_CREAT?|?O_TRUNC,?0644);if?(setup_context(QD,?&ring))return?1;if?(get_file_size(infd,?&insize))return?1;ret?=?copy_file(&ring,?insize);close(infd);close(outfd);io_uring_queue_exit(&ring);return?ret; }

    其他說明

    代碼中實現了三個函數:

  • copy_file():高層復制循環邏輯;它會調用 queue_rw_pair(ring, this_size, offset) 來構造 SQE pair;并通過一次 io_uring_submit() 調用將所有構建的 SQE pair 提交。

    這個函數維護了一個最大 DQ 數量的 inflight SQE,只要數據 copy 還在進行中;否則,即數據已經全部讀取完成,就開始等待和收割所有的 CQE。

  • queue_rw_pair() 構造一個 read-write SQE pair.

    read SQE 的 IOSQE_IO_LINK flag 表示開始一個 chain,write SQE 不用設置這個 flag,標志著這個 chain 的結束。用戶 data 字段設置為同一個 data 描述符,并且在隨后的 completion 處理中會用到。

  • handle_cqe() 從 CQE 中提取之前由 ?queue_rw_pair() 保存的 data 描述符,并在描述符中記錄處理進展(index)。

    如果之前請求被取消,它還會重新提交 read-write pair。

    一個 CQE pair 的兩個 member 都處理完成之后(index==2),釋放共享的 data descriptor。最后通知內核這個 CQE 已經被消費。

  • 4 io_uring 性能壓測(基于 fio)

    對于已經在使用 linux-aio 的應用,例如 ScyllaDB,不要期望換成 io_uring 之后能獲得大幅的性能提升,這是因為:io_uring 性能相關的底層機制與 linux-aio 并無本質不同(都是異步提交,輪詢結果)。

    在此,本文也希望使讀者明白:io_uring ?首先和最重要的貢獻在于:將 linux-aio 的所有優良特性帶給了普羅大眾(而非局限于數據庫這樣的細分領域)。

    4.1 測試環境

    本節使用 fio 測試 4 種模式:

  • synchronous reads

  • posix-aio (implemented as a thread pool)

  • linux-aio

  • io_uring

  • 硬件:

    • NVMe 存儲設備,物理極限能打到 ?3.5M IOPS

    • 8 核處理器

    4.2 場景一:direct I/O 1KB 隨機讀(繞過 page cache)

    第一組測試中,我們希望所有的讀請求都能 命中存儲設備(all reads to hit the storage), 完全繞開操作系統的頁緩存(page cache)。

    測試配置:

    • 8 個 CPU 執行 72 fio job,

    • 每個 job 隨機讀取 4 個文件,

    • iodepth=8(number of I/O units to keep in flight against the file.)。

    這種配置 保證了 CPU 處于飽和狀態,便于觀察 I/O 性能。如果 CPU 數量足夠多,那每組測試都可能會打滿設備帶寬,結果對 I/O 壓測就沒意義了。

    表 1. Direct I/O(繞過系統頁緩存):1KB 隨機讀,CPU 100% 下的 I/O 性能

    backendIOPScontext switchesIOPS ±% vs io_uring
    sync814,00027,625,004-42.6%
    posix-aio (thread pool)433,00064,112,335-69.4%
    linux-aio1,322,00010,114,149-6.7%
    io_uring (basic)1,417,00011,309,574
    io_uring (enhanced)1,486,00011,483,4684.9%

    幾點分析:

  • io_uring 相比 linux-aio 確實有一定提升,但并非革命性的。

  • 開啟高級特性,例如 buffer & file registration 之后性能有進一步提升 —— 但也還 沒有到為了這些性能而重寫整個應用的地步,除非你是搞數據庫研發,想榨取硬件的最后一分性能。

  • io_uring and linux-aio ?都比同步 read 接口快 2 倍,而后者又比 posix-aio 快 2 倍 —— 初看有點差異。但看看 上下文切換次數,就不難理解為什么 posix-aio 這么慢了。

    • 同步 read 性能差是因為:在這種沒有 page cache 的情況下,每次 read 系統調用都會阻塞,因此就會涉及一次上下文切換。

    • posix-aio 性能更差是因為:不僅內核和應用程序之間要頻繁上下文切換,線程池的 多個線程之間也在頻繁切換

    4.2 場景二:buffered I/O 1KB 隨機讀(數據提前加載到內存,100% hot cache)

    第二組測試 buffered I/O:

  • 將文件數據提前加載到內存,然后再測隨機讀。

    • 由于 數據全部在 page cache,因此 同步 read 永遠不會阻塞

    • 這種場景下,我們預期 同步讀和 io_uring 的性能差距不大(都是最好的)。

    其他測試條件不變。

    表 2. Buffered I/O(數據全部來自 page cache,100% hot cache):1KB 隨機讀,100% CPU 下的 I/O 性能

    BackendIOPScontext switchesIOPS ±% vs io_uring
    sync4,906,000105,797-2.3%
    posix-aio (thread pool)1,070,000114,791,187-78.7%
    linux-aio4,127,000105,052-17.9%
    io_uring5,024,000106,683

    結果分析:

  • 同步讀和 io_uring 性能差距確實很小,二者都是最好的。

    但注意, **實際的應用**不可能一直 100% 時間執行 IO 操作,因此 基于同步讀的真實應用性能 還是要比基于 io_uring 要差的,因為 io_uring 會將多個系統調用批處理化。

  • posix-aio 性能最差,直接原因是 上下文切換次數太多,這也和場景相關:在這種 ?CPU 飽和的情況下,它的線程池反而是累贅,會完全拖慢性能。

  • linux-aio 并 不是針對 buffered I/O 設計的,在這種 page cache 直接返回的場景, 它的 異步接口反而會造成性能損失 —— 將操作分 為 dispatch 和 consume 兩步不但沒有性能收益,反而有額外開銷。

  • 4.3 性能測試小結

    最后再次提醒,本節是極端應用/場景( 100% CPU + 100% cache miss/hit)測試, 真實應用的行為通常處于同步讀和異步讀之間:時而一些阻塞操作,時而一些非阻塞操作。但不管怎么說,用了 io_uring 之后,用戶就無需擔心同步和異步各占多少比例了,因為它 在任何場景下都表現良好。

  • 如果操作是非阻塞的,io_uring 不會有額外開銷;

  • 如果操作是阻塞式的,也沒關系,io_uring 是完全異步的,并且不依賴線程池或昂貴的上下文切換來實現這種異步能力;

  • 本文測試的都是隨機讀,但對 其他類型的操作,io_uring 表現也是非常良好的。例如:

  • 打開/關閉文件

  • 設置定時器

  • 通過 network sockets 傳輸數據

  • 而且 使用的是同一套 io_uring 接口。

    4.4 ScyllaDB 與 io_uring

    Scylla 重度依賴 direct I/O,從一開始就使用 linux-aio。在我們轉向 io_uring 的過程中,最開始測試顯示對某些 workloads,能取得 50% 以上的性能提升。但 深入研究之后發現,這是因為我們 之前的 linux-aio 用的不夠好。這也揭示了一個 經常被忽視的事實:獲得高性能沒有那么難(前提是你得弄對了)。在對比 io_uring 和 linux-aio 應用之后,我們 很快改進了一版,二者的性能差距就消失了。但坦率地說,解決這個問題 需要一些工作量,因為要改動一個已經使用 了很多年的基于 linux-aio 的接口。而對 io_uring 應用來說,做類似的改動是輕而 易舉的。

    以上只是一個場景,io_uring 相比 linux-aio 的 **優勢**是能應用于 file I/O 之外的場景。此外,它還自帶了特殊的 高性能[19] 接口,例如 buffer registration、file registration、輪詢模式等等。

    啟用 io_uring 高級特性之后,我們看到性能確實有提升:Intel Optane 設備,單個 CPU ?讀取 512 字節,觀察到 5% 的性能提升。與 表 1 & 2 對得上。雖然 5% 的提升 看上去不是太大,但對于希望壓榨出硬件所有性能的數據庫來說,還是非常寶貴的。

    linux-aio:

    Throughput ? ? ? ? : ? ? ?330 MB/s
    ? ? Lat average ? ? ? ?: ? ? 1549 usec
    ? ? Lat quantile= ?0.5 : ? ? 1547 usec
    ? ? Lat quantile= 0.95 : ? ? 1694 usec
    ? ? Lat quantile= 0.99 : ? ? 1703 usec
    ? ? Lat quantile=0.999 : ? ? 1950 usec
    ? ? Lat max ? ? ? ? ? ?: ? ? 2177 usec

    io_uring, with buffer and file registration and poll:

    Throughput ? ? ? ? : ? ? ?346 MB/s
    Lat average ? ? ? ?: ? ? 1470 usec
    Lat quantile= 0.5 ?: ? ? 1468 usec
    Lat quantile= 0.95 : ? ? 1558 usec
    Lat quantile= 0.99 : ? ? 1613 usec
    Lat quantile=0.999 : ? ? 1674 usec
    Lat max ? ? ? ? ? ?: ? ? 1829 usec

    使用 1 個 CPU 從 Intel Optane 設備讀取 512 字節。1000 并發請求。linux-aio 和 io_uring basic interface 性能差異很小。? 但啟用 io_uring 高級特性后,有 5% 的性能差距。

    5 eBPF

    eBPF 也是一個 事件驅動框架(因此也是異步的),允許用戶空間程序動態向內核注入字節碼,主要有兩個使用場景:

  • Networking:本站[20] 已經有相當多的文章

  • Tracing & Observability:例如 bcc[21] 等工具

  • eBPF 在內核 ?4.9 首次引入,4.19 以后功能已經很強大。更多關于 eBPF 的演進信息,可參考:(譯)大規模微服務利器:eBPF + Kubernetes(KubeCon, 2020)。

    談到與 io_uring 的結合,就是用 bcc 之類的工具跟蹤一些 I/O 相關的內核函數,例如:

  • Trace how much time an application spends sleeping, and what led to those sleeps. (wakeuptime)

  • Find all programs in the system that reached a particular place in the code (trace)

  • Analyze network TCP throughput aggregated by subnet (tcpsubnet)

  • Measure how much time the kernel spent processing softirqs (softirqs)

  • Capture information about all short-lived files, where they come from, and for how long they were opened (filelife)

  • 6 結束語

    io_uring 和 eBPF 這兩大特性 將給 Linux 編程帶來革命性的變化。有了這兩個特性的加持,開發者就能更充分地利用 Amazon i3en meganode systems[22]之類的多核/多處理器系統,以及 Intel Optane 持久存儲[23]之類的 us 級延遲存儲設備。

    參考資料

    • Efficient IO with io_uring[24], pdf

    • Ringing in a new asynchronous I/O API[25], lwn.net

    • The rapid growth of io_uring[26], lwn.net

    • System call API[27], manpage

    引用鏈接

    [1]

    How io_uring and eBPF Will Revolutionize Programming in Linux: https://thenewstack.io/how-io_uring-and-ebpf-will-revolutionize-programming-in-linux

    [2]

    An Introduction to the io_uring Asynchronous I/O Framework: https://medium.com/oracledevs/an-introduction-to-the-io-uring-asynchronous-i-o-framework-fad002d7dfc1

    [3]

    淺析開源項目之 io_uring: https://zhuanlan.zhihu.com/p/361955546

    [4]

    Is there really no asynchronous block I/O on Linux?: https://stackoverflow.com/questions/13407542/is-there-really-no-asynchronous-block-i-o-on-linux

    [5]

    thread pool: https://en.wikipedia.org/wiki/Thread_pool

    [6]

    并不想使用操作系統的 page cache: https://www.scylladb.com/2018/07/26/how-scylla-data-cache-works

    [7]

    Intel Optane: https://pcper.com/2018/12/intels-optane-dc-persistent-memory-dimms-push-latency-closer-to-dram

    [8]

    Linux AIO 甚至支持了: https://lwn.net/Articles/742978/

    [9]

    to support opening files asynchronously: https://lwn.net/Articles/671657/

    [10]

    很多可能的原因都會導致它阻塞: https://lwn.net/Articles/724198

    [11]

    aio: support for IO polling: https://lwn.net/ml/linux-fsdevel/20181221192236.12866-9-axboe@kernel.dk

    [12]

    一篇文章: https://www.scylladb.com/2018/01/07/cost-of-avoiding-a-meltdown/

    [13]

    有一次關于 async 的分享: https://www.scylladb.com/2020/03/26/avi-kivity-at-core-c-2019

    [14]

    連系統調用都省了: https://www.phoronix.com/scan.php?page=news_item&px=Linux-io_uring-Fast-Efficient

    [15]

    manpage: https://github.com/axboe/liburing/tree/master/man

    [16]

    liburing: https://github.com/axboe/liburing/

    [17]

    io_uring-test.c: https://github.com/axboe/liburing/blob/liburing-2.0/examples/io_uring-test.c

    [18]

    詳細解釋: https://www.mankier.com/2/io_uring_enter#Description-IOSQE_IO_LINK

    [19]

    高性能: https://www.p99conf.io/

    [20]

    本站: https://arthurchiao.art

    [21]

    bcc: https://github.com/iovisor/bcc

    [22]

    Amazon i3en meganode systems: https://www.scylladb.com/2019/05/28/aws-new-i3en-meganode

    [23]

    Intel Optane 持久存儲: https://www.scylladb.com/2017/09/27/intel-optane-scylla-providing-speed-memory-database-persistency

    [24]

    Efficient IO with io_uring: https://kernel.dk/io_uring.pdf

    [25]

    Ringing in a new asynchronous I/O API: https://lwn.net/Articles/776703/

    [26]

    The rapid growth of io_uring: https://lwn.net/Articles/810414/

    [27]

    System call API: https://github.com/axboe/liburing/tree/master/man

    原文鏈接:https://arthurchiao.art/blog/intro-to-io-uring-zh/

    - END -


    看完一鍵三連在看轉發,點贊

    是對文章最大的贊賞,極客重生感謝你

    推薦閱讀

    滴水石穿非一日之功


    深入理解 Linux的 I/O 系統


    實戰|QUIC協議助力騰訊業務提速30%


    ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 求點贊,在看,分享三連

    總結

    以上是生活随笔為你收集整理的深入理解Linux异步I/O框架 io_uring的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。