Linuxのプロセス間通信

  • 190
    いいね
  • 7
    コメント

この記事について

LinuxのIPC(プロセス間通信)を紹介します。

プロセス間通信とは

Inter Process Communication(IPC)はプログラムの実行単位であるプロセスの間で行われるデータ交換のことを指します。プロセスの依存関係は可能な限り疎結合になるようOSで管理されています。そのため、IPCはLinux OSの機能を経由して行う必要があります。
OSがプロセスに提供するデータ交換の方法はひとつだけではありません。それぞれ特徴のある多彩な方法を提供しています。
ここで紹介するのは以下の5つです。

  1. 共有メモリー
  2. セマフォ
  3. マップドメモリー
  4. パイプ
  5. ソケット通信

(他にありましたらコメントで教えていただければ幸いです。)

それでは、見ていきましょう。

共有メモリ

プロセス間で同じメモリを共有します。
共有メモリの最大の利点はそのアクセススピードにあります。
一度共有メモリを生成してしまえばカーネルの機能を利用せずにアクセスすることができるため、プロセス内にある普通のメモリと同じ速度でアクセスできます。
このアクセス速度のメリットが必要になるような処理性能が求められるソフトウェアではこの方法がよく使われます。
一方、2つのプロセスから同時に書き込みを行うと競合が発生します。
共有メモリにはこの競合を防ぐ相互排他機構が組み込まれていません。
自分で用意する必要があります。

共有メモリを使ったサンプルコードをいかに示します。
process_aで共有メモリを確保して文字列"Hello World!"を共有メモリに書き込みます。
共有メモリに書き込まれたデータはprocess_bで読み取れてコンソールにプリントされます。

process_a.cpp
#include <cstdio>
#include <cstdlib>
#include <iostream>
#include <string>
#include <sys/shm.h>
#include <sys/stat.h>
#include <sys/ipc.h>

int main ()
{
    /* セグメントIDの生成 */
    const std::string file_path("./key_data.dat");
    const int id = 42;
    const auto key = ftok(file_path.c_str(), id);
    if(-1 == key) {
        std::cerr << "Failed to acquire key" << std::endl;
        return EXIT_FAILURE;
    }

    /* セグメントの割当 */
    const int shared_segment_size = 0x6400;
    const auto segment_id = shmget(key, shared_segment_size,
                                   IPC_CREAT | IPC_EXCL | S_IRUSR | S_IWUSR);
    if(-1==segment_id) {
        std::cerr << segment_id << " :Failed to acquire segment" << std::endl;
        return EXIT_FAILURE;
    }

    /* キーとセグメントIDの表示 */
    std::cout << "キー:" << key << std::endl;
    std::cout << "セグメントID:" << segment_id << std::endl;

    /* 共有メモリにアタッチ */
    char* const shared_memory = reinterpret_cast<char*>(shmat(segment_id, 0, 0));
    printf ("shared memory attached at address %p\n", shared_memory);

    /* 共有メモリへの書込み */
    sprintf (shared_memory, "Hello, world.");


    std::cout << "Hit any key when ready to close shared memory" << std::endl;
    std::cin.get();

    /* 共有メモリのデタッチ */
    shmdt(shared_memory);
    /* 共有メモリの解放 */
    shmctl (segment_id, IPC_RMID, 0);

    return EXIT_SUCCESS;
}
process_b.cpp
#include <cstdio>
#include <cstdlib>
#include <iostream>
#include <string>
#include <sys/shm.h>
#include <sys/stat.h>

int main ()
{
    /* セグメントIDの生成 */
    const std::string file_path("./key_data.dat");
    const int id = 42;
    const auto key = ftok(file_path.c_str(), id);
    if(-1 == key) {
        std::cerr << "Failed to acquire key" << std::endl;
        return EXIT_FAILURE;
    }


    /* 共有メモリにアタッチ */
    const auto segment_id = shmget(key, 0, 0);
    const char* const shared_memory = reinterpret_cast<char*>(shmat(segment_id, 0, 0));
    printf ("shared memory attached at address %p\n", shared_memory);

    /* 共有メモリの読み込み */
    printf ("%s\n", shared_memory);

    /* 共有メモリのデタッチ */
    shmdt(shared_memory);

    return EXIT_SUCCESS;
}

プロセス間通信に関わるLinuxのコマンド

プロセス間通信のプログラムを作成するときに必要なコマンドを紹介します。
ひとつはipcsコマンド。
プロセス間通信の状態を表示します。

kei@Glou-Glou:~/src/ipc/shared_memory$ ipcs

------ メッセージキュー --------
キー     msqid      所有者  権限     使用済みバイト数 メッセージ

------ 共有メモリセグメント --------
キー     shmid      所有者  権限     バイト  nattch     状態      
0x00000000 884736     kei        600        16777216   2                       
0x00000000 1409025    kei        600        268435456  2          対象       
0x00000000 819202     kei        600        524288     2          対象

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~省略~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~    

------ セマフォ配列 --------
キー     semid      所有者  権限     nsems     

次にipcrmコマンドを紹介します。
確保された共有リソースを解放します。

kei@Glou-Glou:~/src/ipc/shared_memory$ ipcrm shm <shmid>

セマフォ

セマフォは整数型のデータを親子関係の無いプロセス間で共有します。
複数プロセスの同時アクセスを制御する機構を持っていますが、整数型のデータしか扱えないというのが難点です。
主な用途としては共有メモリの相互排他に使われます。
以下が、セマフォの用いたサンプルコードです。

process_a.cpp
#include<sys/ipc.h>
#include<sys/sem.h>
#include<sys/types.h>
#include<cstdlib>
#include<iostream>

union semun {
    int val;
    struct semid_ds *buf;
    unsigned short int *array;
    struct seminfo *__buf;
};

enum SEMAPHORE_OPERATION
{
    UNLOCK = -1,
    WAIT = 0,
    LOCK = 1,
};

int main()
{
    /* セマフォの確保 */
    const key_t key = 112;
    int sem_flags = 0666;
    int sem_id = semget(key, 1, sem_flags | IPC_CREAT);
    if(-1 == sem_id)
    {
        std::cerr << "Failed to acquire semapore" << std::endl;
        return EXIT_FAILURE;
    }

    /* セマフォの初期化 */
    union semun argument;
    unsigned short values[1];
    values[0] = 1;
    argument.array = values;
    semctl(sem_id, 0, SETALL, argument);

    /* プロセスBの実行を待つ */
    std::cout << "Waiting for post operation..." << std::endl;
    sembuf operations[1];
    operations[0].sem_num = 0;
    operations[0].sem_op = WAIT;
    operations[0].sem_flg = SEM_UNDO;
    semop(sem_id, operations, 1);

    /* セマフォの解放 */
    auto result = semctl(sem_id, 1, IPC_RMID, NULL);
    if(-1 == result)
    {
        std::cerr << "Failed to close semaphore" << std::endl;
        return EXIT_FAILURE;
    }

    return EXIT_SUCCESS;
}
process_b.cpp
#include<sys/ipc.h>
#include<sys/sem.h>
#include<sys/types.h>
#include<cstdlib>
#include<iostream>

union semun {
    int val;
    struct semid_ds *buf;
    unsigned short int *array;
    struct seminfo *__buf;
};

enum SEMAPHORE_OPERATION
{
    UNLOCK = -1,
    WAIT = 0,
    LOCK = 1,
};

int main()
{
    /* セマフォの確保 */
    const key_t key = 112;
    int sem_flags = 0666;
    int sem_id = semget(key, 1, sem_flags);
    if(-1 == sem_id)
    {
        std::cerr << "Failed to acquire semapore" << std::endl;
        return EXIT_FAILURE;
    }

    std::cout << "Unlock semaphore" << std::endl;

    /* セマフォにポスト */
    sembuf operations[1];
    operations[0].sem_num = 0;
    operations[0].sem_op = UNLOCK;
    operations[0].sem_flg = SEM_UNDO;
    semop(sem_id, operations, 1);

    return EXIT_SUCCESS;
}

マップドメモリー

複数のプロセスがファイルを介して通信します。
ファイルのアクセスの時にメモリマッピングを行い処理を高速化させます。
メモリマッピングとはファイルやデバイスなどをあたかもメモリのようにアクセスできるよう仮想アドレス空間にマップすることを指します。
これにより、シリアライズなしでファイルにデータを配置することができます。

それでは、メモリマップを使ったプロセス間通信のサンプルコードを以下に示します。

process_a.cpp
#include<cstdlib>
#include<cstdio>
#include<string>
#include<fcntl.h>
#include<sys/mman.h>
#include<sys/stat.h>
#include<unistd.h>

const unsigned int FILE_LENGTH = 0x100;
const std::string FILE_NAME("./data.dat");

int main()
{
    /* Prepare a file large enough to hold an unsigned integer. */
    auto fd = open(FILE_NAME.c_str(), O_RDWR | O_CREAT, S_IRUSR | S_IWUSR);
    lseek(fd, FILE_LENGTH+1, SEEK_SET);
    write(fd, "", 1);
    lseek(fd, 0, SEEK_SET);
    /* Create the memory mapping. */
    char* const file_memory = reinterpret_cast<char*>(mmap(0, FILE_LENGTH, PROT_WRITE, MAP_SHARED, fd, 0));
    close(fd);
    /* Write a random integer to memory-mapped area. */
    sprintf(file_memory, "%s", "Hello World!");
    /* Release the memory (unnecessary because the program exits). */
    munmap (file_memory, FILE_LENGTH);

    return EXIT_SUCCESS;
}
process_b.cpp
#include<cstdlib>
#include<cstdio>
#include<string>
#include<fcntl.h>
#include<sys/mman.h>
#include<sys/stat.h>
#include<unistd.h>

const unsigned int FILE_LENGTH = 0x100;
const std::string FILE_NAME("./data.dat");

int main()
{
    /* Open the file. */
    auto fd = open(FILE_NAME.c_str(), O_RDWR, S_IRUSR | S_IWUSR);
    /* Create the memory mapping. */
    char* const file_memory = reinterpret_cast<char*>(mmap(0, FILE_LENGTH, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0));
    close(fd);
    /* Read the integer, print it out, and double it. */
    printf("%s\n", file_memory);

    /* Release the memory (unnecessary because the program exits). */
    munmap(file_memory, FILE_LENGTH);

    return EXIT_SUCCESS;
}

パイプ

パイプは親子関係にあるプロセス間の一方向の通信を実現します。
コマンドラインでお馴染みでしょう。

$ ps aux | grep apache

ps auxの実行結果をパイプ|を通じてgrepコマンドで渡されます。
しかし、このパイプ厄介なのがプロセス間に親子関係が必要になるということです。
process_aプログラムがprocess_bプログラムを起動してパイプを通じてデータの交換を行うサンプルプログラムを作成しようとしましたが、うまく行きませんでした。
できる方がいらっしゃいましたらコメント欄でお知らせください。

FIFO

別名named pipedと呼ばれています。ファイルシステム上で名前を持つパイプです。
親子関係がなくても全てのプロセスがFIFOを作成、アクセス、削除することができます。
このFIFOはmkfifoコマンドでコンソールから作成することもできます。

$ mkfifo ./fifo.tmp

作成されたFIFOは普通のファイルのようにアクセスすることができます。

FIFOに書き込み
$ cat > fifo.tmp
FIFOを読み込み
$ cat fifo.tmp

以下、FIFOでデータを交換するサンプルコードです。

process_a.cpp
#include <cstdio>
#include <cstdlib>
#include <string>
#include <iostream>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>

const size_t BUFFER_SIZE(80);

int main()
{
    // ファイルディスクリプタ
    int fd;

    // FIFOの作成
    // mkfifo(<pathname>, <permission>)
    mkfifo("/tmp/myfifo", 0666);

    std::string message("Hello World!");
    // 書き込み専用でFIFOを開く
    fd = open("/tmp/myfifo", O_WRONLY);

    // メッセージの書き込み
    write(fd, message.c_str(), message.size() + 1);
    close(fd);

    return EXIT_SUCCESS;
}
process_b.cpp
#include <cstdio>
#include <cstdlib>
#include <string>
#include <iostream>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>

const size_t BUFFER_SIZE(80);

int main()
{
    // ファイルディスクリプタ
    int fd;

    // FIFOの作成
    // mkfifo(<pathname>, <permission>)
    mkfifo("/tmp/myfifo", 0666);

    char str[BUFFER_SIZE];
    // 読み込み専用でFIFOを開く
    fd = open("/tmp/myfifo", O_RDONLY);
    read(fd, str, BUFFER_SIZE);
    const std::string message(str);

    // 読み込んだ内容の表示
    std::cout << message << std::endl;
    close(fd);

    return EXIT_SUCCESS;
}

FIFOは複数のプロセスから読み書きできます。複数同時のアクセスは自動的に排他処理されます。

ソケット通信

ソケットは依存関係のないプロセス間での通信を実現します。
加えて、他の方法にはないメリットがあります。それは、他のマシンにあるプロセスと通信できることです。
以下が、ソケットを使用したサンプルコードです。

process_a.cpp
#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<string>
#include<cassert>
#include<sys/socket.h>
#include<sys/un.h>
#include<unistd.h>

int server (const int& client_socket)
{
    while (1) {
        const size_t MAX_SIZE = 128;
        char buffer[MAX_SIZE];
        read(client_socket, buffer, MAX_SIZE);
        const std::string message(buffer);
        if(message.size() == 0) {
            return 0;
        } else {
            std::cout << message << std::endl;
            return 1;
        }
    }

    assert(!"This line must not be executed.");
}

int main()
{
    const std::string socket_name("my_socket");
    int socket_fd;
    sockaddr_un name;
    int client_sent_quit_message;
    /* ソケットを作成 */
    socket_fd = socket(PF_LOCAL, SOCK_STREAM, 0);
    /* サーバーとして設定 */
    name.sun_family = AF_LOCAL;
    strcpy(name.sun_path, socket_name.c_str());
    bind(socket_fd, reinterpret_cast<sockaddr*>(&name), SUN_LEN (&name));
    /* ソケットを開く */
    listen(socket_fd, 5);
    /* 接続されたらメッセージが届くまで待機 */
    do {
        sockaddr_un client_name;
        socklen_t client_name_len;
        int client_socket_fd;
        /* 接続があるまで待機 */
        client_socket_fd = accept(socket_fd, reinterpret_cast<sockaddr*>(&client_name), &client_name_len);
        /* メッソージを受け取る */
        client_sent_quit_message = server(client_socket_fd);
        /* 切断する*/
        close(client_socket_fd);
    } while (!client_sent_quit_message);
    /* ソケットを閉じる */
    close(socket_fd);
    unlink(socket_name.c_str());

    return EXIT_SUCCESS;
}
process_b.cpp
#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<string>
#include<sys/socket.h>
#include<sys/un.h>
#include<unistd.h>

/* Write TEXT to the socket given by file descriptor SOCKET_FD. */
int write_text(const int& socket_fd, const std::string& message)
{
    /* Write the string. */
    write(socket_fd, message.c_str(), message.size() + 1);
    return 0;
}

int main ()
{
    const std::string socket_name("my_socket");
    const std::string message = "Hello World!!";
    int socket_fd;
    sockaddr_un name;
    /* ソケットを作成する */
    socket_fd = socket(PF_LOCAL, SOCK_STREAM, 0);
    /* ソケット名を設定 */
    name.sun_family = AF_LOCAL;
    strcpy(name.sun_path, socket_name.c_str());
    /* ソケットを接続 */
    connect(socket_fd, reinterpret_cast<sockaddr*>(&name), SUN_LEN (&name));
    /* メッセージを送信 */
    write_text(socket_fd, message);
    close(socket_fd);
    return EXIT_SUCCESS;
}

結論

ざっと、Linuxで提供されているIPCの方法を紹介してきました。
これだけ方法があるとどれを使ったらいいか迷うと思います。
そこがプログラマの腕の見せ所です。
どの方法も一長一短あります。ソフトウェアの制約の下、どれが最適な方法なのを考え選択していきましょう。

参考文献