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