Search code examples
kqueue

Use kqueue to respond to more than one event type


When an event is registered with kqueue an ID relating to that event type is supplied; for example a file descriptor is used to identify a file to watch

int kq;
struct kevent ke;

kq = kqueue();
fd = open(argv[1], O_RDONLY);
EV_SET(&ke, fd, EVFILT_VNODE, EV_ADD, NOTE_DELETE | NOTE_RENAME, 0, NULL);
kevent(kq, &ke, 1, NULL, 0, NULL);

while (1) {
    kevent(kq, NULL, 0, &ke, 1, NULL);
    /* respond to file system event */
}

Now if I also need to respond to other event types such signals we need a new instance of kqueue so as to avoid a conflict with the ident argument of kevent().

kq_sig = kqueue();
struct kevent ke_sig;

/* set the handler and ignore SIGINT */
signal(SIGINT, SIG_IGN);
EV_SET(&ke_sig, SIGINT, EVFILT_SIGNAL, EV_ADD, 0, 0, NULL);
kevent(kq_sig, &ke_sig, 1, NULL, 0, NULL);
while (1) {
    kevent(kq_sig, NULL, 0, &ke_sig, 1, NULL);
    /* respond signals */
}

Watching more than one event type appears to necessitate multiple threads that act on shared state (receiving a signal could close a file descriptor for example).

Is there a more general mechanism for sending a message from one thread to another using kqueue? In some cases I can conceive of enabling and disabling a filter as a means of edge-triggering another kevent.


Solution

  • The kevent struct actually provides info about the event that occured:

    struct kevent {
             uintptr_t       ident;          /* identifier for this event */
             int16_t         filter;         /* filter for event */
             uint16_t        flags;          /* general flags */
             uint32_t        fflags;         /* filter-specific flags */
             intptr_t        data;           /* filter-specific data */
             void            *udata;         /* opaque user data identifier */
     };
    

    You must be interested in:

    • ident that in your case returns either fd or SIGINT;
    • filter that (still in your case) returns either EVFILT_VNODE or EVFILT_SIGNAL;
    • fflag that in the EVFILT_VNODE will tell you if the file descriptor event was NOTE_DELETE or NOTE_RENAME.

    You can register two kevent structures to a single queue and then use these structure members to determine if the event was related to a file descriptor or a signal.

    Here is a complete example that demonstrates how to do this:

    #include <stdlib.h>
    #include <stdio.h>
    #include <fcntl.h>
    #include <string.h>
    #include <sys/types.h>
    #include <sys/event.h>
    #include <sys/time.h>
    
    int
    main(int argc, char** argv)
    {
        /* A single kqueue */
        int kq = kqueue();
        /* Two kevent structs */
        struct kevent *ke = malloc(sizeof(struct kevent) * 2);
    
        /* Initialise one struct for the file descriptor, and one for SIGINT */
        int fd = open(argv[1], O_RDONLY);
        EV_SET(ke, fd, EVFILT_VNODE, EV_ADD | EV_CLEAR, NOTE_DELETE | NOTE_RENAME, 0, NULL);
        signal(SIGINT, SIG_IGN);
        EV_SET(ke + 1, SIGINT, EVFILT_SIGNAL, EV_ADD, 0, 0, NULL);
    
        /* Register for the events */
        if(kevent(kq, ke, 2, NULL, 0, NULL) < 0)
            perror("kevent");
    
        while(1) {
            memset(ke, 0x00, sizeof(struct kevent));
            if(kevent(kq, NULL, 0, ke, 1, NULL) < 0)
                perror("kevent");
    
            switch(ke->filter)
            {
                /* File descriptor event: let's examine what happened to the file */
                case EVFILT_VNODE:
                    printf("Events %d on file descriptor %d\n", ke->fflags, (int) ke->ident);
    
                    if(ke->fflags & NOTE_DELETE)
                        printf("The unlink() system call was called on the file referenced by the descriptor.\n");
                    if(ke->fflags & NOTE_WRITE)
                        printf("A write occurred on the file referenced by the descriptor.\n");
                    if(ke->fflags & NOTE_EXTEND)
                        printf("The file referenced by the descriptor was extended.\n");
                    if(ke->fflags & NOTE_ATTRIB)
                        printf("The file referenced by the descriptor had its attributes changed.\n");
                    if(ke->fflags & NOTE_LINK)
                        printf("The link count on the file changed.\n");
                    if(ke->fflags & NOTE_RENAME)
                        printf("The file referenced by the descriptor was renamed.\n");
                    if(ke->fflags & NOTE_REVOKE)
                        printf("Access to the file was revoked via revoke(2) or the underlying fileystem was unmounted.");
                    break;
    
                /* Signal event */
                case EVFILT_SIGNAL:
                    printf("Received %s\n", strsignal(ke->ident));
                    exit(42);
                    break;
    
                /* This should never happen */
                default:
                    printf("Unknown filter\n");
            }
        }
    }
    

    Note that here we use a single thread, which is way more efficient and requires no further synchronization in the user space.