前回の記事では割とわけのわからんことを書いたと反省。
まあやりたかったことはこうだ。Linux環境において、
1.プロセスAとプロセスBで少量のデータを一方通行に送りたい
2.割とタイムクリティカルな制約でやりたい
3.通信経路をread()でブロッキング監視したい
最後の3はCPUを無駄食いさせないためだ。そしてこの仕様を満たすために使おうとしたのがFIFO、名前つきパイプだった。
プロセス間通信のやり方はいろいろあるが、
・ドメインソケットは便利だがオーバーヘッドが気になる。
・共有メモリは高速だがCPUの無駄食い対策がしにくい。
・read()でブロッキングしてOSにディスパッチを任せたい。
ということでFIFOを使ってみた。結果、大外れであった。
まずfdのopen()が曲者で、FIFOに対する使用 の場合
・write側だけをopen()するとSIGPIPEを発生する(無視もできる)
※SIGPIPEは改めて調べる
・ブロッキングopen()するとread()がブロッキングしない
※openモードで挙動が違う
・ノンブロッキングでopen()するとread()でブロッキングできない
・FIFOの両端が閉じるとその瞬間に抱えているデータが消える
とまあどこまでも扱いにくい代物であった。
やむを得ず
open()でブロッキングして
データが到着したらread()して
close()で最初に戻る
という実装をテストしてみたが、open()/close()の遅延がでかい。
最初のタイムクリティカルな制約を満たせなくなってしまった。
そうじゃない。やりたいことは違うんだ。
1.通信経路をブロッキング指定でopen()するが即時復帰して
2.通信経路をread()したらデータ到着までブロッキングして
3.あとは2をループ中で繰り返せばオーバーヘッドが最小
4.ループが終わったら通信経路をclose()
これを満たすAPIを探していたのだ。そしてついに見つけた。
POSIXメッセージキューがそれだった。
こいつはSystemVメッセージキューを拡張したものだ。
・一度キューを作ればプロセスが落ちてもカーネル寿命で存在する
・両端が閉じてもデータを失うことがない
・read端とwrite端のどちらを先にオープンしてもよい
・mq_open()しても即時復帰する
・mq_receive()を呼ぶとブロッキングモードならデータを待つ
・相手側プロセスがmq_send()すれば起床してデータが取れる
・しかも動作が思ったより高速である
・メッセージに優先度をつけたりシグナルを送ったりできる
(使わないけど)
とまあ、いいことずくめのようだが少々のクセはある。
「メッセージ」の「キュー」なので、データはパケット扱いだ。
パイプのようなバイトストリームではない。
それと、キュー作成時のパラメータをいくつか指定するのだが
・ブロッキングかノンブロッキングか
・1メッセージのサイズ
・キューが保持可能なメッセージ数
・あとなんか1つ(忘れた)
を指定する必要がある。
・データがキューに渋滞しない程度のメッセージ数
・最大公約数的なメッセージサイズ
・キューのブロッキング動作は作成時に決定
という設計が必要だ。加えて落とし穴が1つあって、
・mq_receive()のバッファサイズはメッセージサイズ以上
という制約がある。
メッセージサイズより小さいバッファを指定するとエラーを返す。
これさえ押さえておけば、割と簡単に使うことができる。
ただしプロセスが落ちてもカーネルバッファにキューのデータが残ることには注意が必要である。プロセス落ちからの再起動があるならキューの残存データは考慮しておこう。
まあ細かいこと言いだせば、シグナル通知とかforkしてパイプとかメモリマップトファイルとかあるにはあるのだが、赤の他人ならぬ他プロセスとのデータ交換を考えるなら、FIFOかキューかドメインソケットから選ぶことになるだろう。共有メモリは排他制御とCPU浪費対策が面倒だ。
API仕様の表面からは見えにくい罠にはまってしまった。