Tokio (tokio::fs) と io_uring
tokio::fs::read()
を追ってみる
エントリーポイント
基本的には tokio::fs::*
は全部この syncify()
を通る。中で対応する std::fs::*
を呼ぶだけ
asyncify の中身はこれまたシンプル
spawn_blocking(f)
の実体は tokio::fs::blocking::spawn_blocking(f)
になるのでシンプル・・・なはず
そいつの宣言はここ
なので tokio::runtime
に飛ぶ
tokio::runtime::spawn_blocking
の実体はここ
この rt
はなんぞや?が難しい。 context
を探しに行く。ここでは use crate::runtime::context;
となっているので見に行くと current()
はすごくSimple
これがセットされてるのは
ここなんだけど、これはランタイムの起動時にセットされていて、よくある
#[tokio::main(flavor="multi_thread", worker_threads = 2)]
async fn main() {
println!("Hello world");
}
は
fn main() {
tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.unwrap()
.block_on(async {
println!("Hello world");
})
}
と等価であると tokio-macros
のドキュメントにかいてある。なので .build()
あたりを見に行くとあった
この Handle
の実体はこれ
なので、 .spawn_blocking(f)
の実装はここになる
_inner(func, None)
だと・・・まあすぐ下にある。 runtime::blocking::task::BlockingTask(func, None)
をつくって self.blocking_spawner
に渡している。これはRuntime初期化時に作られていて
これの実体はここで作られている
実際に↑で作ったTaskがQueueに入るのはここ
Queueの実体は↑↑にあるとおりMutexでラップされた VecDeque<Task>
になる
ちなみにタスクが埋まってたりするとここでワーカースレッドが起動される。
Mutexについては以下の記事が詳しい。
ワーカースレッドはすぐ下で起動されている。ワーカースレッドが shared.queue
からポップしたタスクを実行するところは簡単なのでここでは割愛する
この thread::Builder
は use crate::loom::thread;
で宣言されているがこれはテスト用のモックを std
を切り替えるためのやつ。
なので実体は std::thread
になる。
Builder が読んでいる spawn はこれ
いろいろラップしているがどうもここスレッドを作っているようだ
いろいろラップされていて難しいがこういう感じ
Ok(JoinHandle(Ok(JoinInner{ native: crate::sys::thread::Thread::new(..), ...})))
で、つまり pthread
JoinInnerはこう
JoinHandleはこう(こんな書き方できるんやな・・・・)
さてnewされたスレッドはここで pthread_create()
に渡る
ここで呼ばれている thread_start(main, ...)
はすぐ下に定義されている(こんな書き方できるんや・・・)
ここで main....()
とようやく呼ばれていることがわかる。これで晴れてスレッドが起動したことになる
なおここでヌル的ななにかをReturnしてるので pthread_join()
で値を拾えるということはない。
io_uring
以上みてきたことからわかるように、 tokio::fs
の実装はファイルIOに関しては非同期化していないことになる。ファイルIOを非同期化する手段として今は io_uring(7)
があるので、そちらを調べる(といってもリンクを列挙するだけだけど
なぜ新しいシステムコールがLinuxに必要なのか?については作者の解説PDFが詳しい
CloudFlare Blog
内部実装に少し触れている(わかりやすい絵があるCloudFlareの記事)
↑Cloudflareのブログにリンクされているすごくざっくり要約するとファイルIOはbounded time で終わると期待されるのでワーカープールには行かない(マジかよ)。ネットワークIOは unbounded なのでワーカープールに行くということのようだ
(C) CloudFlare blog
その他リンク
Tokio
ネットワークIOまわりの非同期IOはいくつか詳しい記事がある
少し古いけどわかりやすい記事
絵と漢字でなんとなく理解できた気になる本
本家の一番くわしい記事
ちなみにTokio入門日本語はこのへん
io_uring の仕組みを↑↑のCloudFlareのブログをベースに調べる
We can observe io_uring when it calls vfs_poll, the machinery behind non-blocking I/O, to monitor the sockets. If that happens, we will be hitting the io_uring:io_uring_poll_arm tracepoint. Meanwhile, the wake-ups that follow, if the polled file becomes ready for I/O, can be recorded with the io_uring:io_uring_poll_wake tracepoint embedded in io_async_wake() wake-up call.
といってるので、 vfs_poll
を調べることにする
公式が言ってるのはこう
called by the VFS when a process wants to check if there is activity on this file and (optionally) go to sleep until there is activity. Called by the select(2) and poll(2) system calls
まあそうだよね。ということでここでは ext4 を見てみることにする
カーネルの pull が一生終わらないので適当にググる
うんうんそうだね・・・・という絵
日本語記事もあった
なお(1)からリンクしているサンプルコードのGistはない(筆者に問い合わせ中)
pull できたので vfs_poll(..)
なおext4 の poll()
は虚無だった
DEFAULT_POLLMASK
は #define DEFAULT_POLLMASK (EPOLLIN | EPOLLOUT | EPOLLRDNORM | EPOLLWRNORM)
なのでつまり全部入りということになる
io_uring code reading?
エントリーポイントからかな
Ubuntu 24.04でつかわれている 6.8 をベースに読んでみる