PTY
Pseudo-Terminal、擬似端末
背景
なぜPTYが必要になったか
1980年代以降、物理端末は消滅した。代わりにGUI上で動くTerminal Emulatorが登場した
しかし問題がある
シェルやvimなどのプログラムは、「自分はTTYデバイスに繋がっている」と思って動く
code:c
// シェルやvimの中のコード
if (isatty(STDIN_FILENO)) {
// TTYに繋がっている → インタラクティブモードで動作
} else {
// パイプやファイルに繋がっている → バッチモードで動作
}
物理端末が消えても、この isatty() が true を返し、stty で設定を変更でき、Ctrl+CでSIGINTが飛ぶ——という挙動を維持する必要がある。既存のプログラムを一切変更せずに。
この互換性を実現するのがPTY
PTYの構造
PTYはmaster/slaveのペアとして作られる
PTY master
PTY slave
PTY master と slave の間には、物理端末(テレタイプ端末)時代と同じ line discipline がいる
code:_
物理端末の時代:
テレタイプ端末 ←→ シリアルケーブル ←→ シリアルドライバ + line discipline ←→ シェル
└──── /dev/ttyS0 ────┘
現代:
ターミナルエミュレータ ←→ PTY master + line discipline + PTY slave ←→ シェル
└───── カーネル内 ─────┘ └ /dev/pts/N ┘
シリアルケーブル + シリアルドライバがまるごと「PTY master」に置き換わっただけ
slave側のインターフェースは変わらない
だからシェルやvimは何も変更しなくていい
ターミナルエミュレータがmaster側で何をしているか
ターミナルエミュレータの主な仕事は3つ
1. キー入力をPTY masterに書き込む
ユーザーが 'a' キーを押す
→ OSのキーイベント(GUI層)
→ ターミナルエミュレータが受け取る
→ PTY master に write(fd, "a", 1)
→ line disciplineがエコーバック処理
→ PTY slave側に 'a' が出力として現れる
→ ターミナルエミュレータがmasterからreadして画面に描画
2. PTY masterからの出力を読んで画面に描画する
シェルが ls を実行し結果を出力
→ シェルが PTY slave に write
→ PTY master 側にデータが現れる
→ ターミナルエミュレータがmasterから read
→ エスケープシーケンスを解釈して画面に描画
3. エスケープシーケンスの解釈
プログラムが出力する \033[31m のようなエスケープシーケンスを解釈して、色、カーソル位置、画面クリアなどの描画処理を行う。
これはターミナルエミュレータの仕事であってカーネルの仕事ではない。
シェルがslave側で何をしているか
シェルから見ると、PTY slaveは普通のTTYデバイス
code:bash
$ tty
/dev/pts/3 # ← 自分が繋がっているPTY slaveのパス
シェルは /dev/pts/3 に対して:
read() → ユーザーの入力を受け取る(line discipline処理済み)
write() → 実行結果を出力する
ioctl() → 端末のウィンドウサイズ取得、raw mode切り替えなど
複数のターミナルタブ/ウィンドウ
ターミナルエミュレータでタブを開くたびに、新しいPTYペアが作られる:
code:_
タブ1: ターミナル ←→ PTY master/slave (/dev/pts/0) ←→ zsh (PID 1234)
タブ2: ターミナル ←→ PTY master/slave (/dev/pts/1) ←→ zsh (PID 1235)
タブ3: ターミナル ←→ PTY master/slave (/dev/pts/2) ←→ zsh (PID 1236)
それぞれが独立したPTYペアを持つので、各タブの入出力は互いに干渉しない。
参考
The TTY demystified — PTY/TTYの仕組みの決定版解説
portable-pty crate — WezTerm由来のクロスプラットフォームPTYライブラリ
nix crate pty module — Rust低レベルPTY API
man 4 pty — macOSのPTYマニュアル
man forkpty — forkpty(3)のマニュアル
Alacritty pty module — 実際のターミナルエミュレータでの実装例