之前的文章提到 POSIX 版的 async IO 界面其實只是啟動新的 thread 進行普通的 blocking IO ,如果程式本身已經是在多個 thread 中進行平行的 IO 操作,那麼使用這個 API 其實不會獲得任何效能上的好處。

我們希望的 async IO 應該還是像 epoll 那樣,發送讀寫請求之後讓 process 繼續執行,讀寫任務完成後可以讓 process 把結果撿起來用。這就會需要系統核心的協助,要有介面來讓 process 「發送」要求和在完成時收到通知以「獲得」結果。

Linux AIO

Linux AIO 就對這種類型的 IO 提供部分的支援,主要透過 io_setup, io_submitio_getevents 三個 system call 來達成初始化、發送 request 和取得結果的工作。需要先引入相關的 header 檔案

#include <fcntl.h>
#include <unistd.h>
#include <sys/syscall.h>
#include <linux/aio_abi.h>
#include <linux/ioprio.h>

io_setup 用來建立 async io 的 context ,也就是在 kernel 所需的資料結構,接收兩個參數 nr_eventsctx_idp ,分別表示 io queue 裡最大的請求數量和存放 context 的指標。所以初始化的過程如下:

        int nr_event = 4;
        aio_context_t ctx;
        // We don't assume the typing of aio_context_t here
        // but it's actually integer alike
        memset(&ctx, 0, sizeof(aio_context_t));
        long err = syscall(SYS_io_setup, nr_event, &ctx);

建立請求

接著要準備要發送的 IO 操作請求,這個請求是透過 iocb 這個結構來定義的,底下介紹裡面需要填入的欄位。

aio_rw_flags 的作用相當於使用 open 時設定的 flag ,可以使用的值有:

aio_lio_opcode 用來指定要執行的檔案操作,可以使用的值的名稱和原始的 blocking system call 直接對應:

aio_flags 指定了 IO request 的一些屬性,這裡我只會 IOCB_FLAG_IOPRIO ,表示

aio_reqprio 表示這個 IO request 的優先級,數字越大表示優先級越低。在 aio_flags 的值是 IOCB_FLAG_IOPRIO 的時候使用 IOPRIO_PRIO_VALUE 這個 macro 來定義。這個 macro 接收兩個參數 classdata ,分別表示 IO 排程器的種類和優先級, data 可以是 0-7 的值,有一個中間值常數 IOPRIO_NORM=4class 可以使用的值則有以下三種:

因此一個 aio_reqprio 的值可以寫成 IOPRIO_PRIO_VALUE(IOPRIO_CLASS_BE, IOPRIO_NORM)

剩下的欄位就比較像原始 IO system call 裡會有的參數 aio_fildes 表示要執行 IO request 的 file descriptor , aio_buf, aio_nbytes, aio_offset 表示資料要在記憶體存放的位址、讀寫操作的資料長度和位移。

發送請求和獲取結果

填完這些參數(真的有夠多)就能夠發送 AIO 請求了。發送請求使用的是 io_submit system call ,帶有三個參數 ctx_id, nr, iocbpp ,分別是一開始建立的 context 、 IO request 的數量和一個由指向 iocb 結構的指標組成的陣列或指標(a pointer to the pointer to iocb

        struct iocb *cbpp[] = { &cb };
        syscall(SYS_io_submit, ctx, 1, cbpp);

發送請求之後可以用 io_getevents 獲得結果

        struct io_event events[MAX_NR];
        syscall(SYS_io_getevents, ctx, min_nr, nr, &events, &timeout);

nr 表示要在 events 存放完成事件的最多數量。 這個 system call 會 block 直到 io context 中完成的 request 數量達到至少 min_nr 或是以 timespec 結構定義的 timeout 時間超過為止。收到結果後的 io_event 有這些欄位:

 struct io_event {
    __u64       data;       /* the data field from the iocb */
    __u64       obj;        /* what iocb this event came from */
    __s64       res;        /* result code for this event */
    __s64       res2;       /* secondary result */
};

res2 的內容我不知道,但 res 是在 iocb 結構的 aio_lio_opcode 指定的 IO 操作回傳的結果。data 則是 aio_buf 的資料。這些欄位不知道為什麼都用整數型別而不是指標。可能是為了統一結構大小做的一些小優化,但總之自己轉換型別後就能得到資料。

資源釋放

io_destroy 應該最簡單,就是釋放在 kernel 中佔用的資料結構:

        syscall(SYS_io_destroy, ctx);

libaio 函式庫

使用 syscall 會需要依照不同處理器稍微注意不同 ABI 參數記憶體對齊的問題,這系列的 system call 其實要設定的欄位也算是偏多。因此在 userspace 有人另外寫了 libaio 這個 wrapper 函式庫。基本上大部分函式意思都和名稱對應的 system call 差不多,只是有些參數做了簡化,我好想先發文就不再特別研究他的參數怎麼填了

限制

雖然 Linux AIO 從取名和開發過程就是為了達成對檔案真正的 asynchronous IO 。但其實到現在還是有滿多限制,特定的情境下這個 API 依然會造成阻塞,通常和 kernel 內的 lock 或排程或記憶體管理類的問題有關。這個 API 只有對普通檔案開啟 O_DIRECT 屬性的讀寫操作才會最接近理想 async IO 的行為。

References

https://man7.org/linux/man-pages/man2/io_setup.2.html

https://docs.huihoo.com/doxygen/linux/kernel/3.7/aio__abi_8h_source.html

https://elixir.bootlin.com/linux/v5.19/source/include/uapi/linux/ioprio.h

https://hackmd.io/@sysprog/iouring

https://stackoverflow.com/questions/34572559/asynchronous-io-io-submit-latency-in-ubuntu-linux

https://oxnz.github.io/2016/10/13/linux-aio/