Hello, I've been re-writing the polling mechanisms in the Erlang VM and stumbled across something that might be a bug in the OpenBSD implementation of kqueue.
When using EV_DISPATCH, the event is never triggered again after the EV_EOF flag has been delivered, even though there is more data to be read from the socket. I've attached a smallish program that shows the problem. The shortened ktrace output looks like this on OpenBSD 6.2: 29672 a.out 0.012883 CALL kevent(4,0x7f7ffffe8220,1,0,0,0) 29672 a.out 0.012888 STRU struct kevent { ident=5, filter=EVFILT_READ, flags=0x81<EV_ADD|EV_DISPATCH>, fflags=0<>, data=0, udata=0x0 } 29672 a.out 0.012895 RET kevent 0 29672 a.out 0.012904 CALL kevent(4,0,0,0x7f7ffffe7cf0,32,0) 29672 a.out 0.013408 STRU struct kevent { ident=5, filter=EVFILT_READ, flags=0x81<EV_ADD|EV_DISPATCH>, fflags=0<>, data=6, udata=0x0 } 29672 a.out 0.013493 RET kevent 1 29672 a.out 0.013548 CALL read(5,0x7f7ffffe8286,0x2) 29672 a.out 0.013562 RET read 2 29672 a.out 0.013590 CALL kevent(4,0x7f7ffffe8220,1,0,0,0) 29672 a.out 0.013594 STRU struct kevent { ident=5, filter=EVFILT_READ, flags=0x84<EV_ENABLE|EV_DISPATCH>, fflags=0<>, data=0, udata=0x0 } 29672 a.out 0.013608 RET kevent 0 29672 a.out 1.022228 CALL kevent(4,0,0,0x7f7ffffe7cf0,32,0) 29672 a.out 1.022537 STRU struct kevent { ident=5, filter=EVFILT_READ, flags=0x8081<EV_ADD|EV_DISPATCH|EV_EOF>, fflags=0<>, data=4, udata=0x0 } 29672 a.out 1.022572 RET kevent 1 29672 a.out 1.022663 CALL read(5,0x7f7ffffe8286,0x2) 29672 a.out 1.022707 RET read 2 29672 a.out 1.022816 CALL kevent(4,0x7f7ffffe8220,1,0,0,0) 29672 a.out 1.022822 STRU struct kevent { ident=5, filter=EVFILT_READ, flags=0x84<EV_ENABLE|EV_DISPATCH>, fflags=0<>, data=0, udata=0x0 } 29672 a.out 1.022835 RET kevent 0 29672 a.out 2.032238 CALL kevent(4,0,0,0x7f7ffffe7cf0,32,0) 29672 a.out 5.277194 PSIG SIGINT SIG_DFL In this example I would have expected the last kevent call to return with EV_EOF and data set to 2, but it does not trigger again. If I don't use EV_DISPATCH, the event is triggered again and the program terminates. Does anyone know if this is the expected behavior or a bug? I've worked around this issue by using EV_ONESHOT instead of EV_DISPATCH on OpenBSD for now, but would like to use EV_DISPATCH in the future as I've found that it aligns better with the abstractions that I use, and could possibly be a little bit more performant. Lukas PS. If relevant, it seems like FreeBSD does behave the way that I expected, i.e. it triggers again for EV_DISPATCH after EV_EOF has been shown. DS.
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <stdint.h> #include <err.h> #include <sys/event.h> #include <sys/socket.h> #include <sys/un.h> #include <netdb.h> #include <stdio.h> #include <stdarg.h> #include <string.h> #define USE_DISPATCH 1 int main() { struct addrinfo *addr; struct addrinfo hints; int kq, listen_s, fd = -1; struct kevent evSet; struct kevent evList[32]; /* open a TCP socket */ memset(&hints, 0, sizeof hints); hints.ai_family = PF_UNSPEC; /* any supported protocol */ hints.ai_flags = AI_PASSIVE; /* result for bind() */ hints.ai_socktype = SOCK_STREAM; /* TCP */ int error = getaddrinfo ("127.0.0.1", "8080", &hints, &addr); if (error) errx(1, "getaddrinfo failed: %s", gai_strerror(error)); listen_s = socket(addr->ai_family, addr->ai_socktype, addr->ai_protocol); if (setsockopt(listen_s, SOL_SOCKET, SO_REUSEADDR, &(int){ 1 }, sizeof(int)) < 0) errx(1, "setsockopt(SO_REUSEADDR) failed"); bind(listen_s, addr->ai_addr, addr->ai_addrlen); listen(listen_s, 5); kq = kqueue(); system("echo -n abcdef | nc -v -w 1 127.0.0.1 8080 &"); EV_SET(&evSet, listen_s, EVFILT_READ, EV_ADD, 0, 0, NULL); if (kevent(kq, &evSet, 1, NULL, 0, NULL) == -1) err(1, "kevent"); while(1) { int i; int nev = kevent(kq, NULL, 0, evList, 32, NULL); for (i = 0; i < nev; i++) { if (evList[i].ident == listen_s) { struct sockaddr_storage addr; socklen_t socklen = sizeof(addr); if (fd != -1) close(fd); fd = accept(evList[i].ident, (struct sockaddr *)&addr, &socklen); printf("accepted %d\n", fd); #if USE_DISPATCH EV_SET(&evSet, fd, EVFILT_READ, EV_ADD|EV_DISPATCH, 0, 0, NULL); #else EV_SET(&evSet, fd, EVFILT_READ, EV_ADD, 0, 0, NULL); #endif if (kevent(kq, &evSet, 1, NULL, 0, NULL) == -1) err(1, "kevent"); } else { if (evList[i].flags & EV_EOF && evList[i].data == 0) { printf("closing %d\n", fd); close(fd); fd = -1; exit(0); } else if (evList[i].filter == EVFILT_READ) { char buff[2]; read(fd, buff, 2); if (evList[i].flags & EV_EOF) printf("EOF: "); printf("read %c%c from %d\n", buff[0], buff[1], fd); #if USE_DISPATCH EV_SET(&evSet, fd, EVFILT_READ, EV_ENABLE|EV_DISPATCH, 0, 0, NULL); if (kevent(kq, &evSet, 1, NULL, 0, NULL) == -1) err(1, "kevent"); #endif sleep(1); } } } } }