はじめに

シリアルコンソールの有用性

リモートでマシンを管理する際にシリアルコンソールがあると心強いです。 特にネットワーク関連のものをいじるとき −パケットフィルタリングの設定、IPアドレスをつけ変える、NICのドライバを入れ替える、NICが壊れたなど− にはシリアルコンソールがないと一発勝負もしくは不可能なことが多いですし、 他にもkernel入れ替えたら起動しなくなった、rebootしたらfsckで引っ掛かってしまったなど、ネットワークが有効になる前のトラブルにもシリアルコンソールならば対応できます。

また、 アクセスパスがネットワーク経由(SSH)ともう一つ確保できるというのは片方に障害が発生したときの代替手段にもなり冗長性が増しますし、 リモートではなくすぐそばにマシンがある場合でも、モニタとキーボードを繋げる必要がないのでシリアルコンソールは便利です。

シリアルコンソールを使いたいマシンが沢山ある場合、 専用の箱もののコンソールサーバーを導入するとか、 PCにマルチシリアルカード (cycladesの製品が有名ですが、 玄人志向のSERIAL4P-LPPCIは4ポートですが安価なので購入しやすいと思います) を挿してconserverなどで管理するのがよく行われると方法だと思いますが、 もっと単純に、2台ペアずつシリアルクロスケーブルで繋ぐだけでも安心感は大分高まるのでお奨めです。

シリアルコンソールでできること

通常状態 (initが上がってgettyがシリアルデバイスをつかんでいる状態) でログインできる他、以下のようなこともできます。

  • GRUB(ブートローダ)の操作。 つまり、起動するkernelを選択したりシングルユーザーモードに入ることができる。
  • 最近のLinux kernelの場合、ブートメッセージをシリアルデバイスに出力できる。
  • ファイル転送。(zmodemなど)

最初の二つについては、 Remote Serial Console HOWTO ( JFの日本語訳 ) に詳しく書かれているのでそちらを参照してください。
ファイル転送は、「シリアル」「zmodem」「sz」「rz」などをキーワードに検索すればその方法が見つかると思います。

他に、PCでもハードウエアによってはBIOSの操作もシリアル経由で行えるものもあります。(DELLのサーバークラスの製品のConsole Redirect機能とか)

目的

世の中にはUSBシリアル変換器というものがあります。
大抵のPCではシリアルポートは背面についていてPCの配置によっては手が届きづらいところにある一方で、USBポートは全面についていることが多いです。しかもUSBは抜き差しOKです。

このメモは、「背面のシリアルポートに挿すのが面倒」という理由だけで、 USBポートにUSBシリアル変換器を挿してシリアルコンソールの受け口にした際のメモです。

方針は以下の通りです。

  • GRUBの操作とブートメッセージのリダイレクトは、 USBポートではなくシリアルポートを使う。 特にGRUBの操作は局面によってはクリティカルな操作になることがあるので、より枯れている(信頼性の高い)シリアルで行いたいというのがUSBを使わない理由。
  • USBポートでは通常時のログインのみできればいい。
    • 抜挿のタイミングでinittabを書き換えて、 gettyがUSBポートで待ち受けるのを制御したい。

動作環境

動作環境は以下の通りです。

  • Linux, kernel 2.6.6-rc3 (2.6.5は問題あり)
  • module-init-tools 3.0-pre10
  • hotplug 20040329
  • USBシリアル変換器: サン電子のVS-60R
    因みに、VS-60RはMac OS Xで追加ドライバなしで使える優れモノです。

シリアルコンソールへ繋ぐマシンでは jerm を使いました。機能がシンプルで、*BSD、Linux、Mac OS XなどいろいろなOSで使えます。

USBポートへログイン

USBシリアル変換器のドライバ

LinuxのVS-60Rのドライバはpl2303などdrivers/usb/serial/にあるものではなく、 drivers/usb/serial/cdc-acmです。
cdc-acmも含め、USB関連のドライバは全てmoduleにしました。 (組み込みにするとブートメッセージをUSBポートに出力できるようになりますが、今回は必要なかったのでモジュールにしました)

Device Drivers  --->
  USB support  --->
<M> Support for Host-side USB
(snip)
<M>   EHCI HCD (USB 2.0) support
<M>   OHCI HCD support
(snip)
<M>   USB Modem (CDC ACM) support

kernelをコンパイルして再起動後、 USBシリアル変換器を挿すと自動的にcdc-acmがロードされて変換器が認識されました。ちょっと拍子抜け。 (/lib/modules/modprobe.confalias char-major-166 cdc_acmと書いてあるからかな?)
cdc-acmのデバイスは/dev/ttyUSB0じゃなくて/dev/ACM0であることに注意です。

inittabの書き換え

変換器が認識されたらログインできるようにinittabを書き換えます。
ここらへんはデバイス名が違うぐらいでシリアルもUSBも同じなので、 不馴れならば Remote Serial Console HOWTO ( JFの日本語訳 ) を読んでおくことをお奨めします。

/etc/inittabに次の通り追加します。

T2:23:respawn:/sbin/getty -L ttyACM0 9600 vt100

追加したらtelinitを使ってinittabを再読み込みします。

# /sbin/telinit q

psでttyACM0で待ち受けるgettyが確認できればOKです。

$ ps axww|grep getty
 4280 tty1     Ss+    0:00 /sbin/getty 38400 tty1
 4290 tty2     Ss+    0:00 /sbin/getty 38400 tty2
 4300 tty3     Ss+    0:00 /sbin/getty 38400 tty3
 4310 ttyS0    Ss+    0:00 /sbin/getty -L ttyS0 9600 vt100
 4899 ttyACM0  Ss+    0:00 /sbin/getty -L ttyACM0 9600 vt100

早速、繋いでみる

受け側の設定ができたので早速繋いでみます。

接続元のマシンのシリアルポートにクロスのシリアルケーブルを繋げ、 その先にUSBシリアル変換器、受け側のマシンを繋げます。

     接続元                                              受け側
┏━━━━━━┓   (RS-232C)              (USB)     ┏━━━━━━┓
┃     [D-sub25]--------------<変換器>==============[USB]         ┃
┗━━━━━━┛                                    ┗━━━━━━┛
             jerm ───────────────→ getty
          minicom
           kermit

接続元のマシンで、/dev/ttyS0への読み書き権限があるのを確認して、 jerm で繋ぎます。

$ jerm /dev/ttyS0
Jerminal v0.8091  Copyright (C) 2000, 2001, 2002, 2003, 2004 candy
Type "Ctrl-M ~ ." to exit.
 ispeed 13 ospeed 13
 -IGNBRK -BRKINT -IGNPAR -PARMRK -INPCK -ISTRIP -INLCR -IGNCR -ICRNL -IXON -IXOFF -IXANY -IMAXBEL
 -OPOST +ONLCR -TABDLY
 cs8 -CSTOPB +CREAD -PARENB -PARODD +HUPCL +CLOCAL -CRTSCTS +ECHOKE +ECHOE -ECHO -ECHONL -ECHOPRT +ECHOCTL -ISIG -ICANON -IEXTEN
 -TOSTOP -FLUSHO -PENDIN -NOFLSH

で、リターンキーを押すとログインプロンプトがでるはずなんですが…

host login: inux 3.0 host ttyACM0

ん? なんか変です。本来ならば、

Debian GNU/Linux 3.0 host ttyACM0

host login:

と表示されるはずなんですが…
どうも行末でCRLFじゃなくてCRしか送られてこないのが原因で、 カーソルが行頭に戻るだけで改行されていないようです。

この行末でCRしか送られてこないのはUSBポートを受け側にしたときのみ発生しました。 接続元と接続先マシンを逆にして、USBポートからシリアルポートへ接続するとこのような問題はありませんでした。
また、他のドライバ(pl2303)を使うUSBシリアル変換器 (USB-CVRS9) でも同様の現象でした。

jermの他に、 minicomC-KermitTaylor UUCPのcu も試してみましたがどれも同じでした。
接続元、接続先の両方でsttyでいろいろ(opostとか[oi]nlcrとか)設定したみましたがどれも効果なしでした。

ネットで検索してみると、linux-usb-devel MLで既に度々問題になっていたようなのですが、どうも直すのは難しくて放置してあるようでした。

仕方がないので接続に使うツールの側で対応することにしました。

まず、 C-KermitにはCRをCRLFに変換する機能 (set terminal cr-display crlf) があるのでそれを使えばOKでした。

$ kermit -l /dev/ttyS0

set terminal cr-display crlf
set carrier-watch off
connect

次に、 jerm にはそのような機能はないのですが、クイックハックで機能追加してみました。
実行時に-rオプションを指定するか、 実行中に~rで行末(CRとLF)をCRLFに変換するようになります。

参考までにjerm-v0.8092に対するパッチを置いておきますが、 次期バージョンではこの機能は取り込まれるかもしれません。

jerm-v0.8093から行末文字の変換機能がつきました。-rオプションで制御することができます。
今回のように受け取ったCRをCRLFに変換するには、

jerm -r tnrn /dev/ttyS0

とすればOKです。

抜挿でinittabの書き換えなど

必要ないときは抜いておきたいんだけど…

これでUSBポートのシリアルコンソールへ接続できるようになったのですが、 いつもはUSBシリアル変換器は抜いておいて、 必要なときだけ変換器を挿して使いたいです。
しかし、USBシリアル変換器を抜いた状態ではgettyがデバイス(/dev/ttyACM0)にアクセスできないのでsyslogに延々とエラーメッセージが出力されてしまいます。
気にしなければいいのかもしれませんが、気持が悪いのでちと考えてみます。

モジュールのロード/アンロードのタイミングで

エラーメッセージが出ないようにするには、inittabを書き換えて再読み込み (telinit q) すればよいわけです。 つまり、 USBシリアル変換器が挿されたときにinittabに/dev/ttyACM0をgettyする行を追加して、 抜かれたら追加したものを削除すればよいです。

そのためには抜挿のイベントをフックしてスクリプトを実行すればいいはずです。 そこでイベントを取得する方法を考えてみました。

まず思いついたのはUSBドライバのモジュールがロードされたときとアンロードされたイベントです。
kernel 2.4時代のmodutilsでは/etc/modules.confpost-installpre-removeといった設定ができました。 しかし新しいmodule-init-toolsではこれらの設定項目はなくなってしまったので、 同じことを実現するのはちょっと面倒になりました。
例えば、モジュールがロード/アンロードされたときに/etc/inittabのシンボリックリンクを張り替えてtelinit qするには次のようにmodprobe.confに書きます。

install cdc_acm { ln -sf inittab-acm /etc/inittab && /sbin/telinit q; }; /sbin/modprobe --first-time --ignore-install cdc_acm
remove  cdc_acm /sbin/modprobe -r --first-time --ignore-remove cdc_acm && { ln -sf inittab-normal /etc/inittab && /sbin/telinit q; }

さて、モジュールのロードは自動でなされるのでいいのですが、 問題はモジュールのアンロードです。
USBシリアル変換器を抜いた後でも、モジュールはロードされっぱなしです。 modutilsのrmmodでは-aオプションをつければ使っていないモジュールを見つけて削除してくれるのでcronで定期的に実行すればよかったのですが、 module-init-toolsではrmmodからは-aオプションがなくなりモジュール名の指定が必須になりました。 また、(rmmodも動作しますが)rmmodではなくmodprobe -rを使うようになりました。
従ってmodprobe -r cdc-acmを定期的に実行すればいいのですが、 その殆んどが無駄な実行となりますし、 変換器を抜いてからしばらくはアンロードされない(エラーメッセージが出続ける)のでちといまいちな方法です。

デバイスの抜挿のタイミングで

どうしたもんかなぁといろいろ調べていると、 hotpluggingというものを見つけました。 全く知らなかったのですが、kernel 2.4からの機能で、USBに関してはkernel 2.2にもバックポートされているようです。
この機能を使えばドライバモジュールのロード/アンロードのタイミングではなく、より確実なデバイスの抜き挿しのタイミングで設定を切り替えることができます。

hotpluggingのサイトのドキュメントや、/etc/hotplug/*/etc/hotplug.d/*にあるスクリプトを読んでみて、関係のある個所のみ抜き出すと以下のような動作になるようです。

デバイスが挿された
  1. /proc/sys/kernel/hotplug で指定されているプログラム、 /sbin/hotplug が実行され、/sbin/hotplugが…
    1. /etc/hotplug.d/usb/*.hotplugを実行する。
      しかしここにはファイルがないので何も起こらない。
    2. /etc/hotplug.d/default/*.hotplugを実行する。
      /etc/hotplug.d/default/default.hotplugが実行され、 default.hotplugが…
      1. /etc/hotplug/usb.agentを実行する。 /etc/hotplug/usb.agentが…
        1. /lib/modules/`uname -r`/modules.usbmap を元に合致するデバイスを探すが見つからない。
        2. /etc/hotplug/usb.usermap/etc/hotplug/usb/*.usermap、 を元に合致するデバイスを探すが見つからない。
          もし見つかった場合は
          /etc/hotplug/hotplug.functionsで定義されている load_driversが実行され…
          1. /etc/hotplug/usb.agentで定義されている usb_map_modulesが実行され、ドライバを探す。 (見つからない場合は直ぐにreturnする)
          2. ドライバがロードされていなければロードする。
          3. /etc/hotplug/usb/MODULE という実行ファイルがある場合は実行する。
デバイスが抜かれた
      1. (ここまでは挿された場合と同じ)
        /etc/hotplug/usb.agentを実行する。 /etc/hotplug/usb.agentが…
        1. 変数REMOVERに格納されている場所に実行ファイルがある場合は実行する。

従って、以下のことをすれば抜挿のタイミングで任意のプログラムを実行できることがわかりました。

  1. /etc/hotplug/usb.usermap (もしくは/etc/hotplug/usb/*.usermap) にUSBシリアル変換器の定義を追加する。
  2. /etc/hotplug/usb/cdc-acmを用意する。 これは挿したときに実行されるファイルとなる。
  3. 変数REMOVERの場所に実行ファイルを用意する。 これは抜いたときに実行されるファイルとなる。

USB hotplugの設定

まずは/etc/hotplug/usb.usermapです。
ドキュメントや他のマップファイル/lib/modules/`uname -r`/modules.usbmapを見てみると、この形式はモジュール名で始まる全13カラムで1行1レコードのようです。
そしてこのマップファイルを扱うusb_map_modulesを読んでみると、 必ずしも全てのカラムを記述する必要はなく、第2カラムのmatch_flagsでどのカラムまでマッチすれば見つかったことにするか制御できるようです。 今回はベンダーIDとプロダクトIDの二つで判定するようにしました。

ベンダーIDとプロダクトIDはlsusbや/proc/bus/usb/devicesで確認することができます。

# lsusb
(snip)
Bus 003 Device 006: ID 05db:0012
Device Descriptor:
  bLength                18
  bDescriptorType         1
  bcdUSB               1.10
  bDeviceClass            2 Communications
  bDeviceSubClass         0
  bDeviceProtocol         0
  bMaxPacketSize0         8
  idVendor           0x05db
  idProduct          0x0012
  bcdDevice            1.00
  iManufacturer          17 Sun Corporation SCC div.
  iProduct               18 SUNTAC USB Device
  iSerial                19 USB-COM
(snip)

# less /proc/bus/usb/devices
(snip)
T:  Bus=03 Lev=01 Prnt=01 Port=01 Cnt=01 Dev#=  6 Spd=12  MxCh= 0
D:  Ver= 1.10 Cls=02(comm.) Sub=00 Prot=00 MxPS= 8 #Cfgs=  1
P:  Vendor=05db ProdID=0012 Rev= 1.00
S:  Manufacturer=Sun Corporation SCC div.
S:  Product=SUNTAC USB Device
S:  SerialNumber=USB-COM
(snip)

そして3番目のカラム(プロダクトID)までを判定に使うように match_flagsを3に指定して、次のような行を /etc/hotplug/usb.usermap に追記します。使わないカラムは0で埋めておきました。

# module match_flags idVendor idProduct bcdDevice_lo bcdDevice_hi bDeviceClass bDeviceSubClass bDeviceProtocol bInterfaceClass bInterfaceSubClass bInterfaceProtocol driver_info
cdc-acm 0x0003 0x05db 0x0012 0x0000 0x0000 0x00 0x00 0x00 0x00 0x00 0x00 0x00000000

次に/etc/hotplug/usb/cdc-acmです。
これはshellスクリプトで、以下の機能を実装します。

  • 挿されたとき以外は何もせずに終了する。 本来は挿されたとき以外は実行されないはずですが念のために。 挿されたときは変数ACTIONaddになるのでそれを判定に使う。
  • 期待したデバイスでなければ何もせずに終了する。 これは変数PRODUCTを見れば判定できる。 PRODUCTの形式は 「idVendor/idProduct/bsdDevice」 となっている。
  • 念のためモジュールをロードする。
  • /etc/inittabを書き換えて再読み込みする。
  • 抜いたときに実行されるRMOVERスクリプトを生成する。

注意が必要なのはRMOVERスクリプトで、 デバイスが抜かれたときにusb.agentがREMOVERスクリプトを実行してくれるのですが、実行した後に削除してしまうので、 挿されたタイミングで毎回生成する必要があります。 REMOVERスクリプトのパスは複雑なのですが、 /etc/hotplug/usb/cdc-acmが実行される時点でパスが変数REMOVERに格納されているので、そこへスクリプトの内容をリダイレクトすればよいです。
以下にcdc-acmを示します。

#! /bin/sh
#
# $Id: usb-console.html,v 1.4 2005-11-20 08:29:46 hirose31 Exp $
#

cd /etc/hotplug
. ./hotplug.functions
# DEBUG=yes export DEBUG

if [ "X${ACTION}" != "Xadd" ]; then
    debug_mesg "ABORT: invalid action ${ACTION}"
    exit 1
fi

case $PRODUCT in
    5db/12/*)
        if $MODPROBE cdc-acm >/dev/null 2>&1; then
            debug_mesg "succeed in ${MODPROBE} cdc-acm"
            cp -f /etc/inittab /etc/inittab.$$

            modlabel='cdc-acm'
            if ! grep -q "^#>>${modlabel}" /etc/inittab.$$; then
                cat <<EOFINITTAB >> /etc/inittab
#>>${modlabel}
T2:23:respawn:/sbin/getty -L ttyACM0 9600 vt100
#<<${modlabel}
EOFINITTAB
            fi

            /sbin/telinit q

            mv -f /etc/inittab.$$ /etc/inittab.orig

            debug_mesg "cdc-acm: generate ${REMOVER}"
            cat <<EORMOVER >${REMOVER}
#! /bin/sh
cp -f /etc/inittab /etc/inittab.\$\$

cat /etc/inittab.\$\$ | awk '
/^#(>>|<<)${modlabel}/ { skip = ! skip ; next }
skip == 0 { print }
' >/etc/inittab

/sbin/telinit q

mv -f /etc/inittab.\$\$ /etc/inittab.orig

exit 0
EORMOVER
            chmod +x ${REMOVER}
        else
            mesg "ABORT: failed to ${MODPROBE} cdc-acm"
            exit 1
        fi
        ;;
    *)
        mesg "ABORT: unknown product: ${PRODUCT}"
        exit 1
        ;;
esac

exit 0

問題点

これでデバイスを挿したらUSBポートのシリアルコンソールを準備し、 抜いたら元に戻すようになったのですが、2つ問題が発生しました。

抜挿するとkernel oopsが出る

最初にUSBシリアル変換器を挿して抜いたときは首尾よく機能するのですが、 再度、変換器を挿すとkernel oopsが出て以降ずっとデバイスにアクセスできなくなってしまいました。

ネットで調べたところ、どうもこれはkernel 2.6.5のバグで、2.6.6-rc1あたりで修正された問題のようでした。 そこでkernel 2.6.6-rc3に入れ替えたところ、この問題は解決しました。

2004-05-19追記
kernel oopsが出る問題はkernel 2.6.6で修正されていることを確認しました。

cdc-acmが2つ実行される

USBシリアル変換器を挿すとhotplugが2つ実行されてしまい、 結局、cdc-acmも2つ実行され、REMOVERスクリプトも2つできてしまいます。
どうもインターフェースが複数ある場合はその数だけ実行されてしまうようです。

なので/etc/hotplug/usb/cdc-acmやREMOVERスクリプトでの処理は競合しても大丈夫なように書く必要がありますし、場合によっては排他制御が必要になるかもしれません。
先の例でも、このせいでinittab.origが処理前のオリジナルにならないことがありますが、inittabの書き換え自体には問題ないように実装しているのでまぁいいかと放置してあります。

変更履歴

2004-08-26
(遅ればせながら)jerm-v0.8093に行末文字の変換機能が実装されたことを追記した。それにともないv0.8092用のパッチを引っ込めた。
2004-05-19
kernel 2.6.6では問題ないことを、抜挿するとkernel oopsが出るに追記。
2004-05-09
公開。

目次

関連リンク