tmuxの仕組みを理解する
(PTYとSSH)

リモート接続して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の表示と実行の分離 tmux server ターミナルエミュレータ 表示・入力 接続が切れても無関係 セッション保持 PTY を所有 プロセスは消えない 再接続 tmux attach 状態をそのまま復元 SSH 経由の構造 WezTerm ssh sshd bash tmux server bash pane

この記事では、tmuxが内部でどう動いているかを、ターミナルやPTYといった概念から順に整理します。

関連記事

1. ターミナル、シェル、PTYの違い

まずtmuxを理解するための前提として、概念を区別しておきます。

ターミナル・シェル・PTY の違い ターミナル エミュレータ 文字を表示する WezTerm / iTerm2 PTY 疑似端末デバイス OSが提供する master / slave ペア >_ シェル コマンドを解釈 bash / zsh slave側に接続 ターミナルエミュレータ → PTY master → PTY slave → シェル の順で接続
  • ターミナル(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を起動してシェルを使うとき、内部では次の順序でプロセスが生成されます。

  1. WezTermが openpty() などのシステムコールでPTYを生成する2
  2. WezTermが fork() して子プロセスを作る
  3. 子プロセスが exec() でbashやzshを起動する
  4. シェルの標準入出力がPTY slave側に接続される

WezTermを閉じると、PTYのmaster側が閉じます。
するとslave側にSIGHUP(ハングアップシグナル)が送られ、シェルが終了します3
これが「ターミナルを閉じるとプロセスが死ぬ」理由です。

2. tmuxが解決する問題

SSH接続でリモートサーバを使っているとき、ネットワークが切断されると同じことが起きます。

tmux が解決する問題 問題 SSH切断 PTY が閉じる SIGHUP 送信 プロセスが終了 作業がすべて消える 解決 tmux server が常駐 PTY master を保持 tmux server セッションを保持 SSH再接続後 tmux attach で復帰 tmux client が消えても server は生き続ける

SSHが切れる→PTYが閉じる→SIGHUPが送られる→実行中のプロセスが終了、という流れです。
長時間かかる処理を走らせているときにこれが起きると、作業が全部消えます。

tmuxはこの問題を解決するために設計されています。解決策は単純で、PTYのmaster側を閉じないプロセスを常駐させるというものです。

2.1. tmuxのサーバ・クライアント構造

tmuxを起動すると、2種類のプロセスが生まれます4

tmux サーバ・クライアント構造 tmux server(常駐) session A window 1 pane 1 PTY + bash pane 2 PTY + bash window 2 pane 1 PTY + vim session B … UNIX domain socket tmux client 表示・入力 接続/切断可
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に表示するのが、いわゆる画面分割の実態です。

画面分割と PTY の関係 tmux server I/O多重化(select / epoll)で複数PTYを管理 PTY ペア #1 >_ bash pane 1 シェル常駐 PTY ペア #2 >_ bash pane 2 独立したシェル PTY ペア #3 >_ vim pane 3 別プロセス pane を追加するたびに server が新しい PTY を生成してシェルを接続する

この処理を select()epoll() といったI/O多重化の仕組みで実現しています6

4. まとめ

tmuxの設計を一言で表すなら、「表示と実行を分離する」です。

ターミナルエミュレータは表示を担当し、実際のプロセスはtmux serverが保持します。clientが消えても、serverが生きている限りプロセスは続きます。画面分割・ウィンドウ切り替え・セッション管理はすべて、この構造の上に乗った機能です。

この仕組みを理解すると、tmuxを「なんとなく使う」から「意図して使う」に変わります。

  1. master/slaveという名称は歴史的なものです。Linuxカーネルコミュニティではこの命名を見直す動きがあり、一部のドキュメントではprimary/replicaまたはcontroller/subordinateという表現も使われ始めています。 – pty(7) – Linux manual page
  2. openpty() はBSD由来の関数で、POSIX標準ではありません。POSIX標準の代替として posix_openpt() があり、Linux 2.6.4以降ではBSD方式の疑似端末はdeprecatedとされています。新しいアプリケーションではUNIX 98疑似端末方式(posix_openpt() ベース)の使用が推奨されています。 – pty(7) – Linux manual page
  3. SIGHUPの「HUP」はhangupの略で、モデム時代に電話回線が物理的に切断されたことをOSに知らせる仕組みとして設計されました。キャリア検出(DCD)信号の消失を検知してシグナルを送る経緯があります。現代では物理回線の切断ではなく、制御端末(controlling terminal)が閉じられたことを意味します。 – SIGHUP – Wikipedia
  4. tmuxはNicholas Marriottが2007年に最初のバージョンを公開しました。GNU Screenのコードベースに不満を持ち、より読みやすく拡張しやすい実装を目指して開発されたものです。2009年にOpenBSD 4.6のベースシステムに取り込まれ、広く普及しました。 – Interview with Nicholas Marriott on tmux
  5. tmux serverのsocketはデフォルトで /tmp/tmux-{UID}/default というパスに作成されます。tmux -L フラグでsocket名を変えれば複数の独立したtmuxサーバを並行して起動することも可能です。 – tmux(1) – Linux manual page
  6. tmuxは内部的にlibeventライブラリを使用しており、プラットフォームに応じてepoll(Linux)、kqueue(macOS/BSD)、select(その他)を自動的に切り替えます。epollはselect/pollと異なりO(1)で動作するため、pane数が増えても監視コストが一定に保たれます。 – Event Loops – wonderfly.github.io