iPX社員によるブログ

iPX社員が"社の動向"から"自身の知見や趣味"、"セミナーなどのおすすめ情報"に至るまで幅広い話題を投下していくブログ。社の雰囲気を感じ取っていただけたら幸いです。

"Little and often fills the purse...?"

こんにちは、Hirosakiです。
早いもので、前回の投稿から半年、入社から一年経ちました。
開発作業をUbuntu上で行うことが多くなり、使い慣れていく半面、Windows環境の使いにくさを覚える機会が増えていく日々を送っています。
また、ありがたいことにDRIVE PX2上で動作させるプログラム構築をする機会が増えました。


今回はそのDRIVE PX2上で構築したプログラム中の異なる2つのプロセスが連携して動作する部分にて使用した「<共有メモリ>を使用したプロセス間通信」についてのお話です。


<余談>
本来1つのプログラムで完結させたかったところでしたが、そうできなかったのは
必要ライブラリがアーキテクチャ違いでコンパイルできないという致命的な問題があったからなのでした。
どこかで聞いたような話ですね

では続きます。

まず今回実装するC++コードにおける基本的な共有メモリ動作の流れは以下の通り。
①キー情報を元に共有メモリの取得(なければ生成)
②共有メモリと接続(アタッチ)
③共有メモリの内容を参照・更新
④共有メモリから離脱(デタッチ)
⑤共有メモリ情報の破棄(生成した場合)

そしてこの最小構成をコード化します。

#include <string.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>

int id = shmget(IPC_PRIVATE, 512, IPC_CREAT|0666);

shmget(キー, サイズ, フラグ)*1関数を使用します。
取得または生成に失敗した場合は-1が返却されるのでその辺の回避処理をお忘れなく。

char *adr = (char *)shmat(id, NULL, 0);

shmat*2関数で先ほど取得した共有メモリにアタッチします。
こちらもアタッチ失敗の場合はエラーフラグが返ってきますが、voidポインタ型の-1ですので注意。

strcpy(adr,"Initial");
if (strcmp(adr, "end") == 0) {
    break;
}

更新する場合はshmat関数の返却値(今回の場合変数adr)に対しstrcpyで、
比較する場合はstrcmpを使います。
筆者が少し躓いたところですが、strcmp関数は完全一致した場合に0を返すことに注意。
単純にif条件文に書いてしまうと盛大にスルーされます。

shmdt(adr);

shmdt*3関数を使ってデタッチします。shmat同様こちらもエラーの場合は-1が返ってきます。

shmctl(id, IPC_RMID, 0);

shmctl*4関数で共有メモリを破棄します。この関数もエラーの場合は-1が返ってきます。
①を実装する方のプロセスにのみ実装します。

実際の動くコードを載せるべきでしょうがここまでで既に長々と書いてしまっているので割愛します。
とはいえこれまでのコードを合わせて、③の処理を無限ループに閉じ込めてスレッドスリープで時間調整してしまうだけで完成してしまいます。
そう、これくらいの単純な処理であればですが・・・・


さて、これらを踏まえた最小コードの動きを見てみます。
f:id:ipx-writer:20180430235828p:plain
受信側実行状態。
今回動かしているものはスレッドスリープで1秒ごとに共有メモリの内容を出力しています。

f:id:ipx-writer:20180430235910p:plain
送信側実行状態。
今回は受信側が共有メモリのキー情報を出力しているので、その値と共有メモリに更新する内容を入力情報としています。

f:id:ipx-writer:20180430235859p:plain
送信側実行1回目直後の受信側実行状態。
共有メモリの内容が更新されていることが分かります。

f:id:ipx-writer:20180430235933p:plain
送信側実行2回目直後の受信側実行状態。
"end"という内容を受けて受信側プロセスが終了しました。

と、このように単純ながらプロセス間の通信を行うことができました。
今回の最小構成コードでは出力されたキー情報を手打ちで別プロセスに渡しましたが、無論実際のシステムでそんなことは行いません。
ftok関数を使ってファイル情報を元にキー情報を生成します。
この関数を使うことで、同じファイルから生成したキー情報を参照するとめでたく同じ共有メモリを参照することができるようになります。

// 送信側
std::string file_path("/tmp/key_data.dat");
key_t key = ftok(file_path.c_str(),42);
id = shmget(key, sizeof(int), IPC_CREAT|0666);

// 受信側
std::string file_path("/tmp/key_data.dat");
key_t key_r = ftok(file_path.c_str(),42);
shmid = shmget(key_r, 0, 0);

さて、これで冒頭出てきたDRIVE PX2上へのシステム実装ができそうです。
以下の構成で実装。
◆送信側(判断処理)
①共有メモリ作成・アタッチ
②判断
③判断結果を共有メモリに更新
④終了処理(デタッチ・共有メモリ情報破棄)
(②、③の処理をループ)

◆受信側(結果処理)
①送信側が使用したファイルと同一のファイル情報を元に作成したキー情報で共有メモリ取得・アタッチ
②共有メモリの内容を元に処理を実行
③送信側が更新した終了フラグを参照した場合ループ脱出、デタッチ
(②の処理をループ)

一見問題なさそうですが、10分を過ぎた辺りでSegmentation Fault発生。
あれこれ試行錯誤した結果、以下のような改修を実施。

while(runFlag) {
    std::this_thread::sleep_for(std::chrono::milliseconds(10));

    std::string file_path("/tmp/key_data.dat");
    key_t key_r = ftok(file_path.c_str(),42);

    shmid = shmget(key_r, 0, 0);

    if(shmid == (-1)){
        break;
    }

    shm = (char *)shmat(shmid, 0, 0);
    flag = std::string(shm);
    if( shmdt(shm) == -1){
        std::cout << "Detouch Error! " << std::endl;
        errFlg = true;
        break;
    }

    if(!flag.compare("EOS")){
        break;
    }
    // 以下判断処理

一部抜粋ですが、雰囲気だけでも伝わっていただけるかと。
ループ内でshmget・shmat・shmdtを行い、かつ共有メモリの内容をコピーして退避させておくことでようやくエラーなく実行できるようになりました。
明確に記載された参考情報が存在するわけではありませんが、共有メモリを掴みっぱなしでいることが問題であったのかなと。
DRIVE PX2へ実装したCAN送信プログラムがバッファオーバフローを起こす問題も経験しましたが、メモリも送信バッファも定期的に中身を空にしておく必要があるということでしょうか。
塵も積もれば山となる、ですかね。

以上、共有メモリの実装についてのお話でした。

*1:参考元→Man page of SHMGET

*2:参考元→Man page of SHMOP

*3:参考元→Man page of SHMOP

*4:参考元→Man page of SHMCTL