Character Driver select() Tutorial
Free Linux Device Drivers Course — Kernel 6.x Edition
In this free Linux kernel development course lecture, you will learn how the select system call linux device driver relationship works. The select() system call lets a user-space program wait for data-readiness on one or more file descriptors at the same time, and every character driver that supports it does so through the exact same .poll file operation you learned about in the previous lecture. This lecture is part of EmbeddedPathashala’s free embedded Linux course and free embedded systems course, and continues our hands-on, original character driver series.
Focus Keywords
free linux device drivers course
free linux kernel development course
free embedded linux course
What You Will Learn
- How select() works from user space
- FD_SET, FD_CLR, FD_ISSET, FD_ZERO macros
- How select() maps to the driver’s poll() callback
- Writing a testable character driver for select()
- select() vs poll() vs epoll() on kernel 6.x
- pselect() and race-free signal handling
Prerequisites
Before this lecture, you should be comfortable with the topics from earlier lectures in this free Linux device drivers course:
- struct cdev and device registration
- open(), read(), write() file operations
- wait_queue_head_t and poll_wait()
- A working kernel 6.x build environment (headers + make + gcc)
Why select() Still Matters on Modern Kernels
Many engineers assume select() is obsolete because epoll() exists, but select() is still part of POSIX, still shipped in glibc, and still widely used in portable tooling, scripts, and legacy applications that a driver author cannot avoid supporting. When you write a character driver and implement the .poll callback correctly, you automatically gain support for select(), poll(), and epoll() at the same time — the kernel translates all three into calls against the same callback. Understanding select() end to end is therefore essential groundwork for this free linux kernel development course, even though epoll() is usually the better choice for new production code.
How select() Reaches Your Driver
The select() System Call Prototype
The POSIX prototype, as documented by the Linux man-pages project, is:
select() Prototype
fd_set *exceptfds, struct timeval *timeout);
Each argument has a specific role in this free linux device drivers course context:
| Argument | Purpose |
|---|---|
| nfds | Highest file descriptor number in any set, plus 1 |
| readfds | Set of descriptors to check for read-readiness |
| writefds | Set of descriptors to check for write-readiness |
| exceptfds | Set of descriptors to check for exceptional conditions |
| timeout | Maximum time to block; NULL means wait forever |
You build the descriptor sets with four helper macros:
- FD_ZERO(&set) — clears the set
- FD_SET(fd, &set) — adds a descriptor
- FD_CLR(fd, &set) — removes a descriptor
- FD_ISSET(fd, &set) — checks if a descriptor is ready after select() returns
Writing an Original Driver for select() Testing
Rather than copying an outdated textbook example, here is an original, kernel-6.x-clean character driver called ep_select_demo. It uses a kernel timer to flip a “data ready” flag every three seconds and wakes up any process blocked in select() through the same wait-queue mechanism you studied in the poll() lecture.
ep_select_demo.c
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/poll.h>
#include <linux/wait.h>
#include <linux/timer.h>
#include <linux/uaccess.h>#define DEV_NAME “ep_select_demo”static dev_t devno;
static struct cdev ep_cdev;
static wait_queue_head_t ep_wq;
static struct timer_list ep_timer;
static int data_ready;static void ep_timer_cb(struct timer_list *t)
{
data_ready = 1;
wake_up_interruptible(&ep_wq);
mod_timer(&ep_timer, jiffies + 3 * HZ);
}static int ep_open(struct inode *inode, struct file *filp)
{
return 0;
}
static ssize_t ep_read(struct file *filp, char __user *buf,
size_t count, loff_t *pos)
{
char msg[] = “tick\n”;
if (!data_ready)
return -EAGAIN;
data_ready = 0;
if (count > sizeof(msg))
count = sizeof(msg);
if (copy_to_user(buf, msg, count))
return -EFAULT;
return count;
}
static __poll_t ep_poll(struct file *filp, poll_table *wait)
{
__poll_t mask = 0;
poll_wait(filp, &ep_wq, wait);
if (data_ready)
mask |= EPOLLIN | EPOLLRDNORM;
return mask;
}
static const struct file_operations ep_fops = {
.owner = THIS_MODULE,
.open = ep_open,
.read = ep_read,
.poll = ep_poll,
};
static int __init ep_select_init(void)
{
int ret;
ret = alloc_chrdev_region(&devno, 0, 1, DEV_NAME);
if (ret)
return ret;
cdev_init(&ep_cdev, &ep_fops);
ret = cdev_add(&ep_cdev, devno, 1);
if (ret) {
unregister_chrdev_region(devno, 1);
return ret;
}
init_waitqueue_head(&ep_wq);
timer_setup(&ep_timer, ep_timer_cb, 0);
mod_timer(&ep_timer, jiffies + 3 * HZ);
pr_info(“ep_select_demo: loaded, major=%d\n”, MAJOR(devno));
return 0;
}
static void __exit ep_select_exit(void)
{
timer_delete_sync(&ep_timer);
cdev_del(&ep_cdev);
unregister_chrdev_region(devno, 1);
pr_info(“ep_select_demo: unloaded\n”);
}
module_init(ep_select_init);
module_exit(ep_select_exit);
MODULE_LICENSE(“GPL”);
MODULE_AUTHOR(“EmbeddedPathashala”);
MODULE_DESCRIPTION(“Original select()/poll() demo driver”);
Kernel 6.x note: this driver uses timer_setup() and timer_delete_sync(), the modern timer API covered earlier in this course. Older tutorials that still use init_timer() and del_timer_sync() will not build cleanly on current kernels.
Loading the Driver and Creating the Device Node
Terminal Commands
$ sudo insmod ep_select_demo.ko
$ dmesg | tail -n 2
ep_select_demo: loaded, major=238
$ sudo mknod /dev/ep_select_demo c 238 0
$ sudo chmod 666 /dev/ep_select_demo
A Complete User-Space select() Test Program
This original test program opens the device and blocks in select() until the kernel timer marks data as ready, demonstrating the full select system call linux device driver round trip.
ep_select_test.c
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/select.h>#define DEV_PATH “/dev/ep_select_demo”int main(void)
{
int fd, ret;
fd_set readfds;
struct timeval tv;
char buf[16];fd = open(DEV_PATH, O_RDONLY);
if (fd < 0) {
perror(“open”);
return EXIT_FAILURE;
}while (1) {
FD_ZERO(&readfds);
FD_SET(fd, &readfds);
tv.tv_sec = 5;
tv.tv_usec = 0;
ret = select(fd + 1, &readfds, NULL, NULL, &tv);
if (ret < 0) {
perror(“select”);
break;
} else if (ret == 0) {
printf(“select() timed out, no data yet\n”);
continue;
}
if (FD_ISSET(fd, &readfds)) {
ssize_t n = read(fd, buf, sizeof(buf) – 1);
if (n > 0) {
buf[n] = ‘\0’;
printf(“Received: %s”, buf);
}
}
}
close(fd);
return EXIT_SUCCESS;
}
Compiling and Running
Expected Output
$ ./ep_select_test
Received: tick
Received: tick
Received: tick
^C
Every three seconds the kernel timer sets data_ready and calls wake_up_interruptible(), which unblocks the select() call in user space. If no tick arrives within five seconds, the program prints the timeout message instead, proving the timeout path works correctly.
select() vs poll() vs epoll() on Kernel 6.x
| API | Descriptor Limit | Complexity | Best Use Case |
|---|---|---|---|
| select() | FD_SETSIZE (usually 1024) | O(n) scan each call | Portable scripts, legacy code |
| poll() | No hard limit | O(n) scan each call | Moderate descriptor counts |
| epoll() | No hard limit | O(1) per ready event | High-performance servers, many descriptors |
From the driver author’s perspective, the good news is that none of this matters: as long as your .poll callback is implemented correctly with poll_wait() and an accurate __poll_t mask, select(), poll(), and epoll() all work automatically against your device.
pselect() and the Signal Race
A subtle bug appears when a program wants to block in select() while also handling signals safely. Blocking a signal, checking a flag, and calling select() as three separate steps creates a race where the signal can arrive between the check and the call. POSIX added pselect() to close this gap:
pselect() Prototype
fd_set *exceptfds, const struct timespec *timeout,
const sigset_t *sigmask);
pselect() atomically installs the given signal mask for the duration of the call, so from a driver-development standpoint no extra work is needed — the same .poll callback serves both select() and pselect() identically.
Common Mistakes and Troubleshooting
| Mistake | Symptom | Fix |
|---|---|---|
| Forgetting FD_ZERO before FD_SET | Stale descriptors checked from previous loop iteration | Always FD_ZERO at the top of every loop |
| Passing nfds without +1 | select() silently ignores the highest descriptor | Always pass highest_fd + 1 |
| Driver poll() missing poll_wait() | select() never blocks, always returns immediately | Always call poll_wait() even when data is already ready |
| Wrong EPOLL mask bits | select() reports ready but read() returns -EAGAIN | Only set EPOLLIN when a read would not block |
| Reusing timeval across calls | Linux updates timeout in place, causing shrinking timeouts on retry | Reinitialize tv.tv_sec/tv.tv_usec every iteration |
Best Practices for Driver Authors
- Always implement .poll if your device can block on read or write
- Keep the poll() callback fast and non-blocking itself
- Register with poll_wait() even on the code path where data is already ready
- Use accurate EPOLLIN/EPOLLOUT/EPOLLRDNORM/EPOLLWRNORM bits
- Prefer epoll() in your own new user-space tools; support select() only for compatibility
Performance Considerations
select() rebuilds and rescans the entire descriptor set on every call, which is fine for a handful of descriptors but becomes expensive as the count grows, since the cost is O(n) per call regardless of how many descriptors are actually active. Kernel-side, this cost is mostly in user space; your driver’s poll() callback itself should remain O(1) so it does not become the bottleneck when many processes select() on the same device simultaneously.
Security Considerations
A poll() callback that reports EPOLLIN when no data is actually available can be used to make a user-space process spin or block incorrectly, so always keep the readiness flag and the actual data buffer in sync using proper locking (a spinlock or mutex, depending on whether the flag is touched from interrupt context). Never trust the size or content of user-supplied buffers in read()/write() without copy_to_user()/copy_from_user(), which you learned earlier in this free linux kernel development course.
Summary / Key Takeaways
- select() and poll() both route through the same driver .poll callback
- FD_ZERO/FD_SET/FD_CLR/FD_ISSET manage the descriptor sets
- poll_wait() must always be called, even when data is already ready
- pselect() fixes the signal-race problem without any driver changes
- epoll() scales better, but select() support comes free once .poll is correct
Conclusion
You now understand how the select system call linux device driver relationship works end to end: from FD_SET macros in user space, through the VFS, into your driver’s poll() callback, and back out through a wait queue wake-up. This completes the readiness-notification trio for this free linux device drivers course — poll() from the previous lecture and select() from this one both rely on identical driver code, and the next lecture in this free embedded Linux course moves on to the ioctl() method for sending custom commands to your device.
Frequently Asked Questions
Does select() require any extra code in my character driver?
No. If your driver already implements a correct .poll callback for poll() support, select() works automatically because the kernel implements select() on top of the same internal poll mechanism.
What is the maximum number of file descriptors select() can monitor?
select() is limited by FD_SETSIZE, typically 1024 on Linux. For larger descriptor counts, use poll() or epoll() instead.
Why does my select() call return immediately every time?
This almost always means your driver’s poll() callback is not calling poll_wait(), so the kernel never registers the calling process on your wait queue.
Is select() deprecated on modern Linux kernels?
No. select() remains part of POSIX and is fully supported on kernel 6.x, though epoll() is generally recommended for new high-performance applications.
What is the difference between select() and pselect()?
pselect() takes a nanosecond-resolution timeout and atomically applies a signal mask for the duration of the call, closing a race condition present in plain select().
Can select() be used with regular files as well as character devices?
Regular files are always reported as ready by the VFS default poll implementation, so select() is mainly useful for devices, pipes, and sockets that can genuinely block.
Do I need locking around the data_ready flag in my driver?
Yes, in real drivers you should protect any flag shared between interrupt/timer context and process context using a spinlock, even though this teaching example keeps it simple for clarity.
What replaces select() in high-concurrency Linux servers?
epoll() is the standard modern replacement for high descriptor counts, offering O(1) event delivery instead of the O(n) rescanning that select() and poll() require.
Continue This Free Linux Kernel Development Course
Next up: the ioctl() method for sending custom commands to your character driver.
