- ブラウザURL入力
- DNS名前→IP
- ルータ経路選択
- ロードバランサ振り分け
- Nginx受け口
- アプリ処理今ここ
- DB保存
第13週『システムコール — アプリとカーネルの境界線』では境界の下(カーネル)を見ました。今回は上(プロセスの中)から同じ境界を見ます。
まず結論
Webサーバは特別なプログラムではありません。socket・bind・listen・accept という4つのシステムコールを順に出す、普通のプロセスです。
「待ち受ける」の実体は、カーネルに接続をキューへ溜めさせることです。listen(2) はソケットを受動(passive)にすると書いており、接続はプロセスではなくカーネル側に積まれます。プロセスは accept で1つずつ取り出します。
`server.listen(3000)` と書いたプロセスは、接続が1本も来ていない間、何をしているでしょうか。
当てることが目的ではありません。外した予測ほど記憶に残るので、直感で選んでください。
「待ち受ける」は4つの依頼でできている
Webサーバの中身は、第13週『システムコール — アプリとカーネルの境界線』で見た境界に向かって決まった依頼を順に出すだけの、普通のプロセスです。依頼は4つで、listen(2) の NOTES 節がこの手順を並べています。
- socket(2) — 通信の端点を作る
- bind(2) — その端点にローカルアドレスを割り当てる
- listen(2) — 受け付ける意思と、キューの上限を伝える
- accept(2) — 接続を1つ取り出す
返るのはファイルディスクリプタです。第13週で見た read(fd, buf, size) の fd と同じ番号空間で、open(2) と同じ形の値が返ります。ただしまだアドレスがありません。bind(2) の man は「socket(2) でソケットが作られたとき、それは名前空間(アドレスファミリ)には属するが、アドレスは割り当てられていない」と書き、bind がそれを割り当てると続けます。
3つ目が今回の主題です。
listen が行うのは「印を付ける」ことで、接続を受け取る作業そのものではありません。 印が付いた後、届いた接続要求を並べるのはカーネルの仕事です。並べておける本数の上限が backlog です。
だから「サーバが接続を待つ」は主語を取り違えています。待ち行列を持つのはカーネルで、プロセスは accept で取り出しに行くだけです。
Node の3行と、lsof に映るもの
Node v24.21.0(macOS)で次の3行を起動しました。ポートは 3000 が使用中のため 3999 です。
const http = require("http");const server = http.createServer((req, res) => { res.end("hello"); });server.listen(3999, () => { console.log("listening", JSON.stringify(server.address())); });$ node srv-a1.jslistening {"address":"::","family":"IPv6","port":3999}
server.address() が返したのは :: — コードに書いていないアドレスです。Node v24 の net.md がこう規定しています。
0.0.0.0 の意味は ip(7) にあります。「bind の呼び出しで INADDR_ANY が指定されると、ソケットは全ローカルインタフェースに束縛される」。host 省略は暗黙に「全部で受ける」を選びます。
外から覗くと、待ち受けの実体が fd として見えます。
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAMEnode 18117 kazu 12u IPv6 0x2199b585b35f4ab 0t0 TCP *:3999 (LISTEN)
fd 番号は 12、NAME 列は *:3999 (LISTEN)(:: への bind の macOS 表示)。fd を並べると、この行が標準入出力と同じ列にあると分かります。
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAMEnode 20335 kazu 0r CHR 3,2 0t0 336 /dev/nullnode 20335 kazu 1w REG 1,16 6 62152503 /private/tmp/claude-501/-Users-kazu-coding-study-site/2275da46-31cf-40a1-9671-68e0e39042e1/scratchpad/al/lab-C1/srv-e.outnode 20335 kazu 2w REG 1,16 6 62152503 /private/tmp/claude-501/-Users-kazu-coding-study-site/2275da46-31cf-40a1-9671-68e0e39042e1/scratchpad/al/lab-C1/srv-e.outnode 20335 kazu 3u KQUEUE count=0, state=0x8...node 20335 kazu 7u KQUEUE count=0, state=0xa...node 20335 kazu 9u KQUEUE count=0, state=0xa...node 20335 kazu 12u IPv6 0x46f59876289fe0c9 0t0 TCP *:3993 (LISTEN)
fd 0/1/2 が標準入力・標準出力・標準エラー、fd 12 が LISTEN ソケットです。「サーバ」という特別な枠は無く、開いたファイルと同じ列に1本増えているだけです。stdout をファイルにリダイレクトして起動したため、fd 1/2 は端末(CHR)ではなく REG です。fd 3・7・9 の KQUEUE は macOS のイベント通知機構です(この実機は macOS なので epoll は現れません)。
`accept()` が返す値について、正しいのはどれですか。
待ち受けとは「戻ってくる時刻を自分で決めない呼び出し」のこと
プロセスがカーネルに出す依頼には2種類あります。片方は、カーネルが仕事を終えればすぐ戻る依頼(開く・書く・閉じる)。戻る時刻は処理速度だけで決まります。
もう片方は、戻る時刻が外部の出来事に依存する依頼です。accept はこちらで、保留キューが空なら誰かが繋いでくるまで戻りません。一晩戻らないこともあり、プロセスの都合では決まりません。
この違いは、accept が何を返すかを読むとはっきりします。
取り出した1件につき、新しいソケットと新しい fd が作られます。待ち受け用の1本は listening のまま影響を受けません。1接続 = accept 1回 = fd 1本という対応が生まれ、同時接続が増えると開く fd が増えるのもこの帰結です。
- ・キューを持っているのはカーネル側で、プロセスではない
- ・backlog を超えた接続要求について man は、ECONNREFUSED を受け取るか、再送のあるプロトコルなら無視されて後の再試行が成功する、と書いている
対比: ls が出すシステムコールと並べる
第13週では ls が出すシステムコールを見ました。openat で開き、read で読み、close で閉じる。一覧を表示するだけで108回もカーネルに依頼している、という例でした。
Webサーバも同じ境界を越え、特権レベルの切り替えも同じように起きます。「システムコールを出すかどうか」で両者は区別できません。
違いは戻る契機です。ls の呼び出しはカーネルの処理が終われば戻るので必ず終わります。一方サーバは accept で外の出来事を待ちます。終わらないのは無限ループのせいではなく、戻る時刻を自分で決められない呼び出しを繰り返しに置いたからです。待ち受け中のCPU使用率がほぼ0なのも、探し回っていないからです。
Node で `server.listen(3000)` と host を省略したとき、bind 先はどこになりますか。
ls が出すシステムコール(第13週)
開く・読む・閉じる・一覧を取る。どれもカーネルが仕事を終えれば戻ります。戻る時刻は処理速度だけで決まり、外の誰かに依存しません。だからコマンドは自分で終わります。
呼び出し 戻る契機 終わるかopenat カーネルが開き終えたら 終わるread 読み終えたら 終わるclose 閉じ終えたら 終わる
Webサーバが出すシステムコール
socket・bind・listen は即座に戻ります。accept だけが違い、保留キューが空なら誰かが接続するまで戻りません。この1つを繰り返しに入れた瞬間、プロセスは終わらなくなります。
呼び出し 戻る契機 終わるかsocket/bind カーネルが処理したら 戻るlisten 印を付けたら 戻るaccept 接続がキューに入ったら 来るまで戻らない
`ls` と Web サーバはどちらもシステムコールを出します。「常駐している」ように見えるのは何が違うからでしょうか。
呼び出しが戻る時刻を誰が決めているかを、両方で考えてみてください。
この章のまとめ
- Webサーバは特別なプログラムではなく、socket → bind → listen → accept を出す普通のプロセス。listen(2) の NOTES がこの4手順を並べている。
listen()は受動ソケットの印を付けるだけで、接続を受け取る作業そのものではない。保留接続のキューを持つのはカーネル。- backlog は保留接続キューの最大長。 Linux では
somaxconnを超える指定は黙って丸められ、既定値は 5.4 以降 4096、それ以前は 128。Node の既定は 511(net.md が「512 ではない」と明記)。 - accept は1件ごとに新しい接続済みソケットと新しい fd を返す。 listen ソケットは listening のまま影響を受けない。1接続 = accept 1回 = fd 1本。
- host 省略の
server.listen(3000)は、Node では IPv6 があれば::、無ければ0.0.0.0に bind する。lsofの NAME 列が*:3000か127.0.0.1:3000かで到達範囲が変わる。 lsとの違いはシステムコールを出すかどうかではなく、戻る時刻を自分で決められない呼び出し(accept)を繰り返しに含むかどうか。
問題で詰めるなら、クイズ群『サーバーのしくみ』の「サーバーのしくみ II(ソケットとカーネル)」が対応範囲です。次回は accept で取り出した fd から読めるバイト列を開きます。
自分の現場で確かめる
読んだだけでは身につきません。手元の環境で実際に見てみてください。
理解度チェック
読んだ直後は「分かった」と感じやすいものです。実際に解いて確かめてください。
listen(2) の backlog は何の上限ですか。
用語集(5語)
- ソケット (socket)
- socket(2) では「通信の端点」。作るとその端点を指す fd が1つ返る。第21週の「IPアドレスとポートの組」は同じ語の別の用法。
- bind
- socket(2) 直後のソケットはアドレスが未割当で、bind(2) がそれを割り当てる。man は伝統的に「ソケットに名前を付ける」と呼ぶと書いている。
- 受動ソケット (passive socket)
- listen(2) がソケットに付ける性質。「accept(2) で入ってくる接続要求を受け付けるために使われるソケット」。接続しに行く側とは役割が逆。
- backlog
- 保留接続のキューが伸びうる最大長。Linux では somaxconn を超える指定は黙って丸められ、既定値は 5.4 以降 4096、それ以前は 128。
- INADDR_ANY (0.0.0.0)
- ip(7) の「ソケットの束縛におけるあらゆるアドレス」。これで bind すると全ローカルインタフェースに束縛される。IPv6 の対応物が `::`。