HN 日本語サマリー

← 一覧へ戻る
プログラミング

epollとkqueue: オペレーティングシステムはいかに効率的に待つことを学んだか

Epoll and Kqueue: How Operating Systems Learned to Wait Efficiently (thecodinggopher.substack.com)

10 pointsby adletbalzhanov1 コメント

要約

この記事は、オペレーティングシステムがI/O待ちを効率化するために採用したepoll(Linux)とkqueue(BSD系)の仕組みについて解説しています。これらは、多数の接続を同時に扱うスケーラブルなサーバーを実現する上で不可欠な技術です。

全文翻訳

epollとkqueue: オペレーティングシステムはいかに効率的に待つことを学んだか スケーラブルなI/Oの背後にある静かな仕組み The Coding Gopher 2026年2月19日 4117 共有 「I/Oの最も難しい部分は、データの読み込みではありません。それは、いつ待つのをやめるべきかを知ることです。」 あらゆる高性能サーバーは、最終的に同じ問題に直面します。それは待ち時間です。CPUを待つのではなく、メモリを待つのでもなく、外部の世界を待つのです。ネットワークソケットはアイドル状態になります。ファイルディスクリプタは停滞します。数千の接続が存在しますが、アクティブなのはごくわずかです。 初期のオペレーティングシステムはこれをうまく処理できませんでした。現代のシステムは、主にLinuxのepollやBSD系システムのkqueueのような仕組みのおかげで、それを処理できます。これらのAPIは、ソフトウェアがI/Oを待つ方法を根本的に変え、今日の高度に並行処理されるサーバーを可能にしました。 問題: 待ち時間はスケーリングしない 低レベルでは、I/Oはブロッキングです。カーネルにソケットからの読み込みを要求し、データが利用可能でない場合、カーネルは待ちます。接続が1つであれば問題ありません。1万あれば、悲惨な結果になります。 単純な解決策は、接続ごとに1つのスレッドを用意することです。各スレッドは独立してブロックされます。これは、スレッド作成、コンテキストスイッチ、メモリオーバーヘッドがシステムを圧倒するまで機能します。 2回目の試みはポーリングでした。 selectとpollがなぜ不十分だったのか 初期のUnixシステムは、複数のファイルディスクリプタを一度に待つ方法としてselectを導入し、後にpollを導入しました。 概念的には、これは次のように機能します。カーネルにファイルディスクリプタのリストを渡し、「これらのうちどれが準備完了か?」と尋ねます。カーネルはそのリストをスキャンして、あなたに伝えます。 そのスキャンが問題なのです。 各呼び出しでは、1つだけアクティブな場合でも、すべてのファイルディスクリプタを反復処理する必要があります。接続数が増加するにつれて、コストは線形に増加します。大規模になると、CPU時間のほとんどは、カーネルに同じ質問を何度も何度も尋ねることに費やされます。 本当の問題は、待ち時間が遅いことではなく、チェックにコストがかかることでした。 epollとkqueueの背後にある核心的な洞察 epollとkqueueは、シンプルでありながら強力なアイデアに基づいています。 あなたが気にかけていることについてカーネルに繰り返し尋ねるのではなく、一度カーネルに伝え、通知させるのです。 待機したいときに毎回ファイルディスクリプタの完全なリストを渡す代わりに、一度だけ関心を登録します。その後、カーネルは内部で準備状況を追跡し、何かが変更されたときにのみあなたを起こします。 待ち時間は、容量ではなく、アクティビティに比例するようになります。 epoll: Linuxのイベント通知システム Linuxでは、このアイデアはepollとして実装されています。 epollは、epollインスタンスと呼ばれる永続的なカーネルオブジェクトを導入します。これを一度作成し、関心のあるイベント(読み取り可能、書き込み可能、エラーなど)とともにファイルディスクリプタを登録します。 int epfd = epoll_create1(0); epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event); このコードは、カーネル内にepollインスタンスを作成し、ファイルディスクリプタをそれに登録します。この時点から、カーネルはfdがいつ読み取り可能、書き込み可能になるか、またはエラーに遭遇するかを追跡する責任を負います。重要なのは、待機したいときに毎回fdをカーネルに渡す必要がなくなったことです。それは記憶されています。 epoll_createシステムコールは、新しく作成されたepollカーネルデータ構造へのファイルディスクリプタを返します。呼び出し元のプロセスは、このファイルディスクリプタを使用して、I/Oを監視したい他のファイルディスクリプタをepollインスタンスに追加、削除、または変更できます。 上記の図では、プロセス483はファイルディスクリプタfd1、fd2、fd3、fd4、fd5をepollインスタンスに登録しています。これは、その特定のepollインスタンスの関心リストまたはepollセットです。その後、登録されたファイルディスクリプタのいずれかがI/Oの準備ができた場合、それらは準備完了リストにあると見なされます。 準備完了リストは、関心リストの部分集合です。 メソッドシグネチャ #include <sys/epoll.h> int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); epfd — epoll_createによって返され、カーネル内のepollインスタンスを識別するファイルディスクリプタです。 fd — epollリスト/関心リストに追加したいファイルディスクリプタです。 op — ファイルディスクリプタfdに対して実行される操作を指します。一般的に、3つの操作がサポートされています。 — fdをepollインスタンスに登録し(EPOLL_CTL_ADD)、fdで発生するイベントの通知を受け取ります。 — epollインスタンスからfdを削除/登録解除します。これは、プロセスがそのファイルディスクリプタ上のイベントに関する通知を一切受け取らなくなることを意味します(EPOLL_CTL_DEL)。ファイルディスクリプタが複数のepollインスタンスに追加されている場合、それを閉じると、追加されたすべてのepoll関心リストから削除されます。 — fdが監視しているイベントを変更します(EPOLL_CTL_MOD)。 event — fdを監視したい実際のイベントを格納するepoll_eventという構造体へのポインタです。 登録したら、待機します。 epoll_wait(epfd, events, maxevents, timeout); ここで、呼び出しスレッドはカーネルにイベントが準備完了になるまでスリープします。登録されているすべてのファイルディスクリプタをスキャンする代わりに、カーネルは状態が変更されたものだけを返します。何も起こっていない場合、この呼び出しは低コストです。多くのことが一度に起こる場合、コストはアクティブなイベントの数に比例します。総接続数ではありません。 決定的な違いは、epoll_waitがすべてのファイルディスクリプタをスキャンしないことです。カーネルはすでにどのファイルディスクリプタが準備完了かを知っており、それらだけを返します。 Git、Docker、Redis、HTTPサーバー、コンパイラをゼロから構築する方法を学びましょう。CodeCraftersで40%オフを手に入れましょう: https://app.codecrafters.io/join?via=the-coding-gopher このSubstackは読者によってサポートされています。新しい投稿を受け取り、私の仕事をサポートするために、無料または有料の購読者になることを検討してください。 購読する レベルトリガー対エッジトリガーの動作 epollは、イベントがどのように配信されるかを形成する2つのモードをサポートしています。 レベルトリガーモードでは、条件が続く限りイベントが報告されます。ソケットが読み取り可能で、それを完全に drained しない場合、epollは通知を続けます。 エッジトリガーモードでは、epollは状態が変更されたとき(準備未完了から準備完了へ)にのみ通知します。イベントを見逃して、利用可能なすべてのデータを読み取らなかった場合、再び通知されない可能性があります。 エッジトリガーモードはより効率的ですが、注意深い非ブロッキングコードが必要です。多くの微妙なバグはここに潜んでいます。 この柔軟性により、epollは強力になりますが、同時に扱いにくいものでもあります。 kqueue: より一般的なイベントシステム BSDシステム(macOSを含む)では、同等のメカニズムはkqueueです。 一見すると、kqueueは似ています。カーネルイベントキューを作成し、関心を登録し、通知を待ちます。 int kq = kqueue(); kevent(kq, &change, 1, NULL, 0, NULL); 最初の呼び出しはカーネル管理のイベントキューを作成します。2番目の呼び出しは変更要求を登録します。これは、ソケットの読み取り可能性、シグナル、ファイル変更など、関心のあるイベントの種類をカーネルに指示する命令です。epollと同様に、この登録は削除されるまで持続します。 しかし、kqueueはさらに進んでいます。それはファイルディスクリプタだけに関するものではありません。 kqueueは以下を監視できます。 ファイル変更 プロセスライフサイクルイベント シグナル タイマー ソケット すべて同じインターフェースを通じて。 epollが特殊化されて最小限であるのに対し、kqueueは一般的で表現力豊かです。それは「イベント」を単なるI/Oの準備状況ではなく、ファーストクラスの市民として扱います。 プッシュ対プル: 真の区別 epollとkqueueが導入する最も重要な変化は哲学的なものです。 selectとpollでは、ユーザー空間はカーネルから状態を繰り返しプルします。epollとkqueueでは、カーネルは興味深いことが起こったときにイベントをユーザー空間にプッシュします。 この逆転はコストモデルを完全に変えます。アイドル状態の接続は低コストです。アクティブな接続が作業を推進します。スケーリングが可能になります。 Goはepollとkqueueをどのように使用するか Goのランタイムは、ネットワークスケジューラをepollとkqueueの上に直接構築しています。 コードから見ると、これはブロックする呼び出しです。ゴルーチンはデータが到着するまで実行を停止します。 舞台裏では、Goはソケットを非ブロッキングモードに設定し、それをepollまたはkqueueに登録し、ゴルーチンをパークします。OSスレッドはデータの到着を待ってブロックされません。カーネルがソケットが準備完了であることをシグナルすると、ランタイムはゴルーチンをウェイクアップし、中断したところから実行を再開します。 これが、Goが非同期を公開することなく、大規模な並行処理を実現する方法です。