リモート接続してCodex CLI作業していると、「自分は今、何の上で何を動かしているのか」という疑問が自然に浮かびます。
- macOSのWezTermでzshを開き、SSHでリモートのLubuntuに接続する。
そこでbashを起動し、tmuxを立ち上げ、その中でCodexを動かす。 - あるいは、macOSでEmacs daemonを常駐させ、Emacs clientのGUI上でvtermを開き、同様にSSH経由でLubuntuのbash、tmux、Codexへとつないでいく。
tmuxは「画面を分割できる便利ツール」として紹介されることが多いです。
でも、その説明だと本質を見落とします。
tmuxの設計の核心は、サーバプロセスがセッションを保持し続けることです。
画面分割はその副産物ということもできます。
この記事では、tmuxが内部でどう動いているかを、ターミナルやPTYといった概念から順に整理します。
1. ターミナル、シェル、PTYの違い
まずtmuxを理解するための前提として、概念を区別しておきます。
- ターミナル(terminal) は、もともとキーボードと画面を持つ物理的な入出力装置です。
1970〜80年代にはDEC VT100のような専用ハードウェアがメインフレームに接続してテキストを送受信するだけに使われました。 - ターミナルエミュレータ(terminal emulator) は、文字の入出力を画面に表示するソフトウェアです。
WezTerm、iTerm2、GNOME Terminalなどがこれにあたり、物理端末の動作をソフトウェアで再現したプログラムです。
コマンドを解釈する機能はなく、それはシェルの役割です。 - シェル(shell) は、コマンドを解釈して実行するプログラムです。
bashやzshがこれです。 - PTY(Pseudo Terminal、疑似端末) は、OSが提供する仮想的な端末デバイスです。
ターミナルエミュレータが起動するとき、OSに対してPTYの生成を要求します。
今日「ターミナル」と呼んでいるものは、ほぼすべてターミナルエミュレータのことです。
PTYはmaster側とslave側のペアで構成され、ターミナルエミュレータがmaster側に、シェルがslave側に接続します。
ターミナルエミュレータ(物理端末の代替)
↕(master側)
PTY(OSが提供する仮想端末デバイス)
↕(slave側)
シェル(コマンド解釈器)
シェルから見ると「本物の端末に接続されている」ように見えますが、実際はPTYのslave側です1。
この抽象化があるため、「端末」という概念をソフトウェア的に何重にも積み重ねることができます。
1.1. 通常の「ターミナル」でのシェルの呼び出し・終了
WezTermを起動してシェルを使うとき、内部では次の順序でプロセスが生成されます。
- WezTermが
openpty()などのシステムコールでPTYを生成する2 - WezTermが
fork()して子プロセスを作る - 子プロセスが
exec()でbashやzshを起動する - シェルの標準入出力がPTY slave側に接続される
WezTermを閉じると、PTYのmaster側が閉じます。
するとslave側にSIGHUP(ハングアップシグナル)が送られ、シェルが終了します3。
これが「ターミナルを閉じるとプロセスが死ぬ」理由です。
2. tmuxが解決する問題
SSH接続でリモートサーバを使っているとき、ネットワークが切断されると同じことが起きます。
SSHが切れる→PTYが閉じる→SIGHUPが送られる→実行中のプロセスが終了、という流れです。
長時間かかる処理を走らせているときにこれが起きると、作業が全部消えます。
tmuxはこの問題を解決するために設計されています。解決策は単純で、PTYのmaster側を閉じないプロセスを常駐させるというものです。
2.1. tmuxのサーバ・クライアント構造
tmuxを起動すると、2種類のプロセスが生まれます4。
tmux server(常駐)
├── session A
│ ├── window 1
│ │ ├── pane 1(PTY + bash)
│ │ └── pane 2(PTY + bash)
│ └── window 2
│ └── pane 1(PTY + vim)
└── session B
└── ...
tmux client ←→ UNIX domain socket ←→ tmux server
Code language: JavaScript (javascript)
- tmux server は長寿命のプロセスです。
内部でPTYを生成・管理し、シェルを保持します。UNIX domain socketを通じてクライアントと通信します5。 - tmux client は表示と入力を担当するプロセスです。
serverと通信して、画面の内容を受け取って表示し、キー入力をserverに送ります。
重要なのは、clientとserverが分離されている点です。
clientが終了してもserverは動き続けます。
2.2. detachしても作業が消えない理由
Ctrl-b d でdetachすると終了するのは、tmux clientプロセス。
tmux serverは生き続けており、PTYのmaster側を保持したままです。
そのため、シェルやその中で動いているプロセスにSIGHUPは送られません。
再接続するときは tmux attach を実行します。
新しいtmux clientが起動し、serverに接続して、既存のセッション状態を表示します。
tmuxが「状態を保存している」わけではありません。
単にプロセスツリーが消えていないだけです。
2.3. SSHとtmuxの組み合わせ
リモートサーバでtmuxを使うときの構造を整理すると、PTYが複数の層で生成されていることがわかります。
ローカルマシン
└── ターミナルエミュレータ(PTY生成)
└── zsh
└── ssh
═══ ネットワーク ═══
sshd(リモート側でPTY生成)
└── bash
└── tmux server(さらにPTY生成)
└── bash(pane内)
それぞれの層でPTYが独立して生成されています。
tmuxがリモート側で動いているため、SSHが切断されてもtmux serverは生き続けます。
再接続後に tmux attach すれば、切断前の状態に戻れます。
3. tmuxの画面分割とPTYの関係
tmux serverが複数のPTYを管理できるからです。
paneを1つ追加するごとに、serverがOSに新しいPTYを要求し、そこにシェルを接続します。
複数のPTYからの入出力をまとめてclientに表示するのが、いわゆる画面分割の実態です。
この処理を select() や epoll() といったI/O多重化の仕組みで実現しています6。
4. まとめ
tmuxの設計を一言で表すなら、「表示と実行を分離する」です。
ターミナルエミュレータは表示を担当し、実際のプロセスはtmux serverが保持します。clientが消えても、serverが生きている限りプロセスは続きます。画面分割・ウィンドウ切り替え・セッション管理はすべて、この構造の上に乗った機能です。
この仕組みを理解すると、tmuxを「なんとなく使う」から「意図して使う」に変わります。
- master/slaveという名称は歴史的なものです。Linuxカーネルコミュニティではこの命名を見直す動きがあり、一部のドキュメントではprimary/replicaまたはcontroller/subordinateという表現も使われ始めています。 – pty(7) – Linux manual page
openpty()はBSD由来の関数で、POSIX標準ではありません。POSIX標準の代替としてposix_openpt()があり、Linux 2.6.4以降ではBSD方式の疑似端末はdeprecatedとされています。新しいアプリケーションではUNIX 98疑似端末方式(posix_openpt()ベース)の使用が推奨されています。 – pty(7) – Linux manual page- SIGHUPの「HUP」はhangupの略で、モデム時代に電話回線が物理的に切断されたことをOSに知らせる仕組みとして設計されました。キャリア検出(DCD)信号の消失を検知してシグナルを送る経緯があります。現代では物理回線の切断ではなく、制御端末(controlling terminal)が閉じられたことを意味します。 – SIGHUP – Wikipedia
- tmuxはNicholas Marriottが2007年に最初のバージョンを公開しました。GNU Screenのコードベースに不満を持ち、より読みやすく拡張しやすい実装を目指して開発されたものです。2009年にOpenBSD 4.6のベースシステムに取り込まれ、広く普及しました。 – Interview with Nicholas Marriott on tmux
- tmux serverのsocketはデフォルトで
/tmp/tmux-{UID}/defaultというパスに作成されます。tmux -Lフラグでsocket名を変えれば複数の独立したtmuxサーバを並行して起動することも可能です。 – tmux(1) – Linux manual page - tmuxは内部的にlibeventライブラリを使用しており、プラットフォームに応じてepoll(Linux)、kqueue(macOS/BSD)、select(その他)を自動的に切り替えます。epollはselect/pollと異なりO(1)で動作するため、pane数が増えても監視コストが一定に保たれます。 – Event Loops – wonderfly.github.io