2014年4月24日

ディスクイメージファイルをマウントする

Raspberry Pi(RPi)をQEMUエミュレータで走らせている。

QEMUには専用カーネルファイルとディスクイメージファイルを与えるのだが、ディスクイメージファイルは自前のRPiの実機のSDカードイメージその物。

ここでちょっと困ったことに気づいた。QEMU用カーネルと実機のカーネルのバージョンが違うのだ。前者は3.10.26+で後者は3.10.24+。自前のディスクイメージファイルには3.10.26+のカーネルモジュールがないので、カーネルモジュールがロードできない。

RPiの公式ダウンロードページに各種ディストロのディスクイメージファイルが掲載されているでの、このディスクイメージファイルに3.10.26+のカーネルモジュールが入っていればそれをQEMUのターゲットファイルシステムにコピーすればよいと考え、我が家のシステムのベースであるRaspbian(Debian Wheezy)をダウンロードする。このディスクイメージは伸長時約2.8GBが約780MBのZIPファイルに収まっている。

さてダウンロードに成功したものの、これはディスクイメージファイルなのでそのままではマウントできない。「loopデバイスを使えば」と言うかもしれないが、「マウント」とはファイルシステムをマウントすることであり、イメージファイルをloopデバイスでブロックデバイスに見せかけても、このディスクイメージファイルは複数パーティションに分かれており、その中のパーティション(=ファイルシステム、この場合)にアクセスできなければマウントできない。一旦SDカード(でも何でも、とにかく物理ブロックデバイスメディア)に書き出して物理的にマウントなどはやりたくない。

例によってぐぐったら見つかった。 ディスクイメージファイルから目的のパーティションの位置を探し、その情報をmountoffsetオプションに与えればよい。パーティション情報を見るにはpartedを使う(MBRパーティションの場合)。fdiskはバイト単位の表示ができない。

h@spice:~/projects/rpi$ parted 2014-01-07-wheezy-raspbian.img
WARNING: You are not superuser.  Watch out for permissions.
GNU Parted 2.1
Using /home/h/projects/rpi/2014-01-07-wheezy-raspbian.img
Welcome to GNU Parted! Type 'help' to view a list of commands.
(parted) unit B                                                         
(parted) print                                                          
Model:  (file)
Disk /home/h/projects/rpi/2014-01-07-wheezy-raspbian.img: 2962227200B
Sector size (logical/physical): 512B/512B
Partition Table: msdos

Number  Start      End          Size         Type     File system  Flags
 1      4194304B   62914559B    58720256B    primary  fat16        lba
 2      62914560B  2962227199B  2899312640B  primary  ext4

(parted) q

h@spice:~/projects/rpi$ sudo mount -o loop,offset=62914560,ro,noload 2014-01-07-wheezy-raspbian.img w/

noloadオプションは、マウント時にEXT4のジャーナル情報を無視するためのおまじない。これがないとマウントエラーになりdmesgが「EXT4-fs (loop0): INFO: recovery required on readonly filesystem」と文句を言うことがある(本稿はこちらを参考にした)。なお、当然のことながら、QEMUで使用中のディスクイメージを書き込みモードでマウントするとキャッシュコヒーレンシが崩れてディスクイメージに損傷を与える可能性があるので避けるべし。

実際にダウンロードしたディスクイメージファイルは3.10.25+カーネル用のもので、目的とする3.10.26+は含まれていなかった。目的は達成できなかったが、今のところQEMUで走っているシステムが実際にカーネルモジュールをロードしようとしてはいなさそうなので、これでよしとする。

loopデバイス作成コマンドのlosetupにもoffsetオプションがあるので、これを使えば、例えばパーティションに区切られたディスクイメージファイルからRAIDを再現してマウントなどができそうなことが分かった。


2014年4月23日

Raspberry PiをQEMUエミュレータ上で走らせる(2) 立ち上げ、X Window、ネットワーク

前回でQUEMが動き、エミュレータ上でカーネルとシェルが動き、基本的な動作が確認できた。次は、これを開発に必要な実用レベルに持って行く。

QEMU版への移行

まず、RPiには標準ではバッテリバックアップ付きのリアルタイムクロックはなく、この段階ではNTPDはおろかネットワークも動いていないので、時計が例の1970年元旦になっている。このままでも致命的ではないが、以下に述べるファイル修正の履歴(タイムスタンプ)が後日奇妙に見えるので、dateコマンドでそれらしい時刻を設定する。

root@rpi:/# date 04221250

次に、ホスト名がディスクイメージのコピー元と同じだと紛らわしいのでエディタで変えておく。エディタはディスクイメージにインストールされているものなら何でも使えるが、viが多少不安定な挙動を示したので、最悪nanoで。

root@rpi:/# cat /etc/hostname
rpi-qemu

余計なライブラリのプリロードを禁止。

root@rpi:/# cat /etc/ld.so.preload
#/usr/lib/arm-linux-gnuebihf/libcofi_rpi.so

最後に、必ずしも必要ではないが、実機とディスクデバイスの名前を揃えるためのudevルールファイルを作る。

root@rpi:/# cat /etc/udev/rules.d/90-qemu.rules
KERNEL=="sda", SYMLINK+="mmcblk0"
KERNEL=="sda?", SYMLINK+="mmcblk0p%n"
KERNEL=="sda2", SYMLINK+="root"

で、一度システムをシャットダウン。

root@rpi:/# halt

ホスト側のQEMUを^Cで殺し、もう一度本チャン起動してシステムをフル稼働にする。

h@spice:~/projects/rip$ qemu-system-arm -kernel kernel-qemu -cpu arm1176 -m 256 -M versatilepb -no-reboot -serial stdio -append "root=/dev/sda2 panic=1 rootfstype=ext4 rw" -hda rpi.img -redir tcp:2222::22

パラメタについていくつか…
-m 256 システムメモリは256MBがハードコードされているのでこれより大きい値は指定するな、と言われている。
-M versatilepb QEMU上の仮想プラットフォーム。実機ではBCM2708。
-hda rpi.img 最初の立ち上げに使ったのと同じ自前のディスクイメージファイル。
-redir tcp:2222:22 ホストのTCPポート2222をQEMUターゲットのポート22にリディレクトする。SSH用(後述)。

Xウィンドウ


これはあっけなく動いた。実機からコピーしたディスクイメージを使っているので、実機とほぼ同じ。ただし、QEMUの問題と思われる以下の問題が発見された。
・マウスポインタがホストから見える「物理ポインタ」(四角い小さなドット)とターゲット(RPi)に見える論理ポインタ(矢印、Iビームなど)が乖離する。論理ポインタがQEMUのウィンドウの外には出られないクリッピングを起こすので両者を近づけることはできるが、そのうちまた離れてしまうので煩わしい。VNCクライアントなどでもこの兆候はあるが、あちらは自分でできるだけ一致させるように働くのに、こちらはその努力が見えない。
・ロカール(locale)が正しくないのか、ASCIIキーボード「@」や「|」のキーが違うコードを出力している模様。まるでJIS配列キーボードの感じ。
・上下の矢印キーが上手く認識されない(左右の矢印キーはOK)。スクリーンエディタは別のキーを使えるが、bashでリコールバッファが使えないのは痛い。

2番目と3番目のキーコードの問題はロカールとかXの設定ファイルをいじれば動きそうな感じもするが、小生はこの辺が不得手であり、また元々現在の時計プロジェクトでは入力UIは不要なので詮索しない。キーが正常に動作したとしても、マウスポインタの問題でこのUIを使っての開発は実際的ではない。

ネットワーク

これも考えるより産むが易し、と言うか、考えと違う形であっけなく動いた、と言うより動いていた。

以下のスタートアップファイルがあることを確認する。

root@rpi:/# cat /etc/network/interfaces
auto lo

iface lo inet loopback
iface eth0 inet dhcp

ここで実現されるネットワークは、ターゲット側は仮想イーサネット。ホスト側のQEMUのユーザコードがNAT付きラウタ、DHCPサーバ、DNSサーバを実現する閉じたLAN。ターゲットは常に10.0.2.15、ホスト側仮想ゲートウェイは10.0.2.2、仮想DNSサーバは10.0.2.3を与えられるようだ(ターゲットの/etc/resolv.confはNetwork Managerが自動的に書き換える)。ホスト側の仮想DNSサーバはキャッシュオンリーなのでホスト側では何も特別なことをする必要はない。また、DHCPのベースアドレスはQEMU起動時のオプションで設定できるようだが試していない。

このNAT付きラウタというトポロジーのため、QEMUホストがファイアウォールとなり、QEMUホスト以外の他のネットワークホストからはターゲットは見えない。また、ラウタは純粋にユーザコードで実現されているので、QEMUホストの他のプロセスから見えるネットワークデバイスなども存在せず、ターゲットに対するトリッキーなラウティングとかも不可能。

従って、QEMUで走るターゲットをサーバとして使うアプリケーションの開発には向いていない

唯一の抜け穴は、QEMU起動時に指定した -redir tcp:2222::22 でリディレクトされたTCPポート。QEMUホストのポート2222にアクセスすればターゲットRPiのポート22にアクセスしたことになるから、QEMUホストが見えるところならどこからでもターゲットでSSHでアクセスできる。

この辺のことは、5年ほど前にAndroidタブレットの立ち上げの仕事(OSはまだDonutだった)をしていたときに使っていたはずなのに、すっかり忘れている。加齢とともに記憶の揮発性が高まってきたのか…。

逆に、ターゲットからはQEMUホストが見えるところならどこでもアクセスできる。ただし、アクセスされた方はアクセス元はQEMUホストに見えるので注意。例えば、ターゲットからQEMUホストにSSHでアクセスすると、SSH_CONNECTIONは127.0.0.1(localhost)からアクセスされたと主張する。うちのシステムはセキュリティのためにSSHのアクセス元でフィルタリングをしているの修正が必要だった。

もう一つ気がついたことは、QEMUのラウタはICMPを通していないようだ。例えば、ターゲットからwww.yahoo.comにpingすると、IPアドレスは解決するのにpingその物は到達できないという現象に2時間悩んだ。

なお、設定によってはターゲットのコンソールがQEMUを起動したセッション(シェルウィンドウ)にリディレクトされ、loginプロンプトが出ることがあるが、これはよほどのことがない限り使わない方が無難。^Cを押すとコンソールに食われるのではなく、QEMUプロセスを殺してしまう。

さて、これで実機とほぼ同等の動作をするエミュレータができたので開発がし易くなる。仕事が増えた。

なお、QEMUでエミュレートされるCPUはあまり速くない。/proc/cpuinfoには600 BogoMIPSが表示されるが、ターゲットのX上でマウスを振り回すだけでtopのCPU使用率は100%になってしまう。だからリアルタイム性とかの確認は当然のことながら実機ですべき。

2014年4月22日

Raspberry PiをQEMUエミュレータ上で走らせる(1) QEMUのインストールとカーネルの起動

Raspberry Piを使ったCaller ID+天気予報付きディジタル時計プロジェクトを加速することにした。

今回は小生の通常の「字ばっかり」のプロジェクトと違ってXウィンドウを使ったUI(表示のみ)なので、表示具合を確認しながら進めなければならないが、実機がないと開発できないというのはやりにくい。そこで、QEMUを使って実機なしでエミュレートすることを計画した。

RPi用のQEMUについては、こちらこちらの記事を参考にしたが、(当然のことながら)記事のとおりに進めてもその通り動作しない。以下にカットアンドトライで試した結果を報告する。

QEMUのインストール

まずはQEMUのインストールから。ホストは我が家の何でも屋のFC13(x86 32bit 8GB)。他のディストロでは少々違いがあるかもしれないが、そこは工夫してください。
 
上記記事によると、ほとんどのディストロの標準パッケージのQEMUはRPiのCPU(arm1176)をサポートしておらず、また実際にサポートしていないことが確認できたので、パッケージのインストールは諦め、ソースから作ることにする。

まず、適当な作業用ディレクトリを作り、そこにQEMUのGITをクローンする。

h@spice:~/projects$ mkdir rpi
h@spice:~/projects$ cd rpi
h@spice:~/projects/rpi$ git git://git.qemu-project.org/qemu.git

あとは普通にconfigureとmakeを走らせる。上記記事ではPREFIXを「/usr」にしているが、標準パッケージとのガチンコを避けるためにデフォルトの「/usr/local」にしておく。「git submodule update --init dtc」は、configureがDTC(libfdt)が見つからないと文句をいい、「libfdt-devel」パッケージが見つからなかったので。

h@spice:~/projects/rpi$ cd qemu
h@spice:~/projects/rpi/qemu$ git submodule update --init dtc
h@spice:~/projects/rpi/qemu$ ./configure --target-list="arm-softmmu arm-linux-user"    \
            --enable-sdl    \
            --prefix=/usr/local
 
・・・

h@spice:~/projects/rpi/qemu$ make
・・・

h@spice:~/projects/rpi/qemu$ sudo make install

上記記事では、ここで「qemu-system-arm -cpu help」でRPiのCPU(arm1176)がサポートされているかどうか確認することになっているが、これが上手くいかない。代わりに「qemu-arm -cpu ?」は動いたので、これでよいことにする。

h@spice:~/projects/rpi/qemu$ qemu-system-arm -cpu help
No machine specified, and there is no default.
Use -machine help to list supported machines!

h@spice:~/projects/rpi/qemu$ qemu-arm -cpu ?  
Available CPUs:
  arm1026
  arm1136
  arm1136-r2
  arm1176
・・・

RPiのイメージファイル

次は、また適当な作業ディレクトリを作って、カーネルイメージとディスク(SDカード)イメージファイルを作る。
カーネルイメージファイル: 自前のカーネルはダメ。以下のように専用カーネルをダウンロードする。
ディスクイメージファイル: 動作するディスクイメージ(ファイルシステムイメージではない)なら何でもよい。ここでは実機「rpi」からコピーしてみた(このコピーは時間がかかるので先行してバックグラウンドで実行するとよい)。

言い忘れたが、この作業ディレクトリはディスクイメージファイル(小生の実機は8GB)を 作るので、ホストに十分なディスク空き容量があることを確認しておく。

h@spice:~/projects/rpi$ mkdir rpi-emu
h@spice:~/projects/rpi$ cd rpi-emu
h@spice:~/projects/rpi/rpi-emu$ wget http://xecdesign.com/downloads/linux-qemu/kernel-qemu
h@spice:~/projects/rpi/rpi-emu$ ssh rpi if=/dev/mmcblk0 bs=16M >rpi.img

イメージファイルができたらQEMUを起動する。

h@spice:~/projects/rpi/rpi-emu$ qemu-system-arm -kernel kernel-qemu -cpu arm1176 -m 256 -M versatilepb -serial stdio -append "root=/dev/sda2 panic=1 rootfstype=ext4 rw init=/bin/bash" -hda rpi.img
audio: Could not init `oss' audio driver
Uncompressing Linux... done, booting the kernel.

お馴染みの「Uncompressing Linux...」が表示され、デスクトップにRPiのコンソールウィンドウが現れるので嬉しくなる。

ここでのキモは、カーネルコマンドラインの「init=/bin/bash」で、ユーザレベルの初期化を何もせずにいきなりbashを起動する

もし何らかのエラーがありカーネルがパニックになると、上記のコマンドラインでは自動的にリブートを繰り返す。「-no-reboot」オプションを追加すると、リブートせずにQEMUが終了する。いずれにせよデバックがやり辛いので、パニックのメッセージがコンソールに表示されたらコンソールウィンドウの MENU>PAUSE でウィンドウを止める。残念ながらこのウィンドウはスクロールできないようなので(もしかしたらできるかもしれないがよく分からない)、PAUSEは上手くタイミングを計らねばならない。

下はわざとエラーを起こして見たところ(ディスクイメージファイルが完成しないうちにマウントさせようとした)。


順調に行けば、bashのプロンプトが出てきて「やった~!」となるが、世の中そう甘くはない。起動しただけの状態では、言わば丸裸で、/procさえマウントされていないから、色々なシステムユーティリティが動かない。取りあえずちょっとだけやってみる。


ダミーのネットワークインタフェースが見えるが、MACアドレスはわざとらしい「~00:12:34:56」。カーネルのバージョンは、実機の3.10.24+ #614より新しそう。

取りあえずカーネルは動き、シェルが動くようになった。次はここでネットワークとXウインドウを動かすが、それは次回に。

おっと、一言。QEMUのコンソールウィンドウは、クリックすると以降のマウスクリックやキーボードの入力をすべてトラップするので、意図せずにトラップされた時は、パニックらずに「Ctrl+Alt+G」で逃げ出す。

2014年4月17日

Raspberry Pi の X のスクリーンブランキングを禁止する

Raspberry Piのビデオ端子(コンポジットまたはHDMI)にXサーバを走らせることができる。R Piでグラフィクスを使い方を色々検討してみたが、汎用性(他のプラットフォームでシミュレーションができる)などを考えると一番楽そうなのは結局Xを使うことのようだ。

残念なことに、デフォルトではスクリーンブランキングが働き、表示開始数分後には真っ暗になってしまう。

ネットでいろいろブランキングに禁止方法を探したが、最終的にたどり着いたのはXサーバの起動時にオプションを与える方法。これは表示のみのアプリケーションなので、ついでにXカーソルも隠しておく。

# X -s 0 -dpms -nocursor

これを起動時に自動的に行うには、/etc/lightdm/lightdm.conf の以下の行を変更すればよいらしい(まだ試していない)。

[SeatDefaults] 
xserver-command=X -s 0 -dpms -nocursor

で、RPiとXで何をしようとしているかと言うと、テレビの足元に置く天気予報+Caller ID(日本の「ナンバーディスプレイ」に相当)付ディジタル時計を実現したいのだ。小生は時計の正確性については一種病的なこだわりがあるが、手頃な値段の置時計型電波時計の市販品はあまり多くない上に、バックライトなしのLCDまたはオンデマンドつまりボタンを押したときだけ発光するLCDしかなく、我が家のように割と薄暗い家族居間で2~3mの距離から見るテレビの足元に置くには役に立たない。

ちょっと考えるとAndroidタブレットでできそうなものだが、タップしかできないUIは「置時計」には使えないしスクリーンサイズも価格も大げさ過ぎる。Amazon.comを見ながらいろいろ考えた結果思い付いたのは、車載カメラ用のLCDモニタ。3.5~4.5インチサイズで$20前後と手頃感言うことなし。

実際に1台手に入れた。RPiのコンポジットビデオ端子に繋ぎ、実験用電源で12Vを与えると快調に動作する。LCDが大したことないので、コントラスト、特に斜めから見たときの背景が黒くないなどの問題はあるが十分実用になる。実験したところ、電源電圧を5Vまで落としても動作するのでUSB用から電源も取れそうだが、RPiのUSBは外部への電源供給があまり余裕がないので別電源にした方がよいと思う。と言うことで、USB電源2系統付きのACアダプタも注文した。

この手のLCDモニタは、なぜか480x234の物理ピクセルを16:9の画面に配置する矩形ピクセル仕様で、入力は640x480の論理ピクセルとややこしいが、どうせ実際の画面レイアウトは実験的に決めるしかない。

取り敢えず簡単に時計を表示するには、以下のコマンドを走らせる 。

# DISPLAY=:0 xclock -digital -update 1 -strftime '%H:%M' -face curier-116:bold -foreground green -background black &
# DISPLAY=:0 xclock -digital -geometry +500+55 -update 1 -strftime '%S' -face curier-72:bold -foreground green -background black &

こんな感じ。

フォントだとかいろいろな面倒はありそうだが、ImageMagickとかと組み合わせて、何となくスクリプトだけで目的を達成できそうな感じがしてきた。

2014年4月5日

va_list の ap は消費される。

前回のデーモンプロジェクトでは結局ログメッセージをsyslogとstderrの両方に出力するようにし、以下のようなwarnx(3)の包み込みを書いたのだがうまく行かない。


static void __warnx(const char *fmt, ...)
{

        va_list ap;

        va_start(ap, fmt);
        if (lopt)
                vsyslog(LOG_WARNING, fmt, ap);
        if (eopt)

                vwarnx(fmt, ap);
        va_end(ap);
}

lopteoptの両方が真の時、vwarnx(3)の出力が乱れるのだ。

vsyslog(3)vwarnx(3) を入れ替えると、必ず後に呼ばれる方が乱れることから、v付きの関数が呼ばれる度にapが消費されるのではないかと考え、以下のようにそれぞれのv付き関数にapを新しく確保するようにしたらうまく行った。


static void __warnx(const char *fmt, ...)
{

        va_list ap;

        va_start(ap, fmt);
        if (lopt)
                vsyslog(LOG_WARNING, fmt, ap);
 
        va_end(ap);
        va_start(ap, fmt);
        if (eopt)
                vwarnx(fmt, ap);
        va_end(ap);
}

このことはva_start(3)のマニュアルページには明記されていない

va_copy(3)を使ってapを複製・保存(va_copy(aq, ap);)するという手もあるのかも知れないが、その際、マニュアルページにはva_copy(3)で複製されたaqva_end(aq);で開放しなければならないと書いてあるが、複製元のapについては明記されていない(通常はva_start(3)で確保されたapは必ずva_end(3)で開放)。

深入りするのが面倒なので今回はこれでよしとする。

2014年4月4日

stderr(標準エラー)をsyslog にリディレクトする

カーネルからNETLINKで情報をデーモンプロセスに送るアプリケーションを書いている。

最初はメモリアロケーションだとかファイルのオープンだとかの「起こり得ない」エラーの処理はてきとーにスタブコードを書いておいた。機能が順調に動作してコードが洗練されてくるに連れ、エラーが起こってもメモリリークが起きないようにだとかの細々した配慮を施したが、エラーの報告は取りあえず warn(3)とかで簡単に済ませてきた。しかしプロジェクトが完成に近づいたところで、ロギングもちゃんと実現しなければならなくなった。

エラーが起こった場合メイルとか何かでユーザに知らせることも考えられるが、取りあえずはsyslogを使ってシステムのログファイルに記録すればよい、となった。
 
ところで、コードの開発中はプロセスはフォアグラウンドで動作させていたので、エラーメッセージはwarn(3)で標準エラーに書き出しているだけだったが、これをできるだけ(と言うより「できれば」)変更しないでsyslogに喰わせてやりたい。またトラブルシュート用にコマンドオプションで従来どおり標準出力にも書き出せるようにしたい。…ということを「簡単に」実現したい、というのが今回の課題。

stderr = popen("/usr/bin/logger -i -t mydaemon", "w");

のようにstderrをパイプでloggerプロセスにつないでやることもできるが、もしこのデーモンが死んだときにloggerプロセスがいつまでも残っているかも知れないなどの懸念がある。

ぐぐってみたら、あった、あった。ユーザ定義の入出力ハンドラを持つストリームをオープンしてstderrと置き換えるいう方法だ。

#include <stdio.h>
#include <err.h>

static bool dopt, lopt;
static FILE *stderr2;

static size_t logit(void *cookie, const char *buf, size_t len)
{
        if (stderr2)
                fwrite(buf, len, 1, stderr2);
        syslog(LOG_WARNING, "%.*s", (int)len, buf);
        return len;
}

static cookie_io_functions_t log_fns = {
        NULL, (cookie_write_function_t *)logit, NULL, NULL,
};

static void setup_logger(void)
{
        if (dopt) {
                int fd = dup(fileno(stderr));

                if (fd < 0)
                        err(1, "failed to dup stderr.");

                if (!(stderr2 = fdopen(fd, "w")))
                        err(1, "failed to fdopen stderr.");
        }

        fclose(stderr);

        stderr = fopencookie(NULL, "w", log_fns);
        setvbuf(stderr, NULL, _IOLBF, 0);
}


int main(int argc, char *argv[])
{
...
        if (lopt)
                setup_logger();
...
        warnx("This is output to stderr, which is redirected to logit().");

上が今回試してみて動作したコードの一部。

fprintf(3)warn(3)など、凡そ標準エラーストリームにこのプロセス内で書き出すデータはすべてlogit()にリディレクトされるから、その後は如何様にでも処理できる。

キモはsetup_logger()から呼ばれるfopencookie(3)が、log_fnsにユーザの定義する読み込み、書き出し、シーク、クローズのハンドラを持つストリームをオープンすること。GLIBCではstdin、stdout、stderrはマクロではなく普通の変数だから、新しい値をいつでも代入でき、その後はfprintf(3)warn(3)などは新しいstderrに書き込むようになる。

もう一つのキモはstderrを更新した後に呼んでいるsetvbuf(3)。これがないとストリームバッファがいっぱいになるまで出力ハンドラは呼ばれないから、エラーメッセージのように何か起きたらリアルタイムで出力したい用途には好ましくない。ここではバッファ単位を行にしてうまく行っている。

こうして標準出力をまるごとsyslog(3)にリディレクトすることに成功したが、問題点があることも分かった。
  • リディレクションはストリームレベルなのでプロセス内で完結しており、例えば子プロセスの動作を買えることはできない
  • warn(3)はストリームに出力する前にメッセージにプログラム名をプリフィックスとして付加するが、syslog(3)もまた同様の処理をするのでプログラム名が二重に付加されて見苦しい
二重プリフィックスの問題はちょっと致命的だったので、せっかくリディレクションに成功したものの、この方法は諦め、warn(3)を__warn()という包み込み関数(またはマクロ)で置き換えてその中で場合分けして処理するという平凡な構成を最終的な実装とすることにした。

お粗末さまでした。