【java入门到放弃】IO多路复用技术
名词解释
fd
fd(File Descriptor,文件描述符)
fd 是一个整数,用来代表一个“已打开的文件/IO 资源”
在 Linux / Unix 中,一切皆文件,内核不直接让你操作这些对象本身,而是给你一个整数编号 —— fd
fd 是一个整数句柄,代表一个 IO 资源;fds 是多个 fd 组成的集合,用于统一监听和管理
fd_set
fd_set 是一个“文件描述符集合”的位图数据结构,用来告诉操作系统:我关心哪些 fd 的 IO 状态(可读 / 可写 / 异常)。
fd_set 的作用就是:
把“我要监听的 fd 列表”传给内核
接收内核返回的“就绪 fd 列表”
fd_set是在select模式下使用的
pollfd
pollfd 是 Linux / POSIX 系统中 poll() 系统调用的核心数据结构,用来告诉内核你想监听哪些文件描述符,以及关心哪些事件。
它类似于 select 的 fd_set,但比 select 更灵活,而且没有 FD_SETSIZE 限制。
在 <poll.h> 里定义(Linux 为例):
struct pollfd {
int fd; // 要监听的文件描述符
short events; // 用户关心的事件(可读/可写/异常)
short revents; // 内核返回的事件(就绪的事件)
};
内核
内核是“管理者”,内存是“被管理的资源”。
内核本身也住在内存里,但它住在“受保护的那一部分”。
内核空间/用户空间
内核是“程序”,内核空间/用户空间是“内存区域(地址空间)”。
内核空间是操作系统为内核及其相关数据保留的内存地址区域。
用户空间是普通应用程序运行和使用的内存地址区域。
同一个线程,在不同时间、不同权限级别,访问不同空间。
理解
IO多路复用技术是操作系统的技术,redis和nginx等一些中间件,使用IO多路复用技术就是去调用操作系统所提供的接口。
select
概念
select 模式是 IO 多路复用的一种实现方式,它允许一个线程同时监控多个文件描述符(fd)的读、写、异常状态,从而实现单线程处理多个 IO 连接的能力。
流程
工作流程:(指的是用户程序调用操作系统实现的select模式IO多路复用)
1、用户态准备 fd_set
2、调用 select 系统调用
3、内核处理
4、用户态处理就绪 fd
内核处理:
1、拷贝 fd_set 到内核空间
2、遍历 fd,检查就绪状态
3、如果没有 fd 就绪 → 阻塞线程
4、IO 就绪 → 内核唤醒线程
5、拷贝回用户态
6、用户态继续处理
详细过程:
1、拷贝 fd_set 到内核空间
在select模式下
在大多数 Unix / Linux系统中:一个 fd_set 最多只能同时监听 1024 个文件描述符
可监听的最大 fd 值必须 < FD_SETSIZE
不是“放多少个 fd”,而是 fd 的编号不能超过 1023
因为fd_set的数据结构是定长位图(bitmap)
2、遍历 fd,检查就绪状态
fd_set不管有多少数据,都是全部扫描一遍,扫描1024个
3、如果没有 fd 就绪 → 阻塞线程
把线程挂起 → 被事件或超时唤醒后,再重新扫描一整遍。
唤醒时,不会传递fd,所以需要重新扫描
细节
select 返回用户态之后,内核空间里“不会保留任何 fd_set”。
下一次调用 select,不管上次有没有就绪 fd,用户都必须重新把“完整的 fd_set”传给内核。
内核把“就绪结果”写回 fd_set 后返回;
用户态必须再从 0 ~ maxfd 自己遍历一遍,才能找出哪些 fd 就绪。
poll
概念
poll 是 IO 多路复用的一种实现方式,用数组而不是位图(fd_set)来描述要监听的 fd,消除了 fd 数量上限,但内核处理模型仍是 O(n)。
解决的问题
select 的核心问题有 3 个:
1、fd 数量有上限;FD_SETSIZE = 1024
2、fd_set 是位图fd 必须是连续编号
3、每次都要全量扫描 fd
poll 的目标是:
解决 1 和 2,但不彻底解决 3
流程
pollfd结构:
struct pollfd {
int fd; // 要监听的文件描述符
short events; // 用户关心的事件(可读/可写/异常)
short revents; // 内核返回的事件(就绪的事件)
};
1、用户态准备 pollfd 数组
2、用户态poll() 系统调用
3、内核态copy_from_user 拷贝 pollfd 数组
4、遍历 pollfd,调用每个 fd 的 poll 回调
5、如果无就绪 fd, 把线程挂起(TASK_INTERRUPTIBLE)
6、IO 中断到来, 唤醒等待队列
7、再次扫描 pollfd
8、填充 revents
9、copy_to_user 返回结果
10、用户态遍历 fds[i].revents,处理就绪 fd
4、遍历 pollfd,调用每个 fd 的 poll 回调
每一种 fd 类型
都可以实现自己的 poll() 方法
poll方法的作用
注册等待关系(如果之后要阻塞)
检查当前是否就绪
“注册等待关系”指的是:
在真正阻塞之前,把“当前线程”登记到各个 fd 对应的等待队列里,
告诉内核:如果这些 fd 的状态发生变化,请唤醒我。
6、IO 中断到来, 唤醒等待队列
“IO 中断” 指的是 硬件或内核子系统在“IO 状态发生变化”时,向 CPU 发出的中断/事件通知。
epoll
解决的问题
epoll 的核心目标:
把「唤醒线程」和「具体是哪个 fd 就绪」绑定在一起,
从而避免“被唤醒后再全量扫描 fd”。
eventpoll
eventpoll 对象是 epoll 的核心
eventpoll
├── rbr (红黑树:所有监听的 fd)
├── rdllist (就绪链表:ready list)
├── wq (等待队列:epoll_wait 睡在这)
└── lock (自旋锁)
epoll_event
epoll_event就是:
epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event)里的event的类型
epoll_wait(epfd, events, maxevents, timeout)里events[i] 的类型
在 Linux 里(<sys/epoll.h>),定义本质是下面这样(略去兼容宏)(c语言结构):
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
struct epoll_event {
uint32_t events; // 事件类型(EPOLLIN / EPOLLOUT / EPOLLERR / ...)
epoll_data_t data; // 用户自定义数据
};
流程
1、epoll_create:创建 epoll 内核对象
用户态创建
int epoll_create(int size);
epoll_create 只需要传一个参数:size,表示“你打算监控的 fd 数量的一个提示值(hint)”。
早期是预分配数据结构,减少动态扩容开销
在现代 Linux 内核中,这个参数已经基本被忽略。
成功:返回一个 epoll 文件描述符(epfd)
失败:返回 -1,并设置 errno
epfd 是一个文件描述符,它指向内核中创建的 epoll 实例对象(eventpoll)。
2、epoll_ctl(ADD):注册 fd(最关键步骤)
epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event);
epoll_ctl(epoll 内核对象的 fd, 操作类型:添加, 要监听的目标 fd(socket / pipe / 等), 你关心的事件 + 用户数据);
&event 是用户告诉内核:“我关心这个 fd 的这些事件,并且请在返回时给我这个数据。”
&event 包含events和dataevents → 关心哪些事件(可读、可写、错误、ET、ONESHOT…)data → 用户自定义标识/数据,方便 epoll_wait 返回时快速定位 fd 或上下文
① 用户态 → 内核态,进入内核态
② 找到 epoll 内核对象(eventpoll)
③ 找到被监听 fd 对应的内核对象
④ 创建一个 epoll 监听节点(关键结构)
内核会创建一个结构体,把 epoll 和 fd 连接起来:
struct epitem {
struct rb_node rbn; // 红黑树节点
struct list_head rdllink; // 就绪链表节点
struct file *file; // 被监听的 fd
struct eventpoll *ep; // 所属 epoll
struct epoll_event event; // 你传进来的 event
};
epitem 是 epoll 的“灵魂节点”
⑤ 把 epitem 插入 epoll 的红黑树(核心)
⑥ 在“被监听 fd”上注册回调(最关键的一步)
ep_poll_callback() 会做几件事:
- 判断 fd 是否满足你关心的事件(EPOLLIN 等)
- 如果满足:
- 把对应的 epitem 挂到 epoll 的 就绪链表
- 唤醒:
- 正在 epoll_wait 上睡眠的线程
⑦ epoll_ctl(ADD) 返回用户态
此时:
- fd 已经被 epoll “长期监听”
- 监听关系 常驻内核
- 不再需要每次传 fd 列表
3、epoll_wait()(用户态进入内核态)
int n = epoll_wait(epfd, events, maxevents, timeout);
epfd → 找到 struct eventpoll
events → 用户态 buffer,用于返回就绪事件;epoll_ctl参数&event 里的events
maxevents → 最大事件数
timeout → 阻塞时长,超时后,代码继续执行,后续fd会存到就绪队列,等待下一次wait
返回event的个数,超时返回0,错误返回-1。
events会被赋值。
int n = epoll_wait(epfd, events, maxevents, timeout);
epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event);
上面的events和&event格式一样,都是包含event和data,一个是单个对象,一个是对象数组
如果 ready list 为空:
把当前线程挂到 epoll 的等待队列
设置线程状态 TASK_INTERRUPTIBLE / TASK_UNINTERRUPTIBLE
线程 sleep
4、 fd 状态变化(IO 事件到来)
内核态事件触发
触发 fd 自己的 poll 方法 / 回调
epoll 注册的回调被调用:
检查 fd 是否已经在 ready list
如果不在 → 加入 ready list
wake_up(epoll_wait 的等待线程)
5、epoll_wait 被唤醒
- 内核态:
- 线程状态改为 RUNNABLE
- 被调度 CPU 执行
- 根据 ready list 中的就绪 epitem,
构造 epoll_event,拷贝到用户态的 events 数组中,
并根据触发模式(LT / ET)决定是否将其从 ready list 移除
- 系统调用返回,线程切回用户态
- 用户态拿到就绪 fd 数组,处理业务逻辑
没有再次扫描所有 fd,只返回已经准备好的 fd。
6、再次等待 / ET / LT 模式
- LT(水平触发):
- fd 只要仍然就绪,每次 epoll_wait 都会返回
- ET(边缘触发):
- fd 只有从不就绪 → 就绪时触发
- 用户必须用非阻塞 IO 读干净,否则不会再次触发
补充
LT(Level Triggered)模式
- 默认模式
- 逻辑:
- fd 可读/可写 → 加入 ready list
- epoll_wait 返回事件
- ready list 中的 epitem 被移除(已经消费)
- 但 只要 fd 仍然可读/可写
- 下一次 epoll_wait 会再次把它加入 ready list
→ 用户没有完全读/写时,会被重复通知
- 特性:
- 类似 select/poll
- “只要条件满足就会被通知”
- 不丢事件,但可能重复通知
ET(Edge Triggered)模式
- 边沿触发模式
- 逻辑:
- fd 从不可读 → 可读,或者不可写 → 可写
→ 内核将 epitem 加入 ready list - epoll_wait 返回事件
- ready list 中的 epitem 移除
- 下一次 epoll_wait 不会自动加入,除非 fd 状态发生新的边沿变化
- 用户必须一次性读/写完
- 否则事件丢失(不会再触发)
- 特性:
- 高效,减少系统调用次数
- 但要求 非阻塞 I/O + 一次性处理完所有数据
- nginx、redis 等高性能框架常用
细节
selete和poll就是一有返回,就把内核态的fd_set或 pollfd 数组删了,epoll是注册了,就一直存在,需要用户态手动删除,才能删除。epoll是一个一个的注册fd。
更多推荐




所有评论(0)