元のLinux非同期ファイル操作、io_uring試食体験


Linux非同期IOの歴史
非同期IOはLinuxシステムの痛みです.Linuxは早くからPOSIX AIOという非同期IOを実現していたが,ユーザ空間で独自にユーザスレッドシミュレーションを開き,効率が極めて低かった.その後、Linux 2.6に本格的なカーネルレベルでサポートされる非同期IO実装(Linux aio)が導入されたが、DirectIOのみがサポートされ、ディスクファイルの読み書きのみがサポートされ、ファイルサイズに制限があり、とにかくいろいろなトラブルがあった.これまで(2019年5月)、libuvはpthread+preadvの形で非同期IOを実現してきた.
Linux 5.1のリリースに伴い、Linuxはついに独自の非同期IO実装を実現し、多くのファイルタイプ(ディスクファイル、socket、パイプなど)をサポートしました.これが本稿の主役です.io_uring
IOCP
IO多重モデルepollとは異なりio_uringの考え方はWindows上のIOCPに似ている.宅配便で例を挙げます:同期モデルはあなたが電子商取引プラットフォームで注文する前に、あなたの家の階下でずっと待っていて、宅配会社が荷物を階下に送るまで、あなたは更に荷物を2階に持って行きます.epollはあなたが注文したように、宅配会社が階下に送って、階下に行って荷物を受け取ることができることを知らせて、この時あなたは階下に降りて荷物を持ってきます.やはりユーザーが下に降りて荷物を受け取る必要がありますが(同期して読み書きする時間があります)、宅配便が道にいる時間を待つ必要がないため、効率は非常に向上しています.ただし、epollはディスクIOには適用されません.ディスクファイルは常に読み取り可能であるためです.
IOCPは一歩で到着し、直接宅配し、階下で取る動作も必要ありません.プロセス全体が完全に非ブロックである.
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)です.第1のパラメータentriesは、キューサイズを表す(実際の空間は、ユーザが指定したよりも大きい場合がある).2番目のパラメータringは初期化が必要なio_ですuring構造ポインタ;3番目のパラメータflagsはフラグパラメータであり、特に0を伝達する必要はない.たとえば
#include 
struct io_uring ring;
io_uring_queue_init(32, &ring, 0);

読み取り/書き込みリクエストの送信
まずio_を使いますuring_get_sqeはsqe構造を取得する.
extern struct io_uring_sqe *io_uring_get_sqe(struct io_uring *ring);

1つの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_readvまたはio_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);

第1のパラメータsqe、すなわち、前に取得されたsqe構造ポインタである.fdは読み書きが必要なファイル記述子であり、ディスクファイルであってもsocketであってもよい.iovecsはiovec配列です.具体的にはreadvとwritevを参照してください.nr_vecsはiovecs配列要素の個数で、offsetはファイル操作のオフセット量です.
この2つの関数は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にしてください.筆者はここで2時間悩んだ.
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_cqeとio_uring_wait_cqeの2つの関数は、現在完了している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構造ポインタ;2番目のパラメータcqe_ptrは、出力パラメータであり、cqeポインタ変数のアドレスである.
cqe(completion queue entry)は、完了したIO操作をマークするとともに、前に送信されたユーザデータも記録する.各cqeは前のsqeに対応する.
この2つの関数、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 
#include 
#include 
#include 

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です.単純にカプセル化すると、非同期ファイルの読み書きが1行に簡略化されることがわかります.https://github.com/CarterLi/C....JavaScriptにasync、awaitと書く快感です