Today I read Async Ruby is the future article about Async Ruby and the event loop. It gave a great overview of how Ruby's async gem uses Fibers and an event loop to handle I/O without blocking.
The article got me curious about how the operating system actually notifies the Ruby runtime when a Fiber is ready to resume. I dug deeper and learned about epoll_ctl
/ epoll_wait
on Linux, and their macOS equivalent, kqueue
and kevent
.
With some help from ChatGPT, I wrote a small C demo using kevent to wait for STDIN input asynchronously. It was a good exercise to understand how these lower-level primitives work under the hood.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/event.h>
#include <sys/time.h>
#include <fcntl.h>
int main() {
int kq = kqueue();
if (kq == -1) {
perror("kqueue");
exit(1);
}
struct kevent change;
EV_SET(&change, STDIN_FILENO, EVFILT_READ, EV_ADD | EV_ENABLE, 0, 0, NULL);
printf("Waiting for input (kqueue)...\n");
struct kevent event;
int nev = kevent(kq, &change, 1, &event, 1, NULL); // NULL = block forever
if (nev == -1) {
perror("kevent");
exit(1);
}
if (event.filter == EVFILT_READ) {
char buffer[1024];
ssize_t n = read(STDIN_FILENO, buffer, sizeof(buffer) - 1);
if (n > 0) {
buffer[n] = '\0';
printf("Read from STDIN: %s\n", buffer);
}
}
close(kq);
return 0;
}
This gave me a deeper appreciation for the abstractions that the OS kernel provides to user-space programs—and how libraries like async in Ruby build on top of that.