原生的 Linux 異步文件操做,io_uring 嚐鮮體驗

Linux異步IO的歷史

異步IO一直是 Linux 系統的痛。Linux 很早就有 POSIX AIO 這套異步IO實現,但它是在用戶空間本身開用戶線程模擬的,效率極其低下。後來在 Linux 2.6 引入了真正的內核級別支持的異步IO實現(Linux aio),可是它只支持 Direct IO,只支持磁盤文件讀寫,並且對文件大小還有限制,總之各類麻煩。到目前爲止(2019年5月),libuv 仍是在用pthread+preadv的形式實現異步IO。node

隨着 Linux 5.1 的發佈,Linux 終於有了本身好用的異步IO實現,而且支持大多數文件類型(磁盤文件、socket,管道等),這個就是本文的主角:io_uringlinux

IOCP

於IO多路複用模型 epoll 不一樣,io_uring 的思想更相似於 Windows 上的 IOCP。用快遞來舉例:同步模型就是你從在電商平臺下單前,就在你家樓下一直等,直到快遞公司把貨送到樓下,你再把東西帶上樓。epoll 相似於你下單,快遞公司送到樓下,通知你能夠去樓下取貨了,這時你下樓把東西帶上來。雖然仍是須要用戶下樓取貨(有一段同步讀寫的時間),可是因爲不須要等快遞在路上的時間,效率已經有很是大的提高。可是,epoll不適用於磁盤IO,由於磁盤文件老是可讀的。git

而 IOCP 就是一步到位,直接送貨上門,連下樓取的動做都不須要。整個過程徹底是非阻塞的。github

io_uring 的簡單使用

io_uring 是一套系統調用接口,雖然總共就3個系統調用,但實際使用卻很是複雜。這裏直接介紹封裝過便於用戶使用的 liburing數組

在嘗試前請首先確認本身的 Linux 內核版本在 5.1 以上(uname -r)。liburing 須要本身編譯(以後可能會被各大Linux發行版以軟件包的形式收錄),git clone 後直接 ./configure && sudo make install 就行了。瀏覽器

io_uring 結構初始化

liburing 提供了本身的核心結構 io_uring,它內部封裝了 io_uring 本身的文件描述符(fd)以及其餘與內核通訊所需變量。服務器

struct io_uring {
    struct io_uring_sq sq;
    struct io_uring_cq cq;
    int ring_fd;
};

使用以前須要先初始化,使用 io_uring_queue_init 初始化此結構。異步

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

如函數名稱所示, io_uring 是一個循環隊列(ring_buffer)。第一個參數 entries 表示隊列大小(實際空間可能比用戶指定的大);第二個參數 ring 就是須要初始化的 io_uring 結構指針;第三個參數 flags 是標誌參數,無特殊須要傳 0 便可。例如socket

#include <liburing.h>
struct io_uring ring;
io_uring_queue_init(32, &ring, 0);

提交讀、寫請求

首先使用 io_uring_get_sqe 獲取 sqe 結構。async

extern struct io_uring_sqe *io_uring_get_sqe(struct io_uring *ring);

一個 sqe(submission queue entry)表明一次 IO 請求,佔用循環隊列一個空位。io_uring 隊列滿時 io_uring_get_sqe 會返回 NULL,注意錯誤處理。注意這裏的隊列是指未提交的請求,已提交的(但未完成)請求不佔位置。

struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);

而後使用 io_uring_prep_readvio_uring_prep_writev 初始化 sqe 結構。

static inline void io_uring_prep_readv(struct io_uring_sqe *sqe, int fd,
                       const struct iovec *iovecs,
                       unsigned nr_vecs, off_t offset);
static inline void io_uring_prep_writev(struct io_uring_sqe *sqe, int fd,
                    const struct iovec *iovecs,
                    unsigned nr_vecs, off_t offset);

第一個參數 sqe 即前面獲取的 sqe 結構指針;fd 爲須要讀寫的文件描述符,能夠是磁盤文件也能夠是socket;iovecs 爲 iovec 數組,具體使用請參照 readv 和 writevnr_vecs 爲 iovecs 數組元素個數,offset 爲文件操做的偏移量。

能夠看到這兩個函數徹底按照 preadvpwritev 設計,語義也相同,因此很好上手。須要注意的是,若是須要順序讀寫文件,偏移量 offset 須要程序本身維護。

struct iovec iov = {
    .iov_base = "Hello world",
    .iov_len = strlen("Hello world"),
};
io_uring_prep_writev(sqe, fd, &iov, 1, 0);

初始化 sqe 後,能夠用 io_uring_sqe_set_data,傳入你本身的數據,通常是一個 malloc 獲得的指針,C++ 裏面能夠直接傳 this。

static inline void io_uring_sqe_set_data(struct io_uring_sqe *sqe, void *data);

注意 prep_* 中會 memset(0),因此必定要先 prep_*set_data。筆者這裏糾結了兩個小時。

準備好 sqe 後便可使用 io_uring_submit 提交請求。

extern int io_uring_submit(struct io_uring *ring);

你能夠初始化多個 sqe 而後一次性 submit

io_uring_submit(&ring);

完成 IO 請求

io_uring_submit 都是異步操做,不會阻塞當前線程。那麼如何得知提交的操做什麼時候完成呢?liburing 提供了函數 io_uring_peek_cqeio_uring_wait_cqe 兩個函數獲取當前已完成的 IO 操做。

extern int io_uring_peek_cqe(struct io_uring *ring,
    struct io_uring_cqe **cqe_ptr);
extern int io_uring_wait_cqe(struct io_uring *ring,
    struct io_uring_cqe **cqe_ptr);

第一個參數是 io_uring 結構指針;第二個參數 cqe_ptr 是輸出參數,是 cqe 指針變量的地址。

cqe(completion queue entry)標記一個已完成的 IO 操做,同時也記錄的以前傳入的用戶數據。每一個 cqe 都與前面的 sqe 對應。

這兩個函數,io_uring_peek_cqe 若是沒有已完成的 IO 操做時,也會當即返回,cqe_ptr 被置空;而
io_uring_wait_cqe 會阻塞線程,等待 IO 操做完成。

for (;;) {
    io_uring_peek_cqe(&ring, &cqe);
    if (!cqe) {
        puts("Waiting...");
        // accept 新鏈接,作其餘事
    } else {
        puts("Finished.");
        break;
    }
}

上文簡單起見用忙等待作示例,在實際應用場景中應該是一個事件循環,瀏覽器、nodejs 給咱們內部隱藏了事件循環的實現,而寫 C/C++ 語言只能咱們本身作。

可經過 io_uring_cqe_get_data 獲取前面給 sqe 設置的用戶數據。

static inline void *io_uring_cqe_get_data(struct io_uring_cqe *cqe);

默認狀況下 IO 完成事件不會從隊列中清除,致使 io_uring_peek_cqe 會獲取到相同事件,使用 io_uring_cqe_seen 標記該事件已被處理

static inline void io_uring_cqe_seen(struct io_uring *ring,
                     struct io_uring_cqe *cqe);
io_uring_cqe_seen(&ring, cqe);

清除 io_uring,釋放資源

清除 io_uring 結構使用 io_uring_queue_exit

extern void io_uring_queue_exit(struct io_uring *ring);
io_uring_queue_exit(&ring);

完整代碼列舉以下:這段代碼做用就是建立文件 /home/carter/test.txt 並寫入字符串 Hello world

#include <liburing.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>

int main()
{
    struct io_uring ring;
    io_uring_queue_init(32, &ring, 0);

    struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
    int fd = open("/home/carter/test.txt", O_WRONLY | O_CREAT);
    struct iovec iov = {
        .iov_base = "Hello world",
        .iov_len = strlen("Hello world"),
    };
    io_uring_prep_writev(sqe, fd, &iov, 1, 0);
    io_uring_submit(&ring);

    struct io_uring_cqe *cqe;

    for (;;) {
        io_uring_peek_cqe(&ring, &cqe);
        if (!cqe) {
            puts("Waiting...");
            // accept 新鏈接,作其餘事
        } else {
            puts("Finished.");
            break;
        }
    }
    io_uring_cqe_seen(&ring, cqe);
    io_uring_queue_exit(&ring);
}

能夠看到,C語言的異步操做仍是比同步操做複雜很多,libuv(nodejs 的底層 IO 庫)已經 表示會引入 io_uring。若是要本身用,必定要使用一個協程庫簡化異步操做。

這裏 是我使用本身編寫的協程庫 Cxx-yield 實現的一個簡單的文件服務器 demo。能夠看到,通過簡單封裝後,異步文件讀寫能夠簡化到一行:https://github.com/CarterLi/C...。就是那種在 JavaScript 裏寫 async、await 的快感

相關文章
相關標籤/搜索