プロセス間通信 ~共有メモリ~

# 目的
プロセス間通信(IPC)のうち「共有メモリ」について調べたことを備忘録としてまとめました。

プロセス間通信のやり方のまとめとしては以下の記事がとても参考になりました。
コードに関してもかなり参考にさせていただきました。
[Linuxのプロセス間通信 (Qiita)](https://qiita.com/MoriokaReimen/items/5c4256ef620499a88bb3)

ここでの環境は以下の通りです。コードはC++で書いています。
MacOS 10.13.4
gcc 7.3.0

不備等あれば指摘していただけると幸いです。

[追記]
以下で解説しているのはSystemVの共有メモリで、POSIXの共有メモリも存在するそうです。

# 解説
## 共有メモリとは
[Wikipedia](https://ja.wikipedia.org/wiki/共有メモリ)によると「共有メモリはプログラム間でデータをやりとりする効率的手段である。」「1つのプロセスがメモリ上に他のプロセスからもアクセスできる領域を作成する。」とのことです。
1つのメモリ領域を使ってデータをやりとりするのは処理としてかなり高速でしょう。
ただ複数のプロセスが同じメモリにアクセスできるので競合には気をつけないといけないみたいです。

## 共有メモリ操作の流れ
* 共有メモリ用のkey(一意な値)を発行する
* 大きさを指定したセグメントとkeyを紐づけて共有メモリを作成する
* プロセスと共有メモリを紐付ける
* （行いたい処理を行う）
* プロセスと共有メモリの紐付けを解除する。
* メモリを解放する

大事なことは最初にプロセスとは独立に共有メモリが作成される点です。
その後に複数のプロセスと紐付けられていきます。



## 共有メモリ操作の詳細
共有メモリを扱うためにUnix系では以下の関数が用意されています。
`shmget()` ([Man page of SHMGET](https://linuxjm.osdn.jp/html/LDP_man-pages/man2/shmget.2.html))
`shmat()` ([Man page of SHMOP](https://linuxjm.osdn.jp/html/LDP_man-pages/man2/shmop.2.html))
`shmdt()` ([Man page of SHMOP](https://linuxjm.osdn.jp/html/LDP_man-pages/man2/shmop.2.html))
`shmctl()` ([Man page of SHMCTL](https://linuxjm.osdn.jp/html/LDP_man-pages/man2/shmctl.2.html))
細かいことはそれぞれの Man page を読んでみてください。

ここでは大事な部分だけ解説していきます。


### shmget()
この関数は共有メモリを作り、その識別子を発行する関数です。
またすでに作成された共有メモリの識別子を入手するためにも使います。
3つの引数を取ります。
・*key*
これはIPCキーのことで、`ftok()`か`IPC_PRIVATE`を使って取得できます。
ただの一意な値だと考えていいと思います。
詳しくは[System V IPC 基礎](https://www.ksworks.org/memo/net/sysv-ipc.html)や[Man Page of FTOK](https://linuxjm.osdn.jp/html/LDP_man-pages/man3/ftok.3.html)などを読んでみてください。
このkeyはkey_t型ですが、これを使うために`<sys/types.h>`が必要です。  

・*メモリサイズ*
これはPAGE_SIZEの倍数でないといけません。
そうなってなかったら、ちょっと増やしたりして勝手に直しておいてくれるらしいです。  

・*flag*
共有メモリ作成のオプションや、作成したメモリの権限の指定ができます。
詳しくは[Man Page](https://linuxjm.osdn.jp/html/LDP_man-pages/man2/shmget.2.html)をみてください。
ここに載っている以外にも権限を設定するフラグ(`chmod`で使うやつ)が使えるみたいです。

作成済みの共有メモリのIDを入手したい場合は、keyのみを指定しそれ以外を0としておけばOKです。

### shmat()
この関数は作成した共有メモリをプロセスのアドレス空間にアタッチしてくれます。
3つの引数を取ります。
・*共有メモリID*
`shmget()`が作成し返してくれた値です。  

・*メモリ番地*
共有メモリをアタッチしたいアドレスです。
ここは0にしておけば勝手に選んでおいてくれるみたいです。  

・*flag*
オプションを指定するためのものです。
詳しくは[Man Page](https://linuxjm.osdn.jp/html/LDP_man-pages/man2/shmop.2.html)を読んでください。

### shmdt()
この関数は共有メモリをプロセスのアドレス空間から切り離してくれます。
引数は1つだけで`shmat()`が返してくれたアドレスでいいみたいです。
非常に簡単です。

### shmctl()
この関数は共有メモリに関して様々な制御を行うために使います。
色んな機能があるので細かいことは[Man Page](https://linuxjm.osdn.jp/html/LDP_man-pages/man2/shmctl.2.html)に委ねたいと思います。
3つの引数を取ります。
・*共有メモリID*
制御を行いたい共有メモリのIDです。
`shmget()`が発行してくれたやつですね。  

・*コマンド*
共有メモリに対して行いたい制御の内容です。
この記事では`IPC_RMID`というメモリを解放するためのものを使います。

・*shmid_ds構造体へのポインタ*
共有メモリに対して付加したい情報を収めておくための構造体です。
`IPC_STAT`や`IPC_SET`といったコマンドを使用する際には必要です。
今回は必要ないのでNULL(あるいは0)としておきます。


# 実際に使ってみる
これだけ解説すればきっともう大丈夫。
せっかく解説したので実際に動くものを作ってみます。
shm_a.cppが共有メモリを管理し、そこに好きな文字列を書き込みます。
shm_b.cppは書き込まれた内容を読み取って表示します。

2つを同じディレクトリにおいて別のターミナルから実行しましょう。
shm_aで書き込んだ内容がshm_bでも反映されるのが分かると思います。

```cpp:shm_a.cpp
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <string>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/stat.h>
using namespace std;


int main(){
    // 空のファイル作成
    FILE *fp;
    const string file_path = "./key.dat";
    fp = fopen(file_path.c_str(), "w");
    fclose(fp);

    // IPC keyの取得
    const int id = 50;
    const key_t key = ftok(file_path.c_str(), id);
    if(key == -1){
        cerr << "Failed to acquire key" << endl; 
        return EXIT_FAILURE;  
    }

    // 共有メモリIDの取得
    const int size = 0x6400;
    const int seg_id = shmget(key, size, 
                              IPC_CREAT | IPC_EXCL | S_IRUSR | S_IWUSR);
    if(seg_id == -1){
        cerr << "Failed to acquire segment" << endl;
        return EXIT_FAILURE;
    }

    // 共有メモリをプロセスにアタッチする
    char* const shared_memory = reinterpret_cast<char*>(shmat(seg_id, 0, 0));

    // 共有メモリに書き込む
    string s;
    int flag = 0;
    cout << "if you want to close, please type 'q'" << endl;
    while(flag == 0){
        cout << "word: ";
        cin >> s;
        if(s == "q") flag = 1;
        else sprintf(shared_memory, s.c_str());
    }

    // 共有メモリをプロセスから切り離す
    shmdt(shared_memory);

    // 共有メモリを解放する
    shmctl(seg_id, IPC_RMID, NULL);    

    return 0;
}
```
```cpp:shm_b.cpp
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <string>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/stat.h>
using namespace std;


int main(){
    // 作成済みの共有メモリのIDを取得する
	const string file_path = "./key.dat";
    const int id = 50;

    const key_t key = ftok(file_path.c_str(), id);

    const int seg_id = shmget(key, 0, 0);
    if(seg_id == -1){
        cerr << "Failed to acquire segment" << endl;
        return EXIT_FAILURE;
    }

    // 共有メモリをプロセスにアタッチする
    char* const shared_memory = reinterpret_cast<char*>(shmat(seg_id, 0, 0));

    // 共有メモリの文字を読み取る
    int flag = 0;
    char c;
    cout << "If you want to close, please type 'q'" << endl;
    cout << "If you want to read the shared memory, push enter button." << endl;
    while(flag == 0){
        cin.get(c);
        if(c == 'q') flag = 1;
        else printf("%s\n", shared_memory);
    }

    // 共有メモリをプロセスから切り離す
    shmdt(shared_memory);

    return 0;
}
```

# 困ったときは
プロセス間通信を管理するコマンドとして`ipcs`があります。
これは存在している共有メモリの情報を教えてくれます。
なのでshm_a.cppを実行しているときに別のターミナルで`ipcs`を実行すると共有メモリの存在を確認できます。
また、このプログラムを強制終了させたりして共有メモリがちゃんと解放されていない場合は、`ipcrm`を使って削除することができます。
使い方は以下のサイトを読んでみてください。
[ipcs コマンド - IBM](https://www.ibm.com/support/knowledgecenter/ja/ssw_aix_71/com.ibm.aix.cmds3/ipcs.htm)
[ipcrm - プロセス間通信 ID を削除する - IBM](https://www.ibm.com/support/knowledgecenter/ja/ssw_ibm_i_71/rzahz/rzahzipcrm.htm)

# 参考
* [Man page of SHMGET](https://linuxjm.osdn.jp/html/LDP_man-pages/man2/shmget.2.html)
* [Man page of SHMOP](https://linuxjm.osdn.jp/html/LDP_man-pages/man2/shmop.2.html)
* [Man page of SHMCTL](https://linuxjm.osdn.jp/html/LDP_man-pages/man2/shmctl.2.html)
* [Man Page of FTOK](https://linuxjm.osdn.jp/html/LDP_man-pages/man3/ftok.3.html)
* [shmget() - 共用メモリー・セグメントの取得 - IBM] (https://www.ibm.com/support/knowledgecenter/ja/SSLTBW_2.2.0/com.ibm.zos.v2r2.bpxbd00/rshmgt.htm)
* [ipcs コマンド - IBM](https://www.ibm.com/support/knowledgecenter/ja/ssw_aix_71/com.ibm.aix.cmds3/ipcs.htm)
* [ipcrm - プロセス間通信 ID を削除する - IBM](https://www.ibm.com/support/knowledgecenter/ja/ssw_ibm_i_71/rzahz/rzahzipcrm.htm)
* [Wikipedia 共有メモリ](https://ja.wikipedia.org/wiki/共有メモリ)
* [Linuxのプロセス間通信](https://qiita.com/MoriokaReimen/items/5c4256ef620499a88bb3)
* [System V IPC 基礎 - ksworks.org](https://www.ksworks.org/memo/net/sysv-ipc.html)