jump to navigation

仮想デバイスドライバを利用したプロセス間通信について September 11, 2006

Posted by butcher in : C, Operating System , trackback

仮想デバイスドライバを利用したプロセス間通信について説明します。といってもよくわからないと思うので、ちゃんと説明します。
Unixでプロセス間通信というと、ソケットを使ったもの、パイプを使ったもの、共有メモリを使ったもの等がありますが、それぞれ長所・短所があると思います。
ものすごく簡単に言うと、
ソケットでは、

  • 複数マシン間での通信が可能
  • 通信処理のオーバーヘッドが大きい(コネクション開始・終了処理も含め)
  • パイプでは、

  • ソケットより通信処理のオーバーヘッドが少ない
  • 親子関係のプロセスに限定される
  • 共有メモリでは、

  • シンプルで高速
  • 書き込み・読み取りの同期をとるのが難しい
  • 等が挙げられると思います。
    共有メモリがシンプルかつ高速ですばらしいのですが、アプリケーションレベルで同期をとるのが難しいという問題があります。そこで、ここでは、共有メモリによるプロセス間通信における同期機構を、仮想デバイスドライバを使って行う手法を説明しようと思います。

    開発環境は、FedoraCore4(2.6.11-1.1369_FC4)、gcc 4.0.0、GNU make 3.80です。

    Linuxのデバイスドライバについては、私も詳しくないですが、以下のページが参考になると思いますので、書いたことのない方は一度見てみてください。

    The Linux Kernel Module Programming Guide
    Linux Kernel Module programming(Kernel 2.4)

    概要

    同期の仕組みですが、デバイスドライバにpoll関数を実装し、そこで読み込みと書き込みの待ち列を設定します。書き込み(write)後に読み込み可能になったら読み込みの待ち列に対して、wake_upを実行します。ユーザ側のプロセスでは、デバイスドライバをselect(2)で待ち(poll関数が呼び出される)、読み込み可能になり次第、カーネルから起こされ読み込み処理を実行できるようになります。 書き込みも同じ原理で動きます。しかし、読み込みと書き込みは同時には実行できません。(当たり前ですが。)

    実装

    次に、実際のソースについて解説します。デバイスドライバといっても、本物のデバイスを操作するわけではなく、同期を取るだけで実際の通信データも扱わないので非常に単純です。ソースはこちらです。(sync.c)

    21~27行目

    #define DEV_NAME "sync"              // デバイスの名前
    #define SYNC_MAJOR 100              // デバイスのメジャー番号
    
    static int is_readable = 0;              // デバイスが読み取り可能かのフラグ
    static struct semaphore sync_sem;  // セマフォ
    static wait_queue_head_t write_q;   // 書き込みのウェイトキュー
    static wait_queue_head_t read_q;   // 読み込みのウェイトキュー
    

    ここでは、デバイス全体から呼ばれる変数等を定義しています。デバイスの名前を”sync”、メジャー番号を100番としています。その他の内容はコメントの通りです。

    29~37行目

    static struct file_operations sync_fops =
    {
        owner   : THIS_MODULE,
        read    : syncread,
        write   : syncwrite,
        poll    : syncpoll,
        open    : syncopen,
        release : syncclose,
    };
    

    各操作(read, write, …)に対応する関数を登録しています。

    39~60行目

    int init_module(void)
    {
        if (register_chrdev(SYNC_MAJOR, DEV_NAME, &sync_fops)) {
            printk(KERN_INFO "register_chrdev failed¥n");
            return -EBUSY;
        }
    
        sema_init(&sync_sem, 1);
        init_waitqueue_head(&write_q);
        init_waitqueue_head(&read_q);
    
        return 0;
    }
    
    void
    cleanup_module(void)
    {
        if (unregister_chrdev(SYNC_MAJOR, DEV_NAME) ) {
            printk(KERN_INFO "unregister_chrdev failed¥n");
        }
    }
    

    モジュールの組み込み(insmod)と削除(rmmod)時に呼び出される関数です。init_moduleでは、キャラクデバイスの登録、(バイナリ)セマフォの設定、書き込みと読み込みの待ち列の初期化を行い、cleanup_moduleでは、キャラクタデバイスの登録解除を行っています。

    62~72行目

    static int
    syncopen(struct inode* inode, struct file* filp)
    {
        return 0;
    }
    
    static int
    syncclose(struct inode* inode, struct file* filp)
    {
        return 0;
    }
    

    open、close時に呼び出される関数です。今回は特に行うことはないので、0(正常終了)を返すだけにしています。

    74~88行目

    static ssize_t
    syncread(struct file* filp, char* buf, size_t count, loff_t* pos)
    {
        if (down_interruptible(&sync_sem)) {
            printk(KERN_INFO "down_interruptible for read failed¥n");
            return -ERESTARTSYS;
        }
    
        is_readable = 0;
    
        up(&sync_sem);
        wake_up_interruptible(&write_q);
    
        return 0;
    }
    

    read時に呼び出される関数です。セマフォを取得し、読み込み可能フラグ(is_readable)を0(不可)に設定しています。(今回はセマフォは特に必要ありません。)その後、書き込みの待ち列に対して、wake_up_interruptibleを実行し、書き込み可能状態にしています。

    90~104行目

    static ssize_t
    syncwrite(struct file* filp, const char* buf, size_t count, loff_t* pos)
    {
        if (down_interruptible(&sync_sem)) {
            printk(KERN_INFO "down_interruptible for write failed¥n");
            return -ERESTARTSYS;
        }
    
        is_readable = 1;
    
        up(&sync_sem);
        wake_up_interruptible(&read_q);
    
        return 0;
    }
    

    write時に呼び出される関数です。今度は読み込み可能フラグ(is_readable)を1(可)に設定しています。writeと同様に、wake_up_interruptibleを読み込みの待ち列に対して実行し、読み込み可能状態にしています。

    106~122行目

    static unsigned int
    syncpoll(struct file* filp, poll_table* wait)
    {
        unsigned int retmask = 0;
    
        poll_wait(filp, &read_q,  wait);
        poll_wait(filp, &write_q, wait);
    
        if (is_readable) {
            retmask |= (POLLIN | POLLRDNORM);
        }
        if (!is_readable) {
            retmask |= (POLLOUT | POLLWRNORM);
        }
    
        return retmask;
    }
    

    ユーザプロセスからのselect(2)や、wake_up_interruptible後に呼び出される関数です。読み込みと(read_q)と書き込み(write_q)の待ち列をセットし、読み込み可能なら(POLLIN | POLLRDNORM)、書き込み可能なら(POLLOUT | POLLWRNORM)を返します。selectしているユーザプロセスはこの戻り値によって、読み込可能、書き込み可能を判断しています。

    コンパイル

    ソースのコンパイルは以下のMakefileを使います。

    obj-m := sync.o
    
    all:
        make -C /lib/modules/$(shell uname -r)/build M=`pwd` V=1 modules
    
    clean:
        make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
    

    (※)カーネル2.6用の書式なので、2.4系等をお使いの方は書き換えてください。

    % make
    make -C /lib/modules/2.6.11-1.1369_FC4/build M=`pwd` V=1 modules
    ...
    

    モジュールをカーネルに組み込む

    コンパイルがうまくいくと、モジュールファイルsynk.koができていると思います。後は組み込んで、デバイスファイルを作るだけです。

    % sudo /sbin/insmod sync.ko
    % sudo mknod /dev/sync c 100 0
    

    以上で、プロセス間同期用のデバイスファイルの作成と組み込みは終わりです。

    動作テスト

    次は実際に、書き込み、読み込みの2プロセス間でプロセス間通信をさせてみます。
    まずは書き込みのプログラムです。(map_write.c)

    21〜40行目

        pagesize = sysconf(_SC_PAGE_SIZE);
        mapsize = ((BUFSIZE-1)/pagesize+1) * pagesize;
    
        if((fd = open("./map.shm", O_RDWR|O_CREAT, 0666)) == -1) {
            perror("open map");
            exit(-1);
        }
        if ((dev = open("/dev/sync", O_RDWR)) == -1) {
            perror("open sync");
            exit(-1);
        }
        if (ftruncate(fd, mapsize) == -1) {
            perror("ftruncate");
            exit(-1);
        }
        map = (char *) mmap(0, mapsize, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
        if (map == MAP_FAILED) {
            perror("mmap");
            exit(-1);
        }
    

    mmapにより共有メモリを確保するため、確保するサイズをOSのページサイズに合わせ、共有メモリ用のファイルと同期用のデバイスファイルを開きます。共有メモリ用のファイルはmmapするサイズに合わせた領域を確保し、その領域をmmapで共有メモリにしています。

    42〜57行目

        FD_ZERO(&wfds);
        FD_SET(dev, &wfds);
        printf("selecting ...¥n");
        if (select(dev+1, NULL, &wfds, NULL, NULL) == -1) {
           perror("select");
           exit(-1);
        }
        printf("wait released.¥n");
    
        if (FD_ISSET(dev, &wfds)) {
            strncpy(map, message, mapsize);
            if (write(dev, NULL, 1) == -1) {
                perror("write");
                exit(-1);
            }
        }
    

    デバイスファイルに対して書き込みを待てるように設定し、selectで待機します。書き込み可能になると待ちから復帰し、共有メモリにメッセージを書き、書き込みが完了したことを通知するために、デバイスファイルに書き込みます。

    次に読み込み側です。(map_read.c)
    32〜49行目

        map = (char *) mmap(0, mapsize, PROT_READ,  MAP_SHARED, fd, 0);
        if (map == MAP_FAILED) {
            perror("mmap");
            return -1;
        }
    
        FD_ZERO(&rfds);
        FD_SET(dev, &rfds);
    
        printf("selecting ...¥n");
        if (select(dev+1, &rfds, NULL, NULL, NULL) == -1) {
            perror("select");
        }
    
        if (FD_ISSET(dev, &rfds)) {
            printf("get data from mapped area [%s]¥n", map);
            read(dev, buf, 256);
        }
    

    書き込み時に確保した共有メモリ領域を読み取り専用でmmapします。その後、デバイスファイルに対して読み込みを待てるように設定し、selectで待機します。
    デバイスファイルへの書き込みが読み込みの待ち列を起こし、待ちから復帰したら共有メモリからデータを受け取り、読み込みが完了したことを通知するために、デバイスファイルから読み込みをそれにより、また書き込み可能となります。

    コンパイルし、実際に動かしてみます。

    % gcc map_write.c -o map_write
    % gcc map_read.c -o map_read
    

    読み込みプロセスを実行すると、書き込みがあるまで待ちます。

    % ./map_read
    selecting ...
    

    書き込みを実行すると、書き込み可能なため待つことなく、書き込みを実行し、その直後に
    読み込み可能となり、共有メモリからのデータを受け取ることができます。

    % sudo ./map_write
    selecting ...
    wait released.
    
    % ./map_read
    selecting ...
    get data from mapped area [Hello, are you getting my request ?]
    

    まとめ

    今回は、共有メモリにおけるプロセス間通信の欠点を補うような同期機構を、仮想デバイスファイルを利用することによって実現しました。この機構を利用すれば、同一マシン間での高速なプロセス間のデータ受け渡しが可能になるので、パフォーマンスが重要になってくるようなアプリケーションで役に立つかもしれません。

    補足

    2007.09.23
    この仕組みの利用方法についてhttp://cheesy.dip.jp/diary/archives/143で少し補足しています。

    Linuxデバイスドライバ 第3版 Linuxデバイスドライバ 第3版
    ジョナサン コルベット グレッグ クローハートマン アレッサンドロ ルビーニ

    詳解UNIXプログラミング 詳解UNIXプログラミング
    W.リチャード スティーヴンス W.Richard Stevens 大木 敦雄

    詳解Linuxカーネル 第2版 詳解Linuxカーネル 第2版
    ダニエル・P. ボベット マルコ セサティ Daniel P. Bovet

    プログラミング言語C ANSI規格準拠 プログラミング言語C ANSI規格準拠
    B.W. カーニハン D.M. リッチー 石田 晴久



    Comments»

    1. ku - September 12, 2006

    おもしろそう!
    調べたら named pipe っていうのもあったよ。mknod() で作って普通にopen()で開いて読み書きできるらしい。

    2. no hacking, no life » 非同期メッセージングを実現するPOE::Component::MessageQueue を試してみた - April 3, 2007

    […] 非同期メッセージングを実現するPOE::Component::MessageQueueを使ってみました。まだほんの一機能しか使ってませんが、この使い勝手の良さはかなりいいと思います。非同期なメッセージキューはアプリケーション間の結合を疎にしたり、分散アプリケーションを作るときのメッセージバッファとしてかなり重宝するので、もっと深く見ていこうと思います。また、この辺の仕組みを自分で作りたいマニアックな人は、仮想デバイスドライバを利用したプロセス間通信についてが少し役に立つかもしれません。 […]

    3. no hacking, no life » 「仮想デバイスドライバを利用したプロセス間通信」の補足 - September 23, 2007

    […] 一年前に書いたtutorialogのエントリ「仮想デバイスドライバを利用したプロセス間通信について」がnaoyaさんのブログで言及されてから(?)、今更になってちょっとだけブックマークされだした。 内容は共有メモリを仮想デバイスドライバを使って同期をとるといいよ、という話なのだが、「flockでいいんじゃ」というコメントがはてぶにいくつかあったので、ここで補足しておこうと思います。 […]