Select 是 Linux 系统中最早的 I/O 多路复用机制,它允许程序同时监控多个文件描述符的状态变化。
Select 使用位图(fd_set)来表示要监控的文件描述符集合,通过系统调用进入内核后,内核会遍历位图中设置为 1 的每一位,对每个对应的文件描述符调用其 poll 方法来检查是否有数据可读、可写或发生异常。
当有事件就绪时,内核会将结果位图拷贝回用户空间,用户程序通过检查结果位图来确定哪些文件描述符发生了事件。
Select 的主要特点是实现简单、兼容性好,但也存在明显的性能瓶颈:每次调用都需要遍历所有文件描述符(时间复杂度 O(n)),最多只能监控 1024 个文件描述符,并且每次调用都要进行两次用户态和内核态之间的数据拷贝。
因此,Select 适用于连接数较少的场景,在高并发环境下性能较差,这也是后来出现 Poll 和 Epoll 等更高效 I/O 多路复用机制的原因。
是
否
用户空间调用select方法
系统调用入口 SYSCALL_DEFINE5
kern_select 参数验证和初始化
do_select 核心实现
遍历文件描述符位图
对每个fd调用poll方法
检查事件状态
更新结果位图
有事件就绪?
返回结果到用户空间
用户空间轮询处理相应事件
等待事件或超时
被唤醒后重新检查
1.kern_select 参数验证和初始化流程
是
否
kern_select开始
声明数据结构
参数验证
n < 0 或 n > FD_SETSIZE?
返回 -EINVAL
初始化poll_wqueues
设置select_table
调用do_select
清理资源
返回结果
结束
poll_wqueues: Linux 内核中用于管理 poll/select 等待队列的核心数据结构。select_table: Select 系统调用中连接用户空间和内核空间 poll 机制的桥梁数据结构。
2. do_select() 详细实现流程
全为0
有设置位
否
是
否
是
否
是
是
否
do_select开始
拷贝fd_set到内核空间
初始化变量和指针
遍历每个unsigned long 64位组
检查当前64位组
跳过整个64位组
下一个64位组
遍历当前64位组的每一位
当前位是否设置?
下一位
获取文件描述符fdget_i
文件描述符有效?
释放文件描述符
调用文件的poll方法
检查事件状态
设置结果位
遍历完当前64位组?
更新结果位图到用户空间
有事件就绪?
返回事件数量
调用poll_schedule_timeout等待
被唤醒或超时
重新检查事件状态
结束
fd_set: 表示文件描述符集合的位图数据结构
固定大小:__FD_SETSIZE = 1024 在64位系统上:1024 / 64 = 16 个 unsigned long、 总共可以表示 1024 个文件描述符
3. 位图数据结构详解
位值含义
位图含义
fd_set 位图结构
1: 监控
0: 不监控
位1: 文件描述符1
位0: 文件描述符0
位2: 文件描述符2
...
位1023: 文件描述符1023
bits_1 64位
bits_0 64位
bits_2 64位
...
bits_15 64位
4. 文件描述符处理详细流程
用户空间
内核空间
文件系统
Socket层
select() 系统调用
从用户空间拷贝fd_set到内核
遍历位图
fdget(i) 获取文件描述符
返回文件对象
调用sock_poll()
检查socket状态
返回事件掩码
根据事件掩码设置结果位
fdput(f) 释放文件描述符
loop
[对每个设置的文件描述符]
更新结果位图
返回结果到用户空间
用户空间
内核空间
文件系统
Socket层
5. Poll方法调用链
TCP
UDP
其他
do_select调用poll
sock_poll
检查socket类型
tcp_poll
udp_poll
默认poll
检查TCP连接状态
检查接收缓冲区
检查发送缓冲区
检查错误状态
检查UDP数据
返回默认事件
返回事件掩码
设置结果位
6. 等待机制实现
进程运行中
没有事件就绪
poll_schedule_timeout
文件描述符状态改变
超时时间到达
收到信号
重新检查事件
重新检查事件
重新检查事件
返回结果
Running
Interruptible
Waiting
Woken
Timeout
Signal
7. 用户态处理就绪事件
用户程序收到返回值后,会处理相应事件:
int ready = select ( n, & readfds, & writefds, & exceptfds, & timeout) ;
if ( ready > 0 ) {
for ( int i = 0 ; i < n; i++ ) {
if ( FD_ISSET ( i, & readfds) ) {
read_data ( i) ;
}
if ( FD_ISSET ( i, & writefds) ) {
write_data ( i) ;
}
if ( FD_ISSET ( i, & exceptfds) ) {
handle_exception ( i) ;
}
}
}
8. 性能瓶颈分析
Select性能瓶颈
遍历开销
内存拷贝
文件描述符限制
重复检查
O_n 时间复杂度
每次都要遍历所有fd
即使大部分fd没有事件
每次调用都拷贝fd_set
结果也要拷贝回用户空间
用户态内核态切换开销
最多1024个文件描述符
无法动态扩展
不适合高并发场景
每次重新检查所有fd状态
没有事件驱动机制
轮询方式效率低
9. 与其他I/O多路复用对比
Epoll
Poll
Select
时间复杂度: O_1
fd限制: 无限制
内存拷贝: 只在添加时
事件通知: 事件驱动
数据结构: 红黑树+链表
时间复杂度: O_n
fd限制: 无限制
内存拷贝: 每次调用
事件通知: 轮询
数据结构: 数组
时间复杂度: O_n
fd限制: 1024
内存拷贝: 每次调用
事件通知: 轮询
数据结构: 位图