﻿---
title: IO Multiplexing
date: 2025-07-19
excerpt: A brief walkthrough of how select, poll, and epoll are implemented.
tags:
  - IO
  - Socket
  - File
  - Linux
  - OS
  - Network
cover: https://assets.vluv.space/cover/Dev/Linux/io_multiplexing.webp
updated: 2026-07-08 21:17:39
lang: en
i18n:
  cn: /io_multiplexing
  translation: 2
---

> [!TIP]
>
> This note focuses on network I/O. For an introduction to the 5 I/O models, see [[io_models]]

## Select

`select` is an **I/O multiplexing** mechanism used to monitor multiple [[file_system#File Descriptor|file descriptors]] at once for I/O readiness events, such as readable, writable, or exceptional conditions.

```c
int select(int nfds, fd_set *readfds, fd_set *writefds,
           fd_set *exceptfds, struct timeval *timeout);
```

For the readiness conditions, see UNIX Network Programming Volume 1, section 6.3.1. The figure below summarizes them:

![Conditions for select to return ready](https://assets.vluv.space/select返回就绪条件.webp)

### timeout

The `timeout` parameter sets the wait time, in one of three ways:

- **No waiting**: poll the file descriptors and return immediately, with the time in timeval set to 0
- **Wait until a file descriptor is ready**: specify a wait time in timeval; select blocks until a file descriptor becomes ready or the timeout expires
- **Wait forever**: set timeval to NULL; select blocks until some file descriptor is ready

> [!INFO] struct timeval
>
> ```c
> struct timeval {
>     long tv_sec;  // seconds
>     long tv_usec; // microseconds
> };
> ```

### Fd Set

fd_set is typically a bitmap, implemented internally as an array of long integers. Each bit in the array corresponds to a [[file_system#File Descriptor|file descriptor]].

The size of this bitmap is fixed, determined by the constant FD_SETSIZE (usually 1024 on Linux). This is hard-coded in the Linux kernel; changing the fd_set size requires modifying the source and recompiling glibc and the kernel.

When writing C code, you can use the following macros to manipulate an `fd_set`:

```c
FD_ZERO(fd_set *fdset);          // clear all bits in fdset
FD_SET(int fd, fd_set *fdset);   // turn on the bit for fd in fdset
FD_CLR(int fd, fd_set *fdset);   // turn off the bit for fd
FD_ISSET(int fd, fd_set *fdset); // is the bit for fd on?
```

> [!EXAMPLE]
>
> For example, to monitor file descriptors 3, 5, and 8 for readability, set up the `fd_set` like this:
>
> ```c
> fd_set read_fds;
>
> FD_ZERO(&read_fds);
> FD_SET(3, &read_fds); // watch fd 3
> FD_SET(5, &read_fds); // watch fd 5
> FD_SET(8, &read_fds); // watch fd 8
>
> select(9, &read_fds, NULL, NULL, &timeout);
> ```

When select finishes, it returns the total number of ready file descriptors and modifies the `fd_set` accordingly, setting the bits of the ready file descriptors to 1. Because of this, you usually need to re-populate the `fd_set` before each new call to `select`.

![How select works](https://assets.vluv.space/select_%E5%8A%A8%E5%9B%BE.gif)

### nfds

`nfds` (number of file descriptors) is usually set to the highest file descriptor in the passed-in sets plus 1, that is:

$$
\text{nfds} = \max(\text{MaxReadFd}, \text{MaxWriteFd}, \text{MaxExceptFd}) + 1
$$

UNIX Network Programming Volume 1 notes that "the reason this argument exists, along with the burden of calculating its value, is purely for efficiency." To understand why the parameter matters, first imagine there were no `nfds` parameter:

`select` would copy the entire 1024-bit bitmap (fd_set) into kernel space, then copy the bitmap back to user space when the call finishes. That overhead is not trivial.

Yet an ordinary process typically checks far fewer than 1024 file descriptors. Taking the code above as an example, we only care about descriptors 3, 5, and 8; the remaining 1015 bits are irrelevant, and transferring them is unnecessary.

With the `nfds` parameter, `select` does not have to copy the full 1024-bit bitmap. Instead, it **uses `nfds` to decide up to which byte to copy**, saving CPU time and memory bandwidth.

## Poll

poll offers functionality similar to select. The kernel still scans the array linearly, so the time complexity remains $O(n)$.

The differences are that the size of the fds array you pass in can grow as needed (still bounded by system resources), and the API is a bit cleaner.

> [!warning]
>
> On Linux, the number of file descriptors each process can open is limited, typically defaulting to 1024. You can view and change the value with the `ulimit` command.
>
> `select`'s file descriptor limit is additionally constrained by a hard-coded implementation constant (usually `FD_SETSIZE`, default 1024). Even if the system allows more fds, `select` still cannot support them.
>
> ```bash
> # Maximum number of file descriptors the current process can open
> ulimit -n
>
> # System-wide limit on total open files (across all processes)
> cat /proc/sys/fs/file-max
> ```

```c
int poll(struct pollfd *fds, unsigned long nfds, int timeout);

struct pollfd {
    int fd;         // file descriptor
    short events;   // events of interest on fd
    short revents;  // events that occurred on fd
};
```

Each bit of events represents one kind of event; like select, they fall into three categories: read, write, and exception. Run `man poll` in a shell for the detailed event types.

revents is filled in by the kernel after the poll call, indicating the events that actually occurred.

## Epoll

`epoll` is a Linux-specific I/O multiplexing mechanism designed for handling large numbers of file descriptors. It is more efficient than `select` and `poll`, especially with many concurrent connections. For a deep dive into epoll, see [图解 | 深入揭秘 epoll 是如何实现 IO 多路复用的！](https://mp.weixin.qq.com/s?__biz=MjM5Njg5NDgwNA==&mid=2247484905&idx=1&sn=a74ed5d7551c4fb80a8abe057405ea5e&chksm=a6e304d291948dc4fd7fe32498daaae715adb5f84ec761c31faf7a6310f4b595f95186647f12&scene=21#wechat_redirect)

| Feature              | select               | poll        | epoll                 |
| -------------------- | -------------------- | ----------- | --------------------- |
| Interface standard   | POSIX                | POSIX       | Linux-specific        |
| fd count limit       | Limited (usually 1024) | Unlimited | Unlimited             |
| fd representation    | Bitmap               | Array       | Kernel structure      |
| Copies fd set        | On every call        | On every call | Registered once     |
| Scan complexity      | O(n)                 | O(n)        | O(1)~O(active fds)    |

### Epoll LT & ET

| Dimension                  | Level-triggered (LT)                                         | Edge-triggered (ET)                                     |
| -------------------------- | ------------------------------------------------------------ | ------------------------------------------------------- |
| Trigger condition          | Fires continuously as long as the state holds (e.g. buffer non-empty) | Fires once, only on the "not ready → ready" transition |
| Notification frequency     | High (keeps notifying until data is drained)                 | Low (only on state changes)                             |
| Data handling requirement  | Can process in multiple passes, no need to finish at once    | Must drain all current data in one go                   |
| Requires non-blocking I/O  | Optional (works with blocking I/O too)                       | Required (otherwise threads may block)                  |
| Programming complexity     | Low (hard to get wrong)                                      | High (must handle edge cases to avoid data loss)        |
| Performance                | Moderate                                                     | Higher (less notification overhead)                     |
## Ref

- [你管这破玩意叫 IO 多路复用？](https://mp.weixin.qq.com/s?__biz=MzkxMDc1MDg1Nw==&mid=2247508528&idx=1&sn=ca2920020af8b51c3649d103dd7d3331&source=41#wechat_redirect)
- 《UNIX Network Programming》- W. Richard Stevens
