Fundamentals 35 min read

Master the Linux fasync Mechanism and Understand Signal‑Driven Asynchronous Notification

This article explains the Linux kernel fasync mechanism, compares it with other I/O models, details the underlying data structures and key functions, and provides step‑by‑step driver and user‑space code examples for implementing signal‑driven asynchronous notifications in embedded and Linux driver development.

Deepin Linux
Deepin Linux
Deepin Linux
Master the Linux fasync Mechanism and Understand Signal‑Driven Asynchronous Notification

1. Linux Kernel Asynchronous Notification

Traditional blocking I/O puts a process to sleep, while non‑blocking polling wastes CPU cycles by repeatedly checking device status. Linux solves these problems with the fasync asynchronous notification mechanism, which uses signals to let the kernel actively push events to user space, eliminating inefficient polling.

1.1 What is asynchronous notification?

In the kernel, asynchronous notification works like a messenger: when an event occurs, the kernel sends a signal to the interested process, similar to a hardware interrupt delivering a request to the CPU.

1.2 Comparison with other I/O models

Blocking I/O blocks the process until the operation completes, wasting resources.

Non‑blocking polling requires the process to repeatedly query the device, consuming CPU.

Asynchronous notification (fasync) lets the kernel send a signal when data is ready, allowing the process to perform other work meanwhile.

2. Linux Signal Mechanism

Signals are the kernel’s asynchronous communication method. Common signals include SIGHUP (1), SIGINT (2), SIGQUIT (3), SIGKILL (9) and SIGTERM (15). For asynchronous I/O, the dedicated signal is SIGIO .

2.1 Asynchronous‑notification‑specific signal: SIGIO

When a file descriptor has asynchronous notification enabled, the kernel sends SIGIO to the bound process whenever the device becomes readable or writable.

2.2 Registering a signal handler

Applications register a handler with signal() or sigaction(). The signal() prototype is:

#include <signal.h>
void (*signal(int signum, void (*handler)(int)))(int);

Example for handling SIGINT:

#include <stdio.h>
#include <signal.h>
#include <stdlib.h>

void sigint_handler(int signum) {
    printf("Caught signal %d, exiting gracefully.
", signum);
    exit(0);
}

int main() {
    if (signal(SIGINT, sigint_handler) == SIG_ERR) {
        perror("signal");
        return 1;
    }
    printf("Program running, press Ctrl+C to exit.
");
    while (1) {
        sleep(1);
    }
    return 0;
}

2.3 Using sigaction() for advanced handling

The sigaction prototype is:

#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

It allows signal masking, flags, and a richer handler signature.

3. Understanding the fasync Mechanism

3.1 What is fasync?

fasync (file asynchronous) is the core of signal‑driven I/O in Linux. It lets the kernel notify user space with a signal (usually SIGIO) when a device becomes ready, avoiding blocking and polling.

3.2 Design idea

The design follows an “event‑arrives‑notify” strategy: when the device changes state, the kernel sends a signal, allowing the process to react immediately without busy‑waiting.

3.3 Typical use cases

High‑concurrency network servers: the kernel notifies when a socket has data, reducing CPU usage.

Device drivers (e.g., USB, serial ports): the driver pushes a signal when data is ready.

Embedded key‑press handling: an interrupt triggers a signal, enabling fast response without polling.

Hot‑plug detection for removable storage.

4. Deep Dive into fasync Internals

4.1 Key data structure: struct fasync_struct

struct fasync_struct {
    spinlock_t fa_lock;   // protects fields (may be absent in some kernels)
    int magic;            // debug magic number (FASYNC_MAGIC)
    int fa_fd;            // user‑space file descriptor (may be unused)
    struct fasync_struct *fa_next; // next entry in the list
    struct file *fa_file; // pointer to the associated file
    struct rcu_head fa_rcu; // RCU‑safe reclamation
};

The kernel links all processes that registered for a given device into a singly‑linked list via fa_next. When an event occurs, the kernel traverses this list to send signals.

4.2 Core functions

(1) fasync_helper – registers or unregisters a file descriptor in the fasync list.

int fasync_helper(int fd, struct file *filp, int mode, struct fasync_struct **fa);
fd

: file descriptor to register. filp: pointer to the file structure. mode: 1 to add, 0 to remove. fa: pointer to the driver’s async queue pointer.

Typical driver implementation:

static int my_fasync(int fd, struct file *filp, int mode) {
    struct my_dev *dev = filp->private_data;
    return fasync_helper(fd, filp, mode, &dev->async_queue);
}

(2) kill_fasync – sends a signal to all registered processes.

void kill_fasync(struct fasync_struct **fa, int sig, int band);
fa

: pointer to the async queue. sig: signal to send (usually SIGIO). band: POLL_IN for readable, POLL_OUT for writable.

Example in an interrupt handler:

if (dev->async_queue) {
    kill_fasync(&dev->async_queue, SIGIO, POLL_IN);
}

4.3 fasync workflow

(1) User registers async notification – open the device (often with O_NONBLOCK), set the owner with fcntl(fd, F_SETOWN, getpid()), and enable the FASYNC flag:

#include <fcntl.h>
int fd = open("/dev/ttyS0", O_RDONLY | O_NONBLOCK);
fcntl(fd, F_SETOWN, getpid());
int flags = fcntl(fd, F_GETFL);
fcntl(fd, F_SETFL, flags | FASYNC);

(2) Kernel processes the registration – the file_operations fasync method calls fasync_helper to add the process to the async list.

(3) Event occurs – the driver’s interrupt or work routine calls kill_fasync, which walks the list and sends SIGIO with the appropriate band.

(4) User process handles the signal – the previously registered handler reads the device data and performs the required logic.

5. Practical Implementation

5.1 Driver side

1. Add an async_queue pointer to the device structure:

struct button_dev {
    int gpio;
    int state;
    struct fasync_struct *async_queue; // async notification queue
};

2. Implement fasync and release callbacks and bind them in file_operations:

static int button_fasync(int fd, struct file *filp, int on) {
    struct button_dev *dev = filp->private_data;
    return fasync_helper(fd, filp, on, &dev->async_queue);
}

static int button_release(struct inode *inode, struct file *filp) {
    struct button_dev *dev = filp->private_data;
    return button_fasync(-1, filp, 0); // unregister
}

static struct file_operations button_fops = {
    .owner   = THIS_MODULE,
    .fasync  = button_fasync,
    .release = button_release,
    // .read, .write, etc.
};

3. In the interrupt handler, notify the user when the key state changes:

static irqreturn_t button_irq_handler(int irq, void *dev_id) {
    struct button_dev *dev = dev_id;
    int value = gpio_get_value(dev->gpio);
    if (value != dev->state) {
        dev->state = value;
        if (dev->async_queue)
            kill_fasync(&dev->async_queue, SIGIO, POLL_IN);
    }
    return IRQ_HANDLED;
}

5.2 Application side

1. Register a SIGIO handler:

#include <stdio.h>
#include <signal.h>
#include <fcntl.h>
#include <unistd.h>

int fd;

void sigio_handler(int sig) {
    char buf[128];
    ssize_t n = read(fd, buf, sizeof(buf));
    if (n > 0) {
        buf[n] = '\0';
        printf("Received data: %s
", buf);
    }
}

int main() {
    fd = open("/dev/button_device", O_RDWR);
    if (fd < 0) { perror("open"); return 1; }
    if (signal(SIGIO, sigio_handler) == SIG_ERR) { perror("signal"); close(fd); return 1; }
    if (fcntl(fd, F_SETOWN, getpid()) == -1) { perror("fcntl F_SETOWN"); close(fd); return 1; }
    int flags = fcntl(fd, F_GETFL);
    if (flags == -1) { perror("fcntl F_GETFL"); close(fd); return 1; }
    if (fcntl(fd, F_SETFL, flags | FASYNC) == -1) { perror("fcntl F_SETFL"); close(fd); return 1; }
    while (1) sleep(1);
    close(fd);
    return 0;
}

5.3 Build and test

Driver compilation uses a simple Makefile:

obj-m += button_driver.o
KDIR := /lib/modules/$(shell uname -r)/build
PWD  := $(shell pwd)
all:
	$(MAKE) -C $(KDIR) M=$(PWD) modules
clean:
	rm -f *.ko *.o *.mod.c *.symvers *.order

Load the module on the target board with modprobe button_driver.ko, then run the user program. Pressing the hardware button triggers the interrupt, kill_fasync sends SIGIO, and the application prints the received data, confirming the asynchronous notification works.

6. Summary

The fasync mechanism provides a lightweight, signal‑driven way for Linux kernels to notify user‑space processes about I/O events. By understanding the fasync_struct layout, the helper functions fasync_helper and kill_fasync, and the required user‑space setup (signal registration, F_SETOWN, and FASYNC flag), developers can replace inefficient polling with event‑driven designs in network servers, device drivers, and embedded systems.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Linux kerneldevice driversignal-driven I/Oasynchronous notificationfasyncSIGIO
Deepin Linux
Written by

Deepin Linux

Research areas: Windows & Linux platforms, C/C++ backend development, embedded systems and Linux kernel, etc.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.