io_uringメモ
io_uring について
Linux の新しい非同期 I/O インタフェース
Linux AIO における諸問題を解決するためにフルスクラッチで実装された
利点
ファイルシステムなど epoll 非対応の ファイルディスクリプタに対する非同期 I/O
システムコール発行数の削減によるパフォーマンス改善
特に Meltdown/Spectre 後のシステムコールが高コストになった世界では大幅な効率化が期待される
効率性を重視するため、共有メモリを用いてカーネル-アプリケーション間の通信を実現させる
リングバッファを用いることで、ロックによるパフォーマンス低下を回避
io_uring による非同期 IO の流れ
io_uring_setup システムコールを呼び出し、カーネルとの通信に必要な設定を行う
I/Oの要求を送信するキューを Submission Queue (SQ)、結果を受信するキューを Completion Queue (CQ) と呼ぶ
SQ/CQ に対応するメモリ領域にアクセスするため mmap を用いる
次のように I/O を実行する
アプリケーションは I/O 要求を SQ の末尾に追加し、 io_uring_enter システムコールを呼び出すことでカーネルに I/O の実行を指示する
要求する I/O は一度に複数個指定することができる
(optional) アプリケーションは他のタスクを実行する
カーネルは受信したI/O要求を実行し、完了した結果は CQ の末尾に追加される
要求された I/O が複数個ある場合、それらの結果が追加される順番は不定
アプリケーションは、完了した I/O の結果を CQ の先頭から取り出す
liburing
io_uring のセットアップやリングバッファアクセス時のプロセッサ依存の処理(メモリバリアなど)のボイラープレートを削減しシンプルな API を提供するための C ライブラリ
Rustにおけるio_uring対応の現状
クレート一覧
liburing のバインディング
liburing を用いず、相当する機能をフルスクラッチで実装
他のライブラリにおいて起こりうる use-after-free を防止していると謳っている
GPL+3(作者のGitHub Sponserになると MIT/Apache-2.0が選択できる)
ランタイム側の対応
Tokio
ファイルシステムI/Oの改善(現時点ではスレッドプール実装になっている)とシステムコール削減などを目標にしている
他の完了ベース I/O(Windows の IOCP など)との融合や既存の API との互換性を重視
io_uring 自体の機能をフルで使うことに対しては消極的
async-std (smol)
現時点では io_uring 対応に関する議論は(少なくとも表面上は)なさそう
soundness に関する話題
端的に言うと「IO 要求に対応する future がキャンセルされた場合、カーネル側で使用中のメモリを開放してしまい データ競合や use after free を生じてしまう」という問題
code:rust
let file = std::fs::File::open("/path/to/file")?;
let io_uring = my_io_uring::setup()?;
// この時点でリクエストが SQ に登録され、カーネルが I/O を開始する
let future = io_uring.read(&file, &mut buf..); // impl Future<Output = io::Result<usize>> // ここで future を drop してもカーネルは I/O の実行を継続するため
// buf にデータが書き込まれる可能性がある
drop(future);
// …なので、ここで buf を弄ると data race となる
let other: Vec<u8> = ...;
buf.copy_from_slice(&other..); // drop の場合は use after free になる
drop(buf);
デストラクタ内でI/Oが完了するまでブロックする解決策とその問題点
単純な Drop の場合、エグゼキュータ内の他のタスクを巻き込み停止してしまうのでパフォーマンスが大幅に低下する
非同期デストラクタのような仕組みが提案された場合でも、future が所属しているタスクがブロックされるため他のI/O(ネットワーク I/O など)に悪影響を及ぼす
そもそも I/O が実行されている間バッファの所有権はカーネルにあるのだから、そのようにAPIを設計すべきでは
呼び出し側がバッファを管理する既存のトレイト(Read, AsyncRead, etc) との相性が悪い
呼び出し先がバッファを管理することを想定している BufRead や AsyncBufRead などを代わりに使うべき