Kqueue EVFILT_VNODE Example

kqfile.c

In this example we want to have something equivalent to tail -f on many files. We use EVFILT_VNODE to track changes such as file deletion or renaming and EVFILT_READ to track when data has been written to the file. The reason for using a separate EVFILT_READ kevent is that it will return the number of bytes to read, which may be negative when the file shrinks, so we can handle this case too.

We use this struct to represent our files:

#define NFILES 5

struct kqfile {
    int fd;         /* file descriptor                      */
    char *path;     /* full path, eg /var/log/messages      */
    char *name      /* last component of path, eg messages  */
} files[NFILES];

For each line we print will be prepended by name to indicate which file the line comes from, and path is the full name of the path if the file needs to be reopened.

For each file we register the following kevents:

/* set kevents for a file */
void
register_kevents(struct kqfile *f)
{
    struct kevent ke;

    EV_SET(&ke, f->fd, EVFILT_VNODE, EV_ADD,
        NOTE_DELETE | NOTE_RENAME, 0, f);

    if (kevent(kq, &ke, 1, NULL, 0, NULL) == -1)
        err(1, "kevent %s", f->path);

    EV_SET(&ke, f->fd, EVFILT_READ, EV_ADD, 0, 0, f);

    if (kevent(kq, &ke, 1, NULL, 0, NULL) == -1)
        err(1, "kevent %s", f->name);
}

we use EVFILT_VNODE for when the file is deleted or renamed and EVFILT_READ for when there is data ready to be read from the file. Note also that we pass in a pointer as the last argument which is the udata field in struct kevent and is untouched by the kernel.

This is the code for processing the kevents:

i = kevent(kq, NULL, 0, &ke, 1, NULL);
if (i == -1)
    err(1, "kevent");

f = (struct kqfile *)ke.udata;

When the blocking kevent call returns, we take the value of ke.udata which will point to the struct kqfile for the file that triggered this event.

/* data to read */
if (ke.filter == EVFILT_READ) {

    if (ke.data < 0) {
        printf("%s has shrunk\n", f->path);
        lseek(f->fd, 0, SEEK_END);
        continue;
    }

    i = rwfile(f, buf, sizeof(buf), ke.data);
    if (i == -1)
        warn("read %s", f->path);

Since we’re using two different filter types we need to check the value to work out what to do. In this case there is data to be read and ke.data will specify. If the file has shrunk for some reason, we use lseek to reset the offset to the end of file. Otherwise we call rwfile which will read the new data and write it to stdout.

} else if (ke.filter == EVFILT_VNODE &&
    (ke.fflags & NOTE_DELETE || ke.fflags & NOTE_RENAME)) {
        if (kqfreopen(f) == -1) {
            warn("%s went away and didn't come back",
                f->path);
            kqfremove(f);
        }
}

Here the file has either been deleted or renamed. This will happen if tailing logfiles that are rotated every day, for example moving /var/log/messages to /var/log/messages.1. If this occurs, kqfreopen will try 3 times to reopen the file, sleeping for a bit to give other processes which might be trying to create the files a chance to run. If it is successful it will register the kevents on the new file descriptor using register_kevents(). If it can’t be reopened the file descriptor is closed and no longer tracked.