PTY
Pseudo-Terminal、擬似端末
背景
なぜPTYが必要になったか
しかし問題がある
シェルやvimなどのプログラムは、「自分はTTYデバイスに繋がっている」と思って動く
code:c
// シェルやvimの中のコード
if (isatty(STDIN_FILENO)) {
// TTYに繋がっている → インタラクティブモードで動作
} else {
// パイプやファイルに繋がっている → バッチモードで動作
}
物理端末が消えても、この isatty() が true を返し、stty で設定を変更でき、Ctrl+CでSIGINTが飛ぶ——という挙動を維持する必要がある。既存のプログラムを一切変更せずに。
この互換性を実現するのがPTY
PTYの構造
PTYはmaster/slaveのペアとして作られる
code:_
物理端末の時代:
└──── /dev/ttyS0 ────┘
現代:
└───── カーネル内 ─────┘ └ /dev/pts/N ┘
シリアルケーブル + シリアルドライバがまるごと「PTY master」に置き換わっただけ
slave側のインターフェースは変わらない
だからシェルやvimは何も変更しなくていい
ターミナルエミュレータがmaster側で何をしているか
ターミナルエミュレータの主な仕事は3つ
ユーザーが 'a' キーを押す
→ OSのキーイベント(GUI層)
→ ターミナルエミュレータが受け取る
→ PTY master に write(fd, "a", 1)
→ line disciplineがエコーバック処理
→ PTY slave側に 'a' が出力として現れる
→ ターミナルエミュレータがmasterからreadして画面に描画
シェルが ls を実行し結果を出力
→ PTY master 側にデータが現れる
→ ターミナルエミュレータがmasterから read
プログラムが出力する \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ペアを持つので、各タブの入出力は互いに干渉しない。
参考
man 4 pty — macOSのPTYマニュアル
man forkpty — forkpty(3)のマニュアル