epoll は様々な「イベント」の発生を検知できるようにするためのシステムコール。これを使うことで、「リスニングソケットに接続要求が来た」「ソケットにデータが到着した」などのイベントの発生を、カーネルが検知し通知してくれるようになる。
そして複数のファイルディスクリプタを監視対象として登録することができる。そのためネットワークプログラミングにおいては、複数のソケット、つまり複数のクライアントとの接続を同時に監視対象とすることができる。
これにより、 1 つのスレッドで多数のクライアントを処理できるようになる。イベントが発生するまでスレッドは待機し、イベントが発生したタイミングで必要な処理だけを行う。特定のクライアントからの反応を待ち続ける必要はないし、どのクライアントでイベントが発生しているのか見て回る必要もない。イベントが発生したら、イベントが発生したこと、そしてそれはどのクライアント(ソケット)で発生したのかを、カーネルが通知してくれるのだから。その通知を受けて、必要な処理を行えばよい。
この記事では epoll の具体的な使い方や挙動を見ていく。
そのための題材として、 クライアントからの接続要求やデータ送信を epoll で捌いていく簡易的な TCP サーバを Rust で書く。
epoll は Linux のシステムコールであり macOS などでは使えないので、 Linux でコンパイルや動作確認を行う。
この記事の内容は以下の環境で動作確認を行った。
$ lsb_release -a No LSB modules are available. Distributor ID: Ubuntu Description: Ubuntu 24.04.3 LTS Release: 24.04 Codename: noble
$ rustc --version rustc 1.91.0 (f8297e351 2025-10-28) $ cargo --version cargo 1.91.0 (ea2d97820 2025-10-10)
Rust の Edition は2024。
今回は、システムコールを直接呼び出すのではなく、システムコールをラップしたnixクレートを使って epoll を利用する。
まずEpollインスタンスを作り、そのインスタンスのメソッドを呼び出していく。今回使うメソッドは 3 つ。add()、delete()、wait()。
監視するイベントを登録していき(add())、当該イベントが発生するのを待機(wait())。イベントが発生する度に対応する処理を行い、それが終わったら再びwait()で次のイベントが発生するのを待つ。監視が不要になったイベントはdelete()で登録を解除する。これが基本的な流れ。
まず最初にコードの全文を示す。
このサーバを起動し、そしてクライアントを接続した際の挙動を見ながら、処理の流れを説明していく。
use nix::sys::epoll::{Epoll, EpollCreateFlags, EpollEvent, EpollFlags, EpollTimeout}; use std::io::Read; use std::net::TcpListener; use std::os::fd::AsRawFd; fn main() { let listener = TcpListener::bind("127.0.0.1:8080").unwrap(); listener.set_nonblocking(true).unwrap(); let epoll = Epoll::new(EpollCreateFlags::empty()).unwrap(); let event = EpollEvent::new(EpollFlags::EPOLLIN, listener.as_raw_fd() as u64); epoll.add(&listener, event).unwrap(); println!( "リスニングソケット(ファイルディスクリプタ {} )の EPOLLIN イベントを epoll に登録", listener.as_raw_fd() ); let mut events = [EpollEvent::empty(); 10]; let mut clients = Vec::new(); loop { let n = epoll.wait(&mut events, EpollTimeout::NONE).unwrap(); println!("epoll_wait: {} 個のイベントが発生", n); for i in 0..n { let fd = events[i].data() as i32; if fd == listener.as_raw_fd() { let (stream, addr) = listener.accept().unwrap(); stream.set_nonblocking(true).unwrap(); println!( "ファイルディスクリプタ {} (リスニングソケット)がクライアント {} から接続要求を受け取ったので、これを許可。このクライアントと接続しているソケットのファイルディスクリプタは {} 。", fd, addr, stream.as_raw_fd() ); let client_event = EpollEvent::new(EpollFlags::EPOLLIN, stream.as_raw_fd() as u64); epoll.add(&stream, client_event).unwrap(); println!( "ファイルディスクリプタ {} の EPOLLIN イベントを epoll に登録", stream.as_raw_fd() ); clients.push(stream); } else { let stream = clients.iter_mut().find(|s| s.as_raw_fd() == fd).unwrap(); let mut buf = [0u8; 1024]; match stream.read(&mut buf) { Ok(0) => { println!( "ファイルディスクリプタ {} が指し示すクライアントが接続を閉じた", fd ); epoll.delete(stream).unwrap(); println!("ファイルディスクリプタ {} を epoll から除外", fd); } Ok(n) => { let received = String::from_utf8_lossy(&buf[..n]); println!("ファイルディスクリプタ {} から以下のデータを受信した", fd,); println!("{}", received.trim()); } Err(e) => println!("読み取りエラー: {}", e), } } } } }
nixクレートを使っているので、Cargo.tomlのdependenciesを以下のように記述しておく。
[dependencies] nix = { version = "0.29", features = ["event"] }
TCP サーバの起動
サーバを起動すると以下のように表示される。
リスニングソケット(ファイルディスクリプタ 3 )の EPOLLIN イベントを epoll に登録
このプログラムはまず、「リスニングソケット(を指し示すファイルディスクリプタ(上記の場合は3))で発生するEPOLLINイベント」を、 epoll の監視対象として登録している。それが以下のコード。
let epoll = Epoll::new(EpollCreateFlags::empty()).unwrap(); let event = EpollEvent::new(EpollFlags::EPOLLIN, listener.as_raw_fd() as u64); epoll.add(&listener, event).unwrap();
その後loopに入る。このloopは無限ループであり、シグナルが送られてきたりパニックが発生したりしない限りループし続ける。
loopのなかではまずwait()して、監視対象として登録したイベントが発生するまで待機し続ける。
この時点で登録してあるのは、リスニングソケットで発生するEPOLLINイベントのみ。このイベントは TCP クライアントから接続要求が来たときに発生するので、それまではこのプログラムは待機し続ける。
クライアントからの接続要求
ncコマンドを使ってこの TCP サーバに接続要求を送る。
$ nc 127.0.0.1 8080
そうすると TCP サーバは以下を表示する。
epoll_wait: 1 個のイベントが発生 ファイルディスクリプタ 3 (リスニングソケット)がクライアント 127.0.0.1:56506 から接続要求を受け取ったので、これを許可。このクライアントと接続しているソケットのファイルディスクリプタは 5 。 ファイルディスクリプタ 5 の EPOLLIN イベントを epoll に登録
接続要求があったので、wait()が値を返し、処理が再開された。wait()の返り値であるnには発生したイベントの数が入っているので、n回分forループを回して、イベントごとの処理を行っていく。
適切に処理を行うためには「どのファイルディスクリプタで発生したイベントなのか」を把握しなければならないが、それを行っているのが以下のコード。
let fd = events[i].data() as i32;
eventsはwait()に渡していたが、こうすることで、監視対象のイベントが発生した際に、そのイベントに関する情報がeventsに書き込まれるようになる。
let n = epoll.wait(&mut events, EpollTimeout::NONE).unwrap();
eventsは配列であり先頭から書き込まれていくので、for iループのなかでevents[i]から情報を取り出していけばよい。そしてdata()メソッドによって、当該イベントが発生したファイルディスクリプタを取得できる。
forループのなかでは、「リスニングソケットで発生したイベントかどうか」で処理を分岐させている。
既に見たように今発生したのはリスニングソケット(ファイルディスクリプタ3)で発生したイベント。
この場合、接続要求を許可して新しくソケット(今回の場合はファイルディスクリプタ5)を作る。そして、このファイルディスクリプタ5のEPOLLINイベントも、 epoll 監視対象として追加している。
クライアントからのデータ送信
forループの処理が終わるとloopの先頭に戻る。そして再びwait()を呼び出して監視対象のイベントが発生するのを待つわけだが、今は先程とは違い、ファイルディスクリプタ3だけでなくファイルディスクリプタ5のEPOLLINも監視対象として登録されている。
ファイルディスクリプタ5とは、先程接続したncコマンドなので、そこからhelloと送ってみる。
そうするとサーバは以下を表示する。
epoll_wait: 1 個のイベントが発生 ファイルディスクリプタ 5 から以下のデータを受信した hello
ファイルディスクリプタ5からread()でデータを読み込み、その結果に応じた処理を行う。今回はデータを読み取れたので、それをそのまま表示している。
そしてまたloopの先頭に戻る。
クライアントからの接続切断
次はクライアントが接続を閉じたときの挙動を見てみる。
ncコマンドを ctrl + c で閉じるとサーバは以下を表示する。
epoll_wait: 1 個のイベントが発生 ファイルディスクリプタ 5 が指し示すクライアントが接続を閉じた ファイルディスクリプタ 5 を epoll から除外
クライアントが接続を閉じたときも、サーバではEPOLLINイベントが発生する。そのためデータが送信されてきた時と同様にread()を呼び出す。接続が閉じられたケースではread()が0を返すので、その場合はdelete()によって当該ファイルディスクリプタ(今回の場合は5)を監視対象から外す。
そのあと再びloopの先頭に戻りwait()を呼び出すが、ファイルディスクリプタ5は監視対象から外れたので、ファイルディスクリプタ3(リスニングソケット)のEPOLLINイベントのみを監視している状態になっている。
複数のクライアントからの接続に対応する
ここまで見た例では 1 つのクライアントしか接続しなかったので、次は複数のクライアントを同時に接続してみる。
そのために以下のプログラムを用意した。
use std::io::Write; use std::net::TcpStream; use std::thread; use std::time::Duration; fn main() { let mut clients: Vec<TcpStream> = Vec::new(); for i in 1..=5 { let stream = TcpStream::connect("127.0.0.1:8080").unwrap(); println!("クライアント {} 接続完了", i); clients.push(stream); } println!("=== 5つのクライアントが接続完了 ==="); thread::sleep(Duration::from_secs(5)); println!("=== 3つのクライアントからデータ送信 ==="); clients[0].write_all(b"message from client 1\n").unwrap(); clients[2].write_all(b"message from client 3\n").unwrap(); clients[4].write_all(b"message from client 5\n").unwrap(); thread::sleep(Duration::from_secs(5)); println!("=== 終了 ==="); }
このコードではまず、 5 つのクライアントをサーバに接続させる。その 5 秒後に、 5 つのうち 3 つのクライアントからデータを送信する。そしてさらに 5 秒後にプログラムを終了する。
このプログラムを実行すると、サーバは以下を表示する。
epoll_wait: 1 個のイベントが発生 ファイルディスクリプタ 3 (リスニングソケット)がクライアント 127.0.0.1:58408 から接続要求を受け取ったので、これを許可。このクライアントと接続しているソケットのファイルディスクリプタは 6 。 ファイルディスクリプタ 6 の EPOLLIN イベントを epoll に登録 epoll_wait: 1 個のイベントが発生 ファイルディスクリプタ 3 (リスニングソケット)がクライアント 127.0.0.1:58414 から接続要求を受け取ったので、これを許可。このクライアントと接続しているソケットのファイルディスクリプタは 7 。 ファイルディスクリプタ 7 の EPOLLIN イベントを epoll に登録 epoll_wait: 1 個のイベントが発生 ファイルディスクリプタ 3 (リスニングソケット)がクライアント 127.0.0.1:58418 から接続要求を受け取ったので、これを許可。このクライアントと接続しているソケットのファイルディスクリプタは 8 。 ファイルディスクリプタ 8 の EPOLLIN イベントを epoll に登録 epoll_wait: 1 個のイベントが発生 ファイルディスクリプタ 3 (リスニングソケット)がクライアント 127.0.0.1:58422 から接続要求を受け取ったので、これを許可。このクライアントと接続しているソケットのファイルディスクリプタは 9 。 ファイルディスクリプタ 9 の EPOLLIN イベントを epoll に登録 epoll_wait: 1 個のイベントが発生 ファイルディスクリプタ 3 (リスニングソケット)がクライアント 127.0.0.1:58426 から接続要求を受け取ったので、これを許可。このクライアントと接続しているソケットのファイルディスクリプタは 10 。 ファイルディスクリプタ 10 の EPOLLIN イベントを epoll に登録
リスニングソケットが接続要求を受け取る、というイベントが 5 回発生し、それを許可することで新たに 5 つのソケットが作られ、それらのEPOLLINイベントを監視対象に追加している。
この時点で 6 つのファイルディスクリプタが監視対象になっているが、該当するイベントが発生したら epoll を通してカーネルが伝えてくれるため、サーバが各ファイルディスクリプタでイベントが発生していないか見ていく必要はない。
接続要求から約 5 秒後にクライアントからデータが送信されてくるが、 epoll はきちんとそれを検知してくれる。
epoll_wait: 3 個のイベントが発生 ファイルディスクリプタ 6 から以下のデータを受信した message from client 1 ファイルディスクリプタ 8 から以下のデータを受信した message from client 3 ファイルディスクリプタ 10 から以下のデータを受信した message from client 5
さらに約 5 秒経過するとクライアントプログラムが終了し接続が切れるが、それも検知してくれる。
epoll_wait: 5 個のイベントが発生 ファイルディスクリプタ 6 が指し示すクライアントが接続を閉じた ファイルディスクリプタ 6 を epoll から除外 ファイルディスクリプタ 7 が指し示すクライアントが接続を閉じた ファイルディスクリプタ 7 を epoll から除外 ファイルディスクリプタ 8 が指し示すクライアントが接続を閉じた ファイルディスクリプタ 8 を epoll から除外 ファイルディスクリプタ 9 が指し示すクライアントが接続を閉じた ファイルディスクリプタ 9 を epoll から除外 ファイルディスクリプタ 10 が指し示すクライアントが接続を閉じた ファイルディスクリプタ 10 を epoll から除外