開発環境 C言語(その3)

【連載】

にわか管理者のためのLinux運用入門

【第158回】開発環境 C言語(その3)

[2018/12/25 12:20]後藤大地 ブックマーク ブックマーク

サーバ/ストレージ

Linuxカーネルに特有の機能を使う

前回までで、C言語で開発できる最低限の環境を整えてきた。今回はLinuxカーネルに特有の機能を使ってプログラムを作り、コンパイルして実行する方法を紹介する。

コマンドやユーティリティ、アプリケーションは特定の処理に関してはカーネルに処理を依頼する必要がある。カーネルに処理を依頼する場合には「システムコール」と呼ばれる関数を呼び出す。利用している関数がシステムコールなのか否かは、利用する側としては特に気にする必要はないが、「いくつかの関数はシステムコールと呼ばれ、カーネルに直接処理を依頼するものだ」ということだけ知っておいていただきたい。

システムコールはカーネルに特有の処理だ。UNIX系OSのカーネルは大体共通のシステムコールを持っている。しかし、いくつかのシステムコールはそれぞれのカーネルに特有のもので、ほかのカーネルは持っていない。C言語でプログラムを書くことで、こうした個々のカーネルに特有の処理を自由に利用できる。シェルスクリプトやインタプリタ言語を使わずにC言語を利用するモチベーションの1つだ。

システムコール「inotify(2)系」

Linuxカーネルはファイルシステムイベントをモニタリングする機能としてinotify(2)系のシステムコールを提供している。inotify(2)系のシステムコールを利用することで、ディレクトリやファイルをモニタリングして、イベントが発生した場合に処理を行うといったことができるようになる。

例えば、ディレクトリやファイルをモニタリングし、ファイルやディレクトリに何らかの変更が発生するまで処理を停止する、といったコマンドを開発することができる(ログファイルに書き込みがあるまで待機する、など)。ファイルやディレクトリが変更されたかどうかを定期的かつ自動的に(例えば1秒ごとに)調べていくような仕組みを作る必要がなく、inotify(2)系のシステムコールを利用するだけで事足りる。自動的に調べに行く方法よりも軽量で高速だし、正確だ。

C言語を使った開発経験がない場合、こうしたシステムコールを使ったコーディングは敷居が高いと思う。こういった場合にはまずサンプルコードをコンパイルして利用する方法を試してみるとよい。

inotify(2)系のシステムコールに関してはマニュアルに良いサンプルコードが掲載されているので、これを利用するとよいだろう。「man inotify」で表示されるマニュアルに掲載されている次のコードをそのまま使ってみよう。

#include <errno.h>
#include <poll.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/inotify.h>
#include <unistd.h>

/* Read all available inotify events from the file descriptor 'fd'.
   wd is the table of watch descriptors for the directories in argv.
   argc is the length of wd and argv.
   argv is the list of watched directories.
   Entry 0 of wd and argv is unused. */

static void
handle_events(int fd, int *wd, int argc, char* argv[])
{
    /* Some systems cannot read integer variables if they are not
       properly aligned. On other systems, incorrect alignment may
       decrease performance. Hence, the buffer used for reading from
       the inotify file descriptor should have the same alignment as
       struct inotify_event. */

    char buf[4096]
        __attribute__ ((aligned(__alignof__(struct inotify_event))));
    const struct inotify_event *event;
    int i;
    ssize_t len;
    char *ptr;

    /* Loop while events can be read from inotify file descriptor. */

    for (;;) {

        /* Read some events. */

        len = read(fd, buf, sizeof buf);
        if (len == -1 && errno != EAGAIN) {
            perror("read");
            exit(EXIT_FAILURE);
        }

        /* If the nonblocking read() found no events to read, then
           it returns -1 with errno set to EAGAIN. In that case,
           we exit the loop. */

        if (len <= 0)
            break;

        /* Loop over all events in the buffer */

        for (ptr = buf; ptr < buf + len;
                ptr += sizeof(struct inotify_event) + event->len) {

            event = (const struct inotify_event *) ptr;

            /* Print event type */

            if (event->mask & IN_OPEN)
                printf("IN_OPEN: ");
            if (event->mask & IN_CLOSE_NOWRITE)
                printf("IN_CLOSE_NOWRITE: ");
            if (event->mask & IN_CLOSE_WRITE)
                printf("IN_CLOSE_WRITE: ");

            /* Print the name of the watched directory */

            for (i = 1; i < argc; ++i) {
                if (wd[i] == event->wd) {
                    printf("%s/", argv[i]);
                    break;
                }
            }

            /* Print the name of the file */

            if (event->len)
                printf("%s", event->name);

            /* Print type of filesystem object */

            if (event->mask & IN_ISDIR)
                printf(" [directory]\n");
            else
                printf(" [file]\n");
        }
    }
}

int
main(int argc, char* argv[])
{
    char buf;
    int fd, i, poll_num;
    int *wd;
    nfds_t nfds;
    struct pollfd fds[2];

    if (argc < 2) {
        printf("Usage: %s PATH [PATH ...]\n", argv[0]);
        exit(EXIT_FAILURE);
    }

    printf("Press ENTER key to terminate.\n");

    /* Create the file descriptor for accessing the inotify API */

    fd = inotify_init1(IN_NONBLOCK);
    if (fd == -1) {
        perror("inotify_init1");
        exit(EXIT_FAILURE);
    }

    /* Allocate memory for watch descriptors */

    wd = calloc(argc, sizeof(int));
    if (wd == NULL) {
        perror("calloc");
        exit(EXIT_FAILURE);
    }

    /* Mark directories for events
       - file was opened
       - file was closed */

    for (i = 1; i < argc; i++) {
        wd[i] = inotify_add_watch(fd, argv[i],
                                  IN_OPEN | IN_CLOSE);
        if (wd[i] == -1) {
            fprintf(stderr, "Cannot watch '%s'\n", argv[i]);
            perror("inotify_add_watch");
            exit(EXIT_FAILURE);
        }
    }

    /* Prepare for polling */

    nfds = 2;

    /* Console input */

    fds[0].fd = STDIN_FILENO;
    fds[0].events = POLLIN;

    /* Inotify input */

    fds[1].fd = fd;
    fds[1].events = POLLIN;

    /* Wait for events and/or terminal input */

    printf("Listening for events.\n");
    while (1) {
        poll_num = poll(fds, nfds, -1);
        if (poll_num == -1) {
            if (errno == EINTR)
                continue;
            perror("poll");
            exit(EXIT_FAILURE);
        }

        if (poll_num > 0) {

            if (fds[0].revents & POLLIN) {

                /* Console input is available. Empty stdin and quit */

                while (read(STDIN_FILENO, &buf, 1) > 0 && buf != '\n')
                    continue;
                break;
            }

            if (fds[1].revents & POLLIN) {

                /* Inotify events are available */

                handle_events(fd, wd, argc, argv);
            }
        }
    }

    printf("Listening for events stopped.\n");

    /* Close inotify file descriptor */

    close(fd);

    free(wd);
    exit(EXIT_SUCCESS);
}

ちょっと長いと感じるかもしれないが、このサンプルコードにはinotify(2)系のシステムコールを利用するためのエッセンスがたくさん入っている。実際に使うときにはここから必要なところを抜き出して、自分なりに応用しながら使っていけばよいと思う。順次説明していくので、今はまだよくわからなくても大丈夫だ。

コンパイルと実行

オンラインマニュアルに掲載されているサンプルコードを「tryinotify.c」というファイルに保存したとする。この場合、次のようにccコマンドを実行すれば「tryinotify」というバイナリが生成される。

cc try_inotify.c -o try_inotify

前回紹介したMakefileを次のように編集して利用してもよい。

SRCS=   try_inotify.c
CMD=    try_inotify

OBJS=   ${SRCS:.c=.o}

all: ${OBJS}
    cc -o ${CMD} ${OBJS}

.SUFFIXES: .c .c

.c.o:
    cc -c $<

clean:
    rm -f ${OBJS} ${CMD}

このMakefileを使うなら、makeでコンパイル(ビルド)が実行される。

では、生成されたバイナリファイル(try_inotify)を実行してみよう。すると、次のように引数にモニタリング対象のファイルやディレクトリの指定が求められていることがわかるので、空の「log」というファイルを作成して引数に指定すると、モニタリングが開始されることを確認できる。

try_inotifyを実行。ファイルを指定してもモニタリングを開始

tryinotifyを実行しているのとは別のターミナルアプリケーションで先ほど作成したlogファイルに対し、次のようにして文字列を書き込むと、tryinotifyコマンドからメッセージが出力されることを確認できる。

echo log_message > log

try_inotifyがlogファイルの変更を検出してメッセージを取得している

次のようにモニタリング対象のファイルを削除してみても、try_inotifyコマンドがlogファイルに発生したイベントを検出して出力していることを確認できる。

rm log

try_inotifyが削除の処理も検出していることがわかる

なお、いったんモニタリング対象のファイルを削除すると、同じ名前のファイルを作り直してももうモニタリングの対象にはならない。なぜなら、ファイルディスクリプタが変わってしまうためだ。ファイルを新しく作ったなら、try_inotifyコマンドも一度終了してから改めて実行する必要がある。

小さく作って試していこう

C言語のコーディングをしたことがない方には、try_inotify.cはかなり難解なコードかもしれない。しかし、明示的にシステムコールを意図したコーディングを行う際の参考にするにはなかなかよい素材なので、本連載では数回に分けて、このソースコードについて説明していきたいと思う。大切なのはいきなりすべてを理解しようとしないで、小さく小さく理解と実践を重ねていくことだ。その積み重ねは、そのままスキルとして身に付きやすい。

ちなみに、inotify(2)系のシステムコールはLinuxカーネルに特有のものだが、ほかのカーネルにはほかのカーネルで同様の機能を実現するためのシステムコールが用意されている。代表的なものとしては、macOSやFreeBSDカーネルのkqueue(2)システムコールが挙げられる。kqueueも独自のシステムコールだが、*BSD系のカーネルでは実装されている。性能が期待できるのでパフォーマンスを求めるプログラムではすでに長きにわたって使われており、有名なシステムコールの1つだ。

※ 本記事は掲載時点の情報であり、最新のものとは異なる場合がございます。予めご了承ください。

もっと知りたい!こちらもオススメ

なぜ今、統合システムなのか? 押さえておくべき「3つのインパクト」

なぜ今、統合システムなのか? 押さえておくべき「3つのインパクト」

ガートナー ジャパンは10月31日~11月2日、都内で「Gartner Symposium/ITxpo 2017」を開催。11月1日には同社 主席アナリストの青山浩子氏が登壇し「CIOが理解すべき統合システムの3大インパクト」と題する講演を行った。本稿では、講演の内容をダイジェストでお届けする。

関連リンク

この記事に興味を持ったら"いいね!"を Click
Facebook で IT Search+ の人気記事をお届けします

会員登録(無料)

注目の特集/連載
知りたい! カナコさん 皆で話そうAIのコト
教えてカナコさん! これならわかるAI入門
Kubernetes入門
RPA入門
ソフトウェア開発自動化入門
PowerShell Core入門
Swagger 3.0 入門
徹底研究! ハイブリッドクラウド
マイナビニュース スペシャルセミナー 講演レポート/当日講演資料 まとめ
セキュリティアワード特設ページ

一覧はこちら

今注目のIT用語の意味を事典でチェック!

一覧はこちら

ページの先頭に戻る