LoginSignup
179
147

[翻訳] 端末の神秘を解き明かす

Last updated at Posted at 2016-03-05

原文

The TTY demystified
http://www.linusakesson.net/programming/tty/

翻訳

TTYサブシステムはLinuxおよびUNIX一般の設計において中心的な位置を占めます。しかし、不幸なことにその重要性はしばしば見過ごされ、良い入門記事を見つけづらくなっています。それでも、LinuxにおけるTTYの基礎知識は開発者やパワーユーザーにとって不可欠です。

これから見ていくシステムはあまりエレガントではありません。TTYシステムはユーザーから見るととても便利ですが、実際は特殊ケースが多く、入り組んでいます。なぜこうなっているのかを理解するには、歴史を紐解く必要があります。

oldschool tty

歴史

1869年、株価表示器が発明されました。株価表示器は、タイプライター、長いワイヤーのペア、表示用紙テーププリンタから構成される電気機械で、遠く離れた場所にリアルタイムで株価を配信することを目的としていました。このコンセプトはやがてより高速なASCIIベースのテレタイプに発展しました。テレタイプは世界中に大きなネットワークを張り巡らすようになり、テレックスと呼ばれていました。テレックスは商用の電報を伝送するのに使われましたが、まだコンピュータには接続されていませんでした。

一方、コンピュータが発展すると(まだ巨大で原始的なものでしたが)ユーザーとリアルタイムに対話することができるようになりました。コマンドラインが旧来のバッチ処理モデルに取って代わるようになると、入出力デバイスとしてテレタイプが使われるようになりました。そのときテレタイプがすでに市場に広まっていたからです。

テレタイプには無数の製品が存在し、それぞれ微妙に違っていました。そのため、互換性を提供するなんらかのソフトウェアレイヤーが求められました。UNIXの世界では、ワード長、ボーレート、フロー制御、パリティ、基本的な行編集のための制御コードなど全ての低レベルの詳細をOSカーネルが処理するアプローチが取られました。1970年代後期に、VT-100のようなソリッドステートビデオ端末が現れ、高度なカーソル移動、カラー出力、などの高度な機能が可能になりましたが、それらはカーネルでなくアプリケーションに任されていました。

現在では物理的なテレタイプやビデオ端末はほぼ絶滅しています。博物館やハードウェアマニアのところへ行くのでない限り、あなたが目にするTTYとは、本物のビデオ端末をソフトウェア的にエミュレートしたものです。しかしこれから見る通り、その古い機械の遺産が今でも水面下で生き残っているのです。

端末の使われ方

case1.png

ユーザーは端末(物理的なテレタイプ)の前でタイプします。この端末は物理回線を通してコンピュータのUART(Universal Asynchronous Receiver and Transmitter/汎用非同期送受信器)に接続されています。OSにはUARTドライバが含まれており、UARTドライバがパリティチェックやフロー制御など、バイト列の物理的な伝送を管理します。素朴なシステムでは、さらにUARTドライバが受け取ったバイト列を直接アプリケーションプロセスに渡すでしょう。しかしこのアプローチでは次のような基本的な機能が欠けてしまいます:

行編集。ユーザーはタイプミスをします。そのためバックスペースキーが有用です。これを個々のアプリケーションが実装しなければなりません。しかしUNIXの設計思想に従うなら、アプリケーションは可能な限りシンプルにしておくべきです。そこで利便性のため、OSが行編集用のバッファと基本的な編集コマンド(バックスペース、単語削除、行クリア、再描画)を提供することになりました。これはラインディシプリンの中にあり、デフォルトで有効になっています。アプリケーションはこれらの機能を無効にすることもできます。それにはラインディシプリンをデフォルトのcookedモード(またはcanonicalモード)からrawモードへ変更します。対話的なアプリケーションのほとんど(エディタ、メールユーザーエージェント、シェル、cursesやreadlineに依存する全てのプログラム)はrawモードで動作し、全ての行編集コマンドをアプリケーション自身が制御します。またラインディシプリンは、文字のエコー出力やCRとLFの自動変換のオプションも持っています。これをカーネルレベルの原始的なsed(1)と考えることもできます。

ちなみに、カーネルはいくつかの異なるラインディシプリンを提供しています。シリアルデバイスごとにそのうちの1つだけがアタッチされます。デフォルトのディシプリン(行編集を提供する)はN_TTYと呼ばれ、drivers/char/n_tty.cにあります [訳注: linux-4.4.3_2ではdrivers/tty/n_tty.c]。他のディシプリンは他の目的に使われますが(ppp、IrDA、シリアルマウスといったパケット交換データなど)、それはこの記事の範囲外です。

セッション管理。ユーザーは複数のプログラムを同時に動かし、その内の1つと対話することを望みます。プログラムが無限ループに陥った場合はそれをキルするかサスペンドしたいでしょう。バックグラウンドで開始したプログラムは、端末に書き込む必要が出るまで実行し、その必要が出たときにサスペンドするべきです。ユーザーの入力はフォアグラウンドのプログラムだけに渡されるべきです。OSはこれらの機能をTTYドライバの中で実装しています (drivers/char/tty_io.c [訳注: linux-4.4.3_2ではdrivers/tty/tty_io.c])。

OSのプロセスは「alive」です(実行コンテキストを持っている)。TTYドライバは、オブジェクト指向の用語で言うとaliveではなく、受動的なオブジェクトです。TTYドライバはデータフィールドとメソッドを持ちますが、それが実際に何かをするには、他のプロセスのコンテキストまたはカーネルの割り込みハンドラからメソッドを呼ばれる必要があります。ラインディシプリンも同様に受動的な存在です。

UARTドライバ、ラインディシプリンのインスタンス、TTYドライバの三つ組をTTYデバイス、または単にTTYと呼ぶことがあります。ユーザープロセスは/devの下の対応するデバイスファイルを操作することで、TTYデバイスの挙動を変えることができます。そのデバイスファイルへの書き込み許可が必要なので、ユーザーがあるTTYにログインしたとき、そのユーザーがそのデバイスファイルの所有者にならなければなりません。これは伝統的にlogin(1)プログラムによって行われます。loginはroot権限で動作します。

先ほどの図の物理回線(Physical line)はもちろん長距離電話回線であっても構いません。

case2.png

この図は大きく変わっていませんが、システムがモデムの回線切断(ハングアップ)を制御しなければならなくなっています。

典型的なデスクトップシステムに移りましょう。Linuxのコンソールはこのようになっています:

case3.png

TTYドライバとラインディシプリンは前の例と同じです。しかしUARTや物理端末はもう存在しません。代わりにビデオ端末(これは複雑な状態機械で、文字と文字属性によるフレームバッファを含んでいます)がソフトウェアによってエミュレートされており、VGAディスプレイに描画されます。

コンソールサブシステムはある意味硬直しています。端末エミュレーションをユーザーランドへ移動させれば、物事はより柔軟に、そして抽象的になります。そしてこれがxterm(1)とそのクローンが動作する様子です:

case4.png

TTYサブシステム(セッション管理とラインディシプリン)に変更を加えず、端末エミュレーションをユーザーランドへ移動させるために、擬似端末(pty)が発明されました。もう想像していたかもしれませんが、screen(1)やssh(1)のように擬似端末の中で擬似端末を立ち上げると事態はより複雑になります。

さて、一度戻って、これらがどのようにプロセスモデルに合わさるのか見てみましょう。

プロセス

Linuxのプロセスは以下の状態のどれか1つに当てはまります:

プロセス状態

R	Running or runnable      実行可能キューに入っている状態
D	Uninterruptible sleep     割り込み不可能なスリープ(イベント待ち)
S	Interruptible sleep      割り込み可能なスリープ(イベントまたはシグナル待ち)
T	Stopped      停止。ジョブ制御やデバッガにより停止された状態
Z	Zombie      ゾンビ。終了したがまだ親にwait(2)されていない状態

ps lを実行すると、どのプロセスが実行中でどのプロセスがスリープ中なのか分かります。プロセスがスリープ中の場合、WCHANのカラムを見ればどのカーネルイベントを待っているのか分かります("wait channel"はウェイトキューの名前)。

$ ps l
F   UID   PID  PPID PRI  NI    VSZ   RSS WCHAN  STAT TTY        TIME COMMAND
0   500  5942  5928  15   0  12916  1460 wait   Ss   pts/14     0:00 -/bin/bash
0   500 12235  5942  15   0  21004  3572 wait   S+   pts/14     0:01 vim index.php
0   500 12580 12235  15   0   8080  1440 wait   S+   pts/14     0:00 /bin/bash -c (ps l) >/tmp/v727757/1 2>&1
0   500 12581 12580  15   0   4412   824 -      R+   pts/14     0:00 ps l

"wait"はwait(2)システムコールに対応します。つまり、これらのプロセスは、子プロセスのどれかに状態変化が起きたとき実行可能状態に移ります。スリープ状態には割り込み可能と割り込み不可能の2種類があります。割り込み可能なスリープ(最も一般的なケース)とは、プロセスがウェイトキューに入っているが、シグナルを受け取ったとき実行可能状態に移れることを意味します。カーネルのソースコードを見ると、イベント待ちをしているコードではどこでも、schedule()から戻った後でシグナルが保留されているかチェックしていることが分かるでしょう。そしてその場合はシステムコールをエラー終了させます。

上記のpsの出力の中で、STATのカラムは各プロセスの状態を示しています。このカラムにはそれ以外の属性(あるいはフラグ)が含まれることがあります:

s	プロセスがセッションリーダーである
+	プロセスがフォアグラウンドプロセスグループに属する

これらの属性はジョブ制御のために使われます。

ジョブとセッション

ジョブ制御とは、^Zを押してプログラムをサスペンドしたときや、&を使ってプログラムをバックグラウンドで起動したときに起こるものです。jobs、fg、bgなどのシェル組み込みコマンドを使ってセッションの中の既存のジョブを操作できます。各セッションはセッションリーダー(シェル)によって管理され、シグナルとシステムコールの複雑なプロトコルを使ってカーネルと緊密に連携します。
以下の例はプロセス、ジョブ、セッションの関係を示しています:

以下のシェルの状態は

exampleterm.png

以下のプロセスに対応します。

examplediagram.png

そして以下のカーネル構造に対応します。

  • TTY Driver (/dev/pts/0)

サイズ: 45x13
制御プロセスグループ: (101)
フォアグラウンドプロセスグループ: (103)
UART 設定(xtermなので無視される): ボーレート、パリティ、ワード長、など多数
ラインディシプリン設定: cooked/raw モード、改行文字訂正、割り込み文字の意味など。
ラインディシプリン状態: 編集バッファ(今は空)、バッファ中のカーソル位置など。

  • pipe0

読み込み側(PID 104のファイルディスクリプタ0に接続されている)
書き込み側(PID 103のファイルディスクリプタ1に接続されている)
バッファ

基本的な考えとして、1つのパイプラインは1つのジョブです。なぜなら、1つのパイプラインの中の全てのプロセスは同時に操作(停止、再開、キル)されるべきだからです。これがkill(2)でプロセスグループ全体にシグナルを送れるようになっている理由です。デフォルトでは、fork(2)は新しく生成された子プロセスを親と同じプロセスグループに入れます。キーボードで^Cが押されたときなど、親子両方を制御できるようにするためです。しかしシェルはセッションリーダーのつとめとして、パイプラインを立ち上げるたびに新しいプロセスグループを生成します。

TTYドライバはフォアグラウンドプロセスグループIDを記憶しておきますが、あくまでも受動的にです。セッションリーダーが必要に応じて明示的にこの情報を更新しなければなりません。同様に、TTYドライバは接続している端末のサイズを記憶しますが、この情報もターミナルエミュレータあるいはユーザーが明示的に更新しなければなりません。

上の図から分かるように、複数のプロセスの標準入力が/dev/pts/0にアタッチされています。しかしフォアグラウンドジョブ(ls | sortのパイプライン)だけがTTYからの入力を受け取ります。同様に、フォアグラウンドジョブだけがTTYデバイスに書き込むことができます(デフォルト設定の場合)。もしcatのプロセスがTTYに書き込もうとすると、カーネルがシグナルを送ってcatをサスペンドします。

シグナルの狂気

さて、カーネル内のTTYドライバ、ラインディシプリン、UARTドライバがユーザーランドのプロセスとどうコミュニケートするかを詳しく見てみましょう。

UNIXではもちろんファイル(TTYデバイスファイルを含む)は読み書きでき、さらにioctl(2)を使って操作することができます。ioctlはUNIXのスイスアーミーナイフです。TTY関連のioctl操作が多数定義されています。しかしioctlの呼び出しはプロセスから行われなければなりません。そのため、カーネルがアプリケーションと非同期に通信したいときには使えません。

銀河ヒッチハイク・ガイドで、ダグラス・アダムスは極めて退屈な惑星について言及しています。そこには多数の落ち込んだ人間と、鋭い牙を持ち、太ももに噛みつくことで人間とコミュニケーションするある種の動物が住んでいます。これはUNIXと酷似しています。UNIXでは、カーネルはプロセスに対し、動けなくしたりキルするシグナルを送ることでコミュニケートします。プロセスはシグナルを受け取ってそれを処理することもありますが、ほとんどはそうしません。

シグナルは、カーネルがプロセスと非同期にコミュニケートするための無骨なメカニズムです。UNIXにおけるシグナルはクリーンでも一般的でもなく、各々のシグナルは独自で、個々に勉強しなければなりません。

コマンドkill -lでシステムにどんなシグナルが実装されているか調べることができます。これは一例です:

$ kill -l
 1) SIGHUP	 2) SIGINT	 3) SIGQUIT	 4) SIGILL
 5) SIGTRAP	 6) SIGABRT	 7) SIGBUS	 8) SIGFPE
 9) SIGKILL	10) SIGUSR1	11) SIGSEGV	12) SIGUSR2
13) SIGPIPE	14) SIGALRM	15) SIGTERM	16) SIGSTKFLT
17) SIGCHLD	18) SIGCONT	19) SIGSTOP	20) SIGTSTP
21) SIGTTIN	22) SIGTTOU	23) SIGURG	24) SIGXCPU
25) SIGXFSZ	26) SIGVTALRM	27) SIGPROF	28) SIGWINCH
29) SIGIO	30) SIGPWR	31) SIGSYS	34) SIGRTMIN
35) SIGRTMIN+1	36) SIGRTMIN+2	37) SIGRTMIN+3	38) SIGRTMIN+4
39) SIGRTMIN+5	40) SIGRTMIN+6	41) SIGRTMIN+7	42) SIGRTMIN+8
43) SIGRTMIN+9	44) SIGRTMIN+10	45) SIGRTMIN+11	46) SIGRTMIN+12
47) SIGRTMIN+13	48) SIGRTMIN+14	49) SIGRTMIN+15	50) SIGRTMAX-14
51) SIGRTMAX-13	52) SIGRTMAX-12	53) SIGRTMAX-11	54) SIGRTMAX-10
55) SIGRTMAX-9	56) SIGRTMAX-8	57) SIGRTMAX-7	58) SIGRTMAX-6
59) SIGRTMAX-5	60) SIGRTMAX-4	61) SIGRTMAX-3	62) SIGRTMAX-2
63) SIGRTMAX-1	64) SIGRTMAX

見て分かる通り、シグナルには1から始まる番号がついています。しかし、ビットマスクで表現されるときには(例えばps sの出力)、最下位ビットがシグナル1に対応します。

この記事では次のシグナルについて説明します:SIGHUP, SIGINT, SIGQUIT, SIGPIPE, SIGCHLD, SIGSTOP, SIGCONT, SIGTSTP, SIGTTIN, SIGTTOU, SIGWINCH

SIGHUP

デフォルトの動作: 終了
起こりうる動作: 終了, 無視, 関数呼び出し
SIGHUPは、回線切断(ハングアップ)が検出されたときにUARTドライバによってセッション全体に送信されます。通常、これは全てのプロセスをキルします。nohup(1)やscreen(1)などいくつかのプロセスは、自身のセッション(とTTY)をデタッチし、子プロセスが影響を受けないようにします。

SIGINT

デフォルトの動作: 終了
起こりうる動作: 終了, 無視, 関数呼び出し
SIGINTは、割り込み文字(普通は^C。ASCIIコードの3)が入力ストリームに現れたとき、TTYドライバから現在のフォアグラウンドジョブに送られます(これを無効にすることもできます)。TTYデバイスに書き込みができるユーザーであれば、この割り込み文字を変えたり、この機能の有効無効を切り替えることができます。また、セッションマネージャーは各ジョブのTTY設定を記憶しており、ジョブ切り替えが起きたときにTTYを更新します。

SIGQUIT

デフォルトの動作: コアダンプ
起こりうる動作: コアダンプ, 無視, 関数呼び出し
SIGQUITはSIGINTと同様ですが、quit文字は通常^\であり、デフォルトの動作が異なっています。

SIGPIPE

デフォルトの動作: 終了
起こりうる動作: 終了, 無視, 関数呼び出し
プロセスが読み手のいないパイプに書き込もうとしたとき、カーネルがそのプロセスにSIGPIPEを送ります。これがないとyes | headのようなジョブが永遠に終了しなくなってしまうでしょう。

SIGCHLD

デフォルトの動作: 無視
起こりうる動作: 無視, 関数呼び出し
プロセスが死んだときや状態が変わったとき(停止・再開)、カーネルは親プロセスにSIGCHLDを送ります。SIGCHLDは追加的な情報、つまり終了したプロセスのプロセスID、ユーザーID、終了ステータス(または終了シグナル)と実行時間統計を送ります。セッションリーダー(シェル)はこのシグナルを使ってジョブの状態を管理します。

SIGSTOP

デフォルトの動作: サスペンド
起こりうる動作: サスペンド
このシグナルは無条件にプロセスをサスペンドします。つまり、このシグナルを受け取ったときの動作を変更することはできません。しかし、ジョブ制御の際にカーネルはSIGSTOPを送らないことに気をつけてください。^Zは通常、SIGTSTPを送信します。SIGTSTPはアプリケーションが捕捉できます。そしてアプリケーションは例えばカーソルを画面の一番下に移動させ、端末を知っている状態に変更し、それからSIGSTOPを使って自分自身をスリープさせるといったことができます。

SIGCONT

デフォルトの動作: 再開
起こりうる動作: 再開, 再開 + 関数呼び出し
SIGCONTは停止しているプロセスを再開します。ユーザーがfgコマンドを実行したとき、シェルが明示的に送信します。SIGSTOPはアプリケーションが捕捉できないので、予期せずSIGCONTを受け取った場合は、プロセスが以前に(SIGSTOPで)サスペンドされ、それから再開されたことを意味している可能性があります [訳注: ここちょっと自信ないです。原文はSince SIGSTOP can't be intercepted by an application, an unexpected SIGCONT signal might indicate that the process was suspended some time ago, and then un-suspended.]。

SIGTSTP

デフォルトの動作: サスペンド
起こりうる動作: サスペンド, 無視, 関数呼び出し
SIGTSTPはSIGINTとSIGQUITと同様ですが、トリガーとなる文字は通常^Zであり、デフォルトの動作はプロセスをサスペンドさせます。

SIGTTIN

デフォルトの動作: サスペンド
起こりうる動作: サスペンド, 無視, 関数呼び出し
バックグラウンドジョブのプロセスがTTYデバイスから読み込もうとしたとき、TTYがそのジョブ全体にSIGTTINを送ります。これは通常、ジョブをサスペンドさせます。

SIGTTOU

デフォルトの動作: サスペンド
起こりうる動作: サスペンド, 無視, 関数呼び出し
バックグラウンドジョブのプロセスがTTYデバイスに書き込もうとしたとき、TTYがそのジョブ全体にSIGTTOUを送ります。これは通常、ジョブをサスペンドさせます。TTYごとにこの機能を無効にすることもできます。

SIGWINCH

デフォルトの動作: 無視
起こりうる動作: 無視, 関数呼び出し
前述の通り、TTYデバイスは端末サイズを記憶していますが、この情報は手動で更新されなければなりません。端末サイズが変更されると、TTYデバイスはフォアグラウンドにSIGWINCHを送ります。エディタなどの対話的なアプリケーションで行儀のよい物はこれに反応し、TTYデバイスから新しい端末サイズを取得して画面を再描画します。

端末ベースのエディタでファイルを編集しているとしましょう。カーソルは画面上のどこかにあり、エディタはCPU負荷のかかるタスク(巨大なファイル上で置換など)を実行中でビジーであるとします。ここで^Zを押します。ラインディシプリンはこの文字(^ZはASCIIコード26で表される1バイト)を捕捉するように設定されているので、エディタがタスクを完了してTTYデバイスから読み込みを始めるのを待つ必要はありません。即座にラインディシプリンがSIGTSTPをフォアグラウンドプロセスグループに送信します。このプロセスグループにはエディタと他の子プロセスが含まれます。

エディタは既にSIGTSTPのシグナルハンドラを設定してあるので、カーネルはプロセスにシグナルハンドラを実行させます。シグナルハンドラはTTYデバイスへしかるべき制御シーケンス [訳注:エスケープシーケンスともいう] を書き込むことにより、カーソルを画面の最終行へ移動させます。エディタはまだフォアグラウンドにいるため、この制御シーケンスは要求通りに送られます。そしてその後エディタは自分自身のプロセスグループにSIGSTOPを送ります。

ここでエディタは停止します。停止したことがSIGCHLDによってセッションリーダーに通知され、そのシグナルにはサスペンドしたプロセスのIDが含まれています。フォアグラウンドジョブの全てのプロセスがサスペンドしたとき、セッションリーダーはTTYデバイスから現在の設定を読み出し、後で利用するために保存しておきます。続けてセッションリーダーはioctlを使い、自分自身をTTYのフォアグラウンドプロセスグループとして登録します。そして"[1]+ Stopped"のような文字列を表示して、ジョブがサスペンドされたことをユーザーに通知します。

この時点でps(1)を実行すると、エディタのプロセスが停止状態("T")にいることが分かります。シェルの組み込みコマンドbgか、kill(1)でSIGCONTを送ると、エディタは再開してSIGCONTのシグナルハンドラを実行します。このシグナルハンドラはおそらくTTYデバイスに書き込みをしてエディタの画面を再描画しようとするでしょう。しかしこのエディタは今バックグラウンドにいるので、TTYデバイスがそれを許可しません。代わりにTTYはエディタにSIGTTOUを送ります。これによりエディタは再び停止します。停止したことがSIGCHLDによってセッションリーダーに通知され、シェルは再び"[1]+ Stopped"を端末に書き込みます。

fgを入力すると、シェルはまず以前に保存しておいたラインディシプリンの設定を復元します。シェルはTTYドライバに対し、今からエディタのジョブをフォアグラウンドジョブとして扱うように通知します。そして最後にSIGCONTをプロセスグループに送ります。エディタのプロセスは画面を再描画しようとし、今回はSIGTTOUで邪魔されません。今はフォアグラウンドジョブに属しているからです。

フロー制御とブロッキングIO

xtermでyesを実行すると、膨大な数の"y"の行が流れていくのを目にするでしょう。当然のことながら、yesのプロセスは"y"の行を高速に出力することができ、それはxtermがパースしてフレームバッファを更新し、Xサーバーと通信してウィンドウをスクロールさせるよりはるかに高速です。これらのプログラムはどうやって協調しているのでしょう?

その答えはブロッキングIOにあります。擬似端末はある一定量のデータしかカーネルバッファ内部に留めておくことができず、バッファが一杯になったのにまだyesがwrite(2)を呼び出すと、write(2)はブロックし、yesのプロセスは割り込み可能なスリープ状態になります。xtermがバッファされたデータを読み取るまでスリープ状態が続きます。

TTYがシリアルポートに接続されているときも同じことが起こります。yesは(例えば)9600ボーよりはるかに高速にデータを転送できますが、シリアルポートの上限はそのスピードです。カーネルのバッファはすぐに一杯になり、それ以後のwrite(2)の呼び出しはプロセスをブロックさせます(プロセスがノンブロッキングIOを要求した場合はエラーコードEAGAINで失敗します)。

カーネルバッファに空きスペースがあるのにTTYを明示的にブロック状態にできると言ったらどう思いますか?そのTTYにwrite(2)をしようとしたプロセスは自動的にブロックします。この機能の使い道は何でしょうか?

9600ボーの古いVT-100端末を使っていると仮定しましょう。端末に画面をスクロールさせるための複雑な制御シーケンスを送りました。端末はスクロール操作によってビジーになり、9600フルのボーレートで新しいデータを受け取ることができなくなります。物理的には端末のUARTは9600ボーのままで動作しますが、端末には受け取った文字を一時的に収めておくバッファが足りなくなります。これこそTTYをブロック状態にしておくべきときです。しかし端末側からはこれをどうやればいいでしょう?

TTYデバイスを設定して、あるバイトを特別に扱うようにできることは既に学びました。例えば、デフォルト設定では^Cというバイトはアプリケーションのread(2)で読み取られず、SIGINTをフォアグラウンドジョブに送るのでした。同様に、ストップフローバイトとスタートフローバイトに反応するようにTTYを設定することができます。これらは通常は^S(ASCIIコード19)と^Q(ASCIIコード17)です。古いハードウェア端末はこれらのバイトを自動的に送り、OSがデータフローを調整してくれることを期待します。これをフロー制御と呼びます。そしてこれが、うっかり^Sを押したときxtermが固まってしまうように見える理由です。

ここには重要な違いがあります:フロー制御によって停止している、またはカーネルバッファが一杯になっているTTYに書き込んだときはプロセスがブロックします。一方、バックグラウンドジョブからTTYへの書き込みはSIGTTOUを発生させ、プロセスグループ全体をサスペンドさせます。なぜUNIXの設計者達がブロッキングIOに頼るのでなく、わざわざSIGTTOUとSIGTTINを発明しなければならなかったのか私には分かりませんが、私の推測では、TTYドライバはジョブ制御を担当しており、ジョブ全体を監視して操作するように設計されており、ジョブ内の個々のプロセスを対象としていないのでしょう。

TTYデバイスの設定

シェルの制御端末の名前を調べるには、前述のps lの出力を見れば分かります。あるいは単にtty(1)コマンドを実行します。

プロセスはオープンしてあるTTYデバイスの設定をioctl(2)で読みだしたり変更することができます。このAPIはtty_ioctl(4)で説明されています。これはLinuxのアプリケーションとカーネルの間のバイナリインターフェイスの一部なので、Linuxのバージョンが上がっても変わらないでしょう。しかしこのインターフェイスは移植性がないので、アプリケーションはむしろtermios(3)のmanページに記載されているPOSIXのラッパーを使うべきです。

termios(3)の詳細には立ち入りませんが、Cプログラムを書いていて^CがSIGINTになる前に捕捉したい場合は、行編集と文字のエコーを無効にし、シリアルポートのボーレートを変更し、フロー制御を無効化するなどします。前述のmanページに必要なことが書いてあります。

TTYデバイスを操作するためのstty(1)というコマンドラインツールもあります。これはtermios(3)のAPIを使っています。

試してみましょう。

$ stty -a
speed 38400 baud; rows 73; columns 238; line = 0;
intr = ^C; quit = ^\; erase = ^?; kill = ^U; eof = ^D; eol = <undef>; eol2 = <undef>; swtch = <undef>; start = ^Q; stop = ^S; susp = ^Z; rprnt = ^R; werase = ^W; lnext = ^V; flush = ^O; min = 1; time = 0;
-parenb -parodd cs8 -hupcl -cstopb cread -clocal -crtscts
-ignbrk brkint ignpar -parmrk -inpck -istrip -inlcr -igncr icrnl ixon -ixoff -iuclc -ixany imaxbel -iutf8
opost -olcuc -ocrnl onlcr -onocr -onlret -ofill -ofdel nl0 cr0 tab0 bs0 vt0 ff0
isig icanon iexten echo echoe echok -echonl -noflsh -xcase -tostop -echoprt echoctl echoke

sttyの-aフラグは全ての設定を表示させます。デフォルトでは、シェルにアタッチされているTTYデバイスを対象にしますが、-Fで他のデバイスを指定することもできます。

これらの設定はUARTのパラメータに関係するもの、ラインディシプリンに影響するもの、ジョブ制御に影響するものがあります。全てが一緒くたになっています。1行目を見てみましょう:

項目名 関連システム 意味
speed UART ボーレート。擬似端末の場合は無視される。
rows, columns TTYドライバ このTTYデバイスにアタッチされている端末の(誰かが考える)サイズ。文字単位。基本的に、これはただのカーネルスペース内の変数の組に過ぎず、自由にセット、ゲットできます。セットするとTTYドライバがフォアグラウンドジョブにSIGWINCHを送ります。
line ラインディシプリン TTYデバイスにアタッチされたラインディシプリン。0はN_TTYです。全ての有効な数のリストが/proc/tty/ldiscsにあります。このリストにない数はN_TTYへのエイリアスになるようですが、そのことに依存してはいけません。

これを試してみてください:xtermを起動します。そのTTYデバイス(ttyが表示するもの)とサイズ(stty -aが表示するもの)をメモしておきます。xtermの中でvim(または他のフルスクリーンの端末アプリケーション)を起動します。エディタはウィンドウ全体に描画するために現在の端末サイズをTTYデバイスに問い合わせます。ここで、別のシェルウィンドウから次を入力します:

stty -F X rows Y

ここでXはTTYデバイス、Yは端末の高さの半分にします。これによってカーネルのメモリ内のTTYデータ構造が更新され、SIGWINCHがエディタに送られます。するとエディタはウィンドウの上半分だけを使って再描画をします。

stty -aの出力の2行目は全ての特殊文字を表示しています。新しいxtermを起動してこれを試してみてください:

stty intr o

すると^Cの代わりに"o"でフォアグラウンドジョブにSIGINTが送られるようになります。catか何かを起動して、^Cを押してもキルできないことを確認してください。それから"hello"と入力してみてください。

たまに、バックスペースキーが効かないUNIXシステムに出くわすことがあるでしょう。これは、端末エミュレータが送信するバックスペースのコード(ASCIIの8か127)とTTYデバイスの消去の設定が一致しないときに起こります。この問題を直すには普通、stty erase ^H(ASCII 8の場合)かstty erase ^?(ASCII 127の場合)を実行します。しかし、多くの端末アプリケーションはreadlineを使い、readlineはラインディシプリンをrawモードに設定することを覚えておいてください。そういったアプリケーションは影響を受けません。

最後に、stty -aはたくさんのスイッチを表示しています。案の定、特別な順序なしにリストされています。これらのうちの一部はUART関連、一部はラインディシプリンの挙動に影響、一部はフロー制御、一部はジョブ制御です。ダッシュ(-)はそのスイッチがオフであることを示しています。ダッシュがついていなければオンです。スイッチは全てstty(1)のmanページで説明されているので、ここでは簡単にいくつかを説明します:

icanonはcanonical(行指向)モードをトグルします。新しいxtermでこれを試してみてください:

stty -icanon; cat

バックスペースや^Uなどの行編集文字が全て動作しなくなったことに気づいたでしょうか。catが一度に1行でなく1文字ずつ受け取り、従って1文字ずつ出力していることにも注目してください。

echoは文字のエコーを有効にし、これはデフォルトでオンになっています。stty icanonでcanonicalモードを再度有効にして、これを試してください:

stty -echo; cat

1文字タイプするごとに端末エミュレータはカーネルに情報を送信します。通常、カーネルは同じ情報を端末エミュレータにエコーバックするので、ユーザーは自分がタイプしたものを見ることができます。文字エコーを無効にすると、自分がタイプした文字が見えなくなります。しかし今はcookedモードにいるため、行編集機能は有効のままです。エンターを押すとラインディシプリンは編集バッファをcatに送り、自分が書いたものが表示されます。

tostpはバックグラウンドジョブが端末に書き込むのを許可するかどうかを制御します。これを試してください:

stty tostop; (sleep 5; echo hello, world) &

&をつけるとコマンドはバックグラウンドジョブとして実行されます。5秒後にジョブはTTYに書き込もうとします。するとTTYドライバがSIGTTOUを使ってジョブをサスペンドし、シェルが多分その事を通知します(即座にか、あるいは新しいプロンプトを表示するときかもしれません)。次にこのバックグラウンドジョブをキルし、次のコマンドを試してください:

stty -tostop; (sleep 5; echo hello, world) &

プロンプトが表示され、5秒後にバックグラウンドジョブがhello, worldを端末に送信するでしょう。あなたが何かをタイプしている最中であっても。

最後に、stty saneを実行するとTTYデバイスの設定がまともな状態に戻ります。

結論

この記事が、TTYドライバとラインディシプリンの用語、またそれらが端末、行編集、ジョブ制御とどう関係しているかの取っ掛かりになれば幸いです。より詳しい情報は、先述の様々なmanページとglibcのマニュアル(info libc, "Job Control")にあります。

最後に、質問全部に答える時間はありませんが、この記事やサイト上の他のページに関するフィードバックを歓迎します。読んでくれてありがとう!

Posted Friday 25-Jul-2008 19:46

179
147
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
179
147