はじめに

LinuxでCtrl-C打ったらコマンドが中断されるとか。皆さんご存知ですよね。こういうことに関連したちょっとした四択クイズです。Linux使い各位、当然、全問正解できますよね?

問題

いずれもPuTTYなりTeraTermなりのターミナルソフトでLinuxマシンにSSHで接続した、ログインシェル上での対話操作の上でのお話です。ターミナルソフトなんかは特に設定をいじらないことにします。一応。それと、シェルは大体なんでも同じだと思うのですが、念のためbashと指定しておきます。

Q1: シグナル送ったのは誰?

「はじめに」でも書きましたが、Ctrl-Cを入力すると、実行中のコマンドを停止させることができますね。例えば次のように。

Ctrl-Cでの停止
$ cat
^C
$

これはSIGINTというシグナルが送られるためです。このシグナルはkill -INT プロセスIDなんてコマンドなんかで手動で送ることもできますね。

では、Ctrl-C入力で発生するシグナル、誰が送ってるんでしょうか?

  1. 入力データを中継しているsshサーバプロセス ( 一般に sshd )
  2. Ctrl-Cを読み込んだシェル ( bash ) プロセス
  3. Ctrl-Cを読み込んだ、実行中のコマンド ( この例だとcat ) 自身
  4. その他

Q2:入力内容が見えるって便利!!

コマンドに対して、手動で入力を打ち込む場面がたまにあると思います。例えば私なんかだと「次のように操作してもテキストファイルが作れるぜ」みたいに教えられたのですが。

手動で入力を打ち込む例
$ cat > test.txt
hoge
hoge
^D
$

Ctrl-Dで入力を終えると ( ^Dは実際には表示されません )、test.txtに打ち込んだhogeが2行分記録されている、という寸法です。
さて、入力した内容は逐次表示されて分かり易いのですが、これは誰が出してくれてるのでしょうか?

  1. ターミナルソフト ( PuTTYやTeraTerm ) はキーボード入力をそのまま無条件に画面に表示するものだ
  2. シェルがコマンドに入力内容を渡すと同時に、何が入力されているかが分かるように、ターミナルソフト側へ出力している。
  3. 入力を受けたコマンド自身が、内容が分かるように出力している
  4. その他

Q3:バックスペースで訂正できるんです

先ほどの問題のように手動で入力する場合、途中で間違えてもBackspaceキーで訂正できたりしますね。環境にもよりますが、代わりに Ctrl-H でもできたりします。
※余談ですが、口を滑らせた風に訂正を表現するために^H を使うのがかつて流行ったことがあります。つまりAABBというのを表現するのにAA^H^HBBとするわけです。例えば「この動作は○○ソフトのバグ^H^H仕様によるものですね」とか

例えば次のような感じです。なお、^Hというのはバックスペースをそこで打ち込んだ、ということを表していて、実際に^Hが表示されている訳ではないです。( ^Dも同様 )

バックスペースで訂正
$ cat > test.txt
ho^H^Huge
^D
$ cat test.txt
uge

さてこのとき、バックスペースで実際に訂正を行っているのは誰でしょうか?

  1. ターミナルソフトは、Enterを押して行の内容を確定する前なら、Backspaceで送信する内容を訂正してくれる
  2. シェルがコマンドに内容を渡す前に訂正を行う
  3. コマンド自体がバックスペースの入力を検知すると、内部的に前の入力を削除し訂正している
  4. その他

解説

解答

ということで解答です。

ここを開いて詳細を出すと解答が載っています答えは全て 4 の「その他」でした。1~3 なんて選んでませんよね?

解説の前に

手前味噌ながら、先日の標準入力・標準出力ってなに?で、SSHログイン時には次の図のような入出力経路が整えられることに触れました。

a

なので、ターミナルソフト⇔シェルの中間に、OSの仲介があるわけです。なので今回の話の焦点はその仲介が何か、ということでタイトルにあるTTY/PTYの機能なのです。

A1: 制御端末

こちらの答えは「4.その他」なのですが、じゃあ誰がシグナルを送っているかというと、実はその正体はOS(kernel)です。
次の図のように、sshdは素直にCtrl-Cに相当する文字データ ( ASCIIコード3のETX ) をPTYマスターに出力します。kernelはそれを受けて、PTYスレーブにデータを送る代わりに、そこに紐づいているcatにINTシグナルを送るのです。
image.png

ちょっと思い出して頂きたいのですが、catに限らずpasswd等のroot特権で動くコマンドであってもシグナルが送られますよね? これは一介のプロセスでは無理な仕事で、kernel直々に行っているからこそ、なのです。

このように、「紐づいているプロセスに何か働きかける」そのような対象となるTTY/PTYデバイスを制御端末と言っています。このようなケースでは、シェルが起動した時点で既に制御端末として、このPTYスレーブが紐づき、シェルからcatへ引き継がれるわけです。
psコマンドのTTYの欄に出るのがそれです。

~$ ps -f
UID        PID  PPID  C STIME TTY          TIME CMD
angel      392   391  0 14:41 pts/2    00:00:00 bash
angel      414   392  0 14:55 pts/2    00:00:00 ps -f

あれ? じゃあ、シグナルはシェルの方にも送られてるの? とかこまごま気になる点は出てくるかと思いますが、そこらへんはジョブ制御という機構で良いように調整されています。

なお、サービスとして動作するプロセスは、むしろこのような紐づきがあると困るので、制御端末なしという状態を選ぶのが一般的です。コマンドレベルでも制御端末を切り離すことはできて、setsidというコマンドを使います。

$ setsid sleep 1200
$ ps -fC sleep
UID        PID  PPID  C STIME TTY          TIME CMD
angel      416     1  0 14:59 ?        00:00:00 sleep 1200

このような感じで、TTYの欄が?というのが制御端末なしという状況です。良く、バックグラウンドでジョブを動かし続けるという目的でnohupが紹介されることがありますが、setsidの方がある意味強力と言えるでしょう。

A2:端末エコー

こちらの答えも「4.その他」なのですが、じゃあ誰が入力内容の表示元となるデータを生み出しているかというと、実はその正体もOS(kernel)です。

image.png

PTY/TTYにはエコーという機能があり、これが有効になっている場合、ユーザからsshdを経由して送られてきたデータを、スレーブ側に伝えると同時にsshd側へもそっくりそのまま ( とは言え、付加データがあるのでちょっと違いますが ) 返すのです。

なお、エコー機能を無効にすることももちろんできて、passwdコマンド実行時に入力したパスワードが表示されないのが典型です。

A3:COOKEDモード

こちらの答えも「4.その他」なのですが、じゃあ誰がデータの訂正を行っているかというと、実はその正体もOS(kernel)です。( しつこい )

sshdからは1文字ずつデータがPTYマスターに来るのですが、kernelのTTY/PTYドライバではそれをバッファに溜める挙動を採ります。これをCOOKEDモードと呼んでいます。この際、BackSpaceでバッファ内の文字を削除できるのです。

挙動としては次のようになります。( なおエコーにより返される分は省いています )
行が終わってEnterの入力があると、そこでようやくバッファに溜めたデータを ( 改行文字もつけて ) スレーブ側に送り出すのです。

image.png

なお、COOKEDとは別のモードもあって、バッファに溜めずに直ぐにスレーブに送り出すRAWモードというのもあります。今の高機能なシェルは、キーを押す毎にきめ細かなコマンドライン編集ができるようになっています。改行までデータが来るのを待たされるCOOKEDモードでは都合が悪いので、RAWの方を用いるのです。

おまけ

クイズにはしませんでしたが、Ctrl-Dによって入力が終了となる挙動…、これもどうなっているのでしょうか?
賢明な読者の方ならばお気づきでしょうが、それもPTY/TTYの機能です。
kernelのPTY/TTYドライバが、Ctrl-D ( ASCIIコード4、EOT ) を認識すると、スレーブからの読み込み動作に対して「入力終了」のステータスを返すのですね。

しかしながら、その後もシェルなんかが読み込みを続けるわけですから、本当にデータがなくなったわけではありません。「入力終了 ( 終了とは言ってない )」なので、強引に読めばもっと読むことができます。

例えばcat - - -というコマンド、標準入力指定となる-を3回指定していると本当に3回分読み込んでくれます。つまり、Ctrl-D3回分まで読んでくれるということです。

$ cat - - - > test.txt
a
^D
b
^D
c
^D
$ cat test.txt
a
b
c
$

ただ面白いのは、bashなんかでCtrl-Dがログアウトとなる処理。これは実は「入力終了」扱いではなく、本当にbashがCtrl-Dの文字を認識しています。つまり、Ctrl-Dに相当するASCIIコード4を直接渡すとそこで終わっちゃいます。

$ echo -e 'PS1=\ndate\n\04\ndate' | bash -i
$ PS1=
date
Sun Dec 24 15:41:59 DST 2017
exit
$ echo -e 'PS1=\ndate\n\04date' | bash
Sun Dec 24 15:45:22 DST 2017
bash: line 3: $'\004': command not found
Sun Dec 24 15:45:22 DST 2017
$

ちょっと分かりにくいですが、echo -eで非印字文字をbashに渡した場合の実行例です。なおこの挙動はインタラクティブモード ( -i等で指定 ) の時限定で、そうでない場合は普通のコマンドと解釈しようとします。

終わりに

PTY/TTYの機能と言うのも地味ですが、ここら辺の挙動の認識が甘いともやっとすることもあるのではないでしょうか?
なお、こういった機能はOSのAPIで ( コマンドとしては stty で ) 色々変えることができるようになっています。「なんで状況によって違うの?」と疑問に思った場合は、sttyのマニュアルを見るとより深く理解できる…かもしれません。