GNU/Linux OS のブート時に、init(8) を経由せずにシェル (/bin/sh) を実行すると、 このシェル上ではジョブ制御 (job control) が行なえない。 つまりこのシェル環境は制御端末 (controlling tty) に成れない。 これがどんなに不便かというと、 自動的に止まらないプログラム (例えばオプション無しで ping を実行したときなど) を止める方法が無いわけで、 いったんそういうプログラムを動かしてしまったら最後、 CTRL-ALT-DEL で reboot させる他なくなってしまう。
そもそも、なぜ init(8) を起動する前に /bin/sh を実行したいかというと、 ミニルート (initramfs) 上で 作業を行ないたいから。 initramfs の init (これは init(8) ではなくシェル・スクリプト) の中で、 BusyBox の /bin/sh (/bin/ash) を exec する (つまり PID=1) ことによって、 initramfs 上での作業を可能にする。
init(8) は、 GNU/Linux OS の全てのプロセスの親プロセスだが、 その万物の親すら生まれていない創世記以前に作業を行なえるメリットは数多い。 例えば、ルート・ファイル・システム (root file system) すらマウントしていない段階なので、 マウント後 (つまり init(8) 起動後) には実行不可能な操作 (xfs_repair などのファイルシステム修復操作とか) を行なうことができる。 しかもこのシェルはプロセスID が 1番なので、 このシェル環境上で root file system を「/」にマウントし、 続いて init(8) を exec すれば、 そのまま GNU/Linux OS を起動することができる。
root file system のメンテナンス等は、 別の起動ディスク (CD-ROM や USB メモリ等) からブートして行なうのが一般的だが、 CD-ROM ドライブや USB メモリを準備したり、 あるいは CD-ROM や USB メモリの抜き差しが必要になったりと、 なにかと面倒である。 メンテナンス用の起動パーティションを root file system とは別に用意する、 という方法も考えられるが、 メンテナンス専用のパーティションを維持管理するのが面倒くさい (普段使わないものほど陳腐化して、いざというとき役に立たない)。
initramfs だとハードディスクすら不要 (例えば PXE ブート) でメンテナンスが可能になるし、 普段 GNU/Linux OS 起動用として使ってる initramfs が、 そのまま非常用のメンテナンス環境になるため、 陳腐化する心配がない。
実は「突然死したハードディスクを復旧させる『お手軽パック』」は、 initramfs そのものだったりする。 しかも「復旧」用として作った initramfs というわけではなく、 私が普段 GNU/Linux OS を ブートするときに使っている initramfs と 全く同じものである (だからこそ ハードディスクの突然死問題 が勃発した直後にリリースできた)。
というわけで、 いいことづくめの initramfs シェル環境 (initramfs shell environment) なのだが、 「復旧お手軽パック」実行例 にもあるように、 init(8) 以前の段階で /bin/sh を実行すると
/bin/sh: can't access tty; job control turned off #
と表示してジョブ制御がオフになってしまう。 つまりこのシェル環境では、 プログラムを実行中に control-C (^C) を押しても止める (正確にいうと SIGINT シグナルを送る) ことができない。
ジョブ制御 (^C などでシグナルを送ること) ができない状態に陥って 初めて沸き起こる制御端末 (controlling tty) に対する感謝の念なのであるが、 initramfs が役目を終えて init(8) が起動して (GNU/Linux OS がブートして) しまうと、 喉元過ぎればなんとやらで 「can't access tty」をなんとかしようという意欲は雲散霧消し、 そのままになっていた。
制御端末になれない initramfs シェル環境に対して何十回目かの悪態をついた後、 ようやく対策を立てるべく原因を調べてみることにした。
Linuxカーネル(ドライバ)のソースを読んでみたところ、 以下の端末デバイスは制御端末に成れないのです。 興味がある人は、ソース、 drivers/char/tty_io.cのtty_open()を見てみてください。
* /dev/console -- カーネルの起動時の端末。
* /dev/tty0 -- tty1~の「Linux Virtual Terminal」のうち、現在表示している物を示す。
* /dev/tty -- 現在使っている端末を示す。
* PTYのマスター側
Linux:制御端末 から引用
initramfs シェル環境で使っている端末は /dev/console だから制御端末になれない。 だから BusyBox には /dev/console という仮想的な端末ではなく、 本物のデバイスを探すための cttyhack というプログラムが付属している。 /bin/sh を実行する代わりに cttyhack /bin/sh を実行すれば ジョブ制御ができると BusyBox のマニュアルには書いてある。
...という解説は上に引用したページをはじめ、 WWW 上のあちらこちらのページで見かけるし、 私としても当然そんなことは先刻承知で、
/bin/sh: can't access tty; job control turned off # tty /dev/console # cttyhack sh sh: can't access tty; job control turned off # tty /dev/tty1 #
などと、確かに cttyhack の働きにより /dev/console ではなく /dev/tty1 を使うようになったものの、 相変わらず「can't access tty」エラーが出ているので困っているわけである。 cttyhack を使っているのにジョブ制御できないわけで、 cttyhack-- と思っていた。
前置きが長くなったが、ここからが本題である。
Last night, I finally succeeded in vanquishing the dread "can't access tty; job control turned off" message. As it turns out, there are several discrete steps involved in the creation of a controlling terminal, and they're all seemingly mandatory:
「昨晩ついに、 恐怖の "can't access tty; job control turned off" メッセージを抑えつけた!」 という書き出しに、 いやがおうにも期待が高まる。 この、TTY を制御端末にするための必須条件とやらは、 今まで日本語での記述を見たことがないので、 訳してみる:
- session owner かつ process group leader でなければならない。 つまり前もって setsid(2) を呼び出しておく必要がある (もし既に別の制御端末を持っていたら、先に捨てる)。
- /dev/console などカーネルが提供する仮想的な端末ではなく、 /dev/tty1 など「本物の」TTY デバイスでなければならない。 /dev/console が制御端末になれないのは、 Linux カーネル ML での議論 によれば、init を CTRL-ALT-DEL に応答させる方法に関する歴史的理由による。 もし /dev/console が制御端末だったら、 CTRL-C が効いてブートスクリプトを止めることが可能になってしまう。
- CTRL-C, CTRL-Z その他ものもろのキーが入力できる TTY でなければならない。
- TIOCSCTTY ioctl を行なうことによって、 標準入力の TTY を制御端末にすることができる。
つまり cttyhack が解決できるのは上記条件のうち 2番目の条件だけで、 1番目の条件は満たしていない。 そこで、cttyhack を呼ぶ前に setsid してみる:
# setsid cttyhack sh # ping 127.0.0.1 PING 127.0.0.1 (127.0.0.1): 56 data bytes 64 bytes from 127.0.0.1: seq=0 ttl=64 time=0.077 ms ^C --- 127.0.0.1 ping statistics --- 1 packets transmitted, 1 packets received, 0% packet loss round-trip min/avg/max = 0.077/0.077/0.077 ms #
お~ ついにあの忌まわしい「can't access tty; job control turned off」 エラーが表示されなくなり、 ^C でプログラムの実行を止められるようになった。
結局、鍵は setsid にあったわけで、 「setsid cttyhack」をキーに検索してみると、 同様の話が他にも見つかった:
setsid cttyhack ash
This makes the ash error about job control go away,
and "echo Hi > /dev/tty" now works.setsid cttyhack ash から引用
じゃ、なんで cttyhack は自前で setsid しないかというと、 cttyhack は init(8) から呼び出されることを前提としているからのようだ:
On Fri, Dec 07, 2007 at 01:17:57PM -0800, Lombard, David N wrote:
> Hmmm. cttyhack doesn't do:
>
> setsid()
> ioctl(0,TIOCSCTTY,1)
Did you set CONFIG_FEATURE_INIT_SCTTY? There's conditionally compiled code in init to do the above, see init/init.c
init(8) 起動前だからこそ、 initramfs シェル環境は有用であると思うのだが、 そのような initramfs の使い方は、 まだ一般的にはなっていないのだろう。