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つだ。