多路转接select
本文介绍了select系统调用的基本概念和使用方法。select函数用于实现多路复用I/O模型,可以同时监控多个文件描述符的状态变化。文章详细解析了select的函数原型、参数含义和返回值,重点说明了fd_set位图结构和相关操作接口。通过示例展示了select的执行过程和工作原理,包括如何设置监控集合、处理返回结果等。同时分析了select的特点和局限性,如需要手动维护fd集合、用户态与内核态数
初识select
系统提供select函数来实现多路复⽤输⼊/输出模型.
- select系统调⽤是⽤来让我们的程序监视多个⽂件描述符的状态变化的;
- 程序会停在select这⾥等待,直到被监视的⽂件描述符有⼀个或多个发⽣了状态改变;
select函数原型
select的函数原型如下
#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds,
struct timeval *timeout);
参数解释:
- 参数nfds是需要监视的最⼤的⽂件描述符值+1;
- rdset,wrset,exset分别对应于需要检测的可读⽂件描述符的集合,可写⽂件描述符的集合及异常⽂件描述符的集合;
- 参数timeout为结构timeval,⽤来设置select()的等待时间
参数timeout取值:
- NULL:则表⽰select()没有timeout,select将⼀直被阻塞,直到某个⽂件描述符上发⽣了事件;
- 0:仅检测描述符集合的状态,然后⽴即返回,并不等待外部事件的发⽣。非阻塞
- 特定的时间值:如果在指定的时间段⾥没有事件发⽣,select将超时返回。
关于fd_set结构


其实这个结构就是⼀个整数数组,更严格的说,是⼀个"位图".使⽤位图中对应的位来表⽰要监视的⽂件描述符.
提供了⼀组操作fd_set的接⼝,来⽐较⽅便的操作位图.
void FD_CLR(int fd, fd_set *set); // ⽤来清除描述词组set中相关fd 的位
int FD_ISSET(int fd, fd_set *set); // ⽤来测试描述词组set中相关fd 的位是否为真
void FD_SET(int fd, fd_set *set); // ⽤来设置描述词组set中相关fd的位
void FD_ZERO(fd_set *set); // ⽤来清除描述词组set的全部位
关于timeval结构

timeval结构⽤于描述⼀段时间⻓度,如果在这个时间内,需要监视的描述符没有事件发⽣则函数返
回,返回值为0。
函数返回值:
- 执⾏成功则返回⽂件描述词状态已改变的个数
- 如果返回0代表在描述词状态改变前已超过timeout时间,没有返回
- 当有错误发⽣时则返回-1,错误原因存于errno,此时参数readfds,writefds,exceptfds和timeout的值变成不可预测。
错误值可能为:
- EBADF⽂件描述词为⽆效的或该⽂件已关闭
- EINTR此调⽤被信号所中断
- EINVAL参数n为负值。
- ENOMEM核⼼内存不⾜
理解 select 的执行过程:以 fd_set 为例
理解 select 模型的关键在于理解 fd_set 结构。为了便于说明,我们假设 fd_set 的长度为 1 字节(即 8 个 bit),每个 bit 对应一个文件描述符(fd)。这样,最多可以监控 8 个 fd。
- 定义并清空集合
fd_set set;
FD_ZERO(&set);
此时 set 的二进制表示为:0000 0000
- 加入 fd = 5
FD_SET(5, &set);
将第 5 位置为 1,set 变为:0001 0000(从低位 0 开始计数)
- 再加入 fd = 2 和 fd = 1
FD_SET(2, &set);
FD_SET(1, &set);
此时 set 变为:0001 0011
- 执行 select 阻塞等待
select(6, &set, NULL, NULL, NULL);
-
第一个参数 6 表示监控的最大 fd 值 + 1(即监控 fd 0~5)。
-
select 会阻塞,直到 set 中关注的 fd 上有事件发生。
- 事件发生后的集合变化
假设 fd = 1 和 fd = 2 上都发生了可读事件,则 select 返回。
此时 set 被内核修改为:0000 0011
即只有 fd = 1 和 fd = 2 对应的位保持为 1,其他之前加入的 fd(如 fd = 5)被清空。
重要说明
-
select 返回后,fd_set 中只剩下那些真正发生了事件的 fd。
-
因此,每次调用 select 之前,都需要重新用 FD_SET 添加需要监控的 fd(通常需要维护一个自己的数组来备份)。
这种“清空未活跃 fd”的行为是 select 的一大特点,也是它效率较低的原因之一(需要反复从用户态拷贝集合)。
select的特点
- 可监控的文件描述符数量
select 能够监控的文件描述符个数取决于 fd_set 结构体的大小。sizeof(fd_set) 的值决定了位图的总位数(每个 bit 对应一个 fd)。例如,在某台服务器上 sizeof(fd_set) = 512 字节,那么最多可以监控 512 × 8 = 4096 个文件描述符。
不同平台下 fd_set 的大小可能不同,通常可以通过宏 FD_SETSIZE 查看上限。
- 需要额外的数组来保存待监控的 fd
由于 select 调用返回后,fd_set 中只有发生事件的 fd 对应的位被保留,其余位会被清空,因此每次调用 select 之前都必须重新设置需要监控的 fd 集合。为此,通常的做法是:
-
使用一个数组(例如 array)独立保存所有需要监控的 fd。
-
每次调用 select 之前:
先调用 FD_ZERO 清空 fd_set。
遍历 array,用 FD_SET 将每个 fd 重新加入 fd_set。
同时遍历过程中计算出最大 fd 值(maxfd),作为 select 的第一个参数(maxfd+1)。 -
select 返回后,再遍历 array,对每个 fd 使用 FD_ISSET 判断是否发生了事件,从而进行相应的读写操作。
select缺点
-
每次调用都需要手动设置 fd 集合
由于 select 返回后会修改 fd_set(只保留发生事件的 fd),因此每次调用前都必须重新添加所有待监控的 fd。这既繁琐又容易出错,从接口易用性的角度来看非常不便。 -
用户态与内核态之间的频繁拷贝
每次调用 select 都需要将 fd 集合从用户态拷贝到内核态。当监控的 fd 数量很大时,这一拷贝操作的开销不可忽视。 -
内核中需要线性遍历所有 fd
select 在内核中实现时,会线性遍历传入的所有 fd,检查每个 fd 是否有事件发生。随着 fd 数量的增加,遍历开销呈线性增长,导致性能下降。 -
可监控的文件描述符数量太小
select 默认能够监控的 fd 数量受 FD_SETSIZE 限制(通常为 1024)。虽然可以通过修改宏重新编译内核来增大,但既不灵活,也无法满足高并发场景的需求。
select使⽤⽰例 使⽤select实现字典服务器
-
启动:监听 socket 创建成功,fd_array[0] 存入监听 fd。
-
主循环:
-
清空 rfds,根据 fd_array 中所有有效 fd 重新设置 rfds。
-
调用 select 阻塞等待读事件。
-
一旦有 fd 可读,select 返回。
-
EventDispatcher 遍历 fd_array,用 FD_ISSET 找出就绪的 fd。
-
如果是监听 fd → Accepter 接收新连接,把新 client fd 加入 fd_array。
-
如果是 client fd → Recver 读取请求,处理业务(此处为简单回显),若对方关闭则从 fd_array 移除。
-
循环往复:每次循环都重新构建 fd_set,保证新增或移除的 fd 能立即被监控或取消监控。
#pragma once
#include <iostream>
#include <string>
#include <memory>
#include <sys/select.h>
#include "Logger.hpp"
#include "InetAddr.hpp"
#include "Socket.hpp"
// gsize = fd_set 的 bit 位数,即可监控的最大文件描述符数量
// sizeof(fd_set) 是字节数,乘以 8 得到 bit 数
const static int gsize = sizeof(fd_set) * 8;
const static int gdefaultfd = -1;
class SelectServer
{
public:
// 构造函数:接收监听端口
SelectServer(uint16_t port)
: _listensock(std::make_unique<TcpSocket>()) // 创建 TcpSocket 对象,用基类指针管理
{
// 初始化监听套接字(socket, bind, listen)
_listensock->BuildListenSocketMethod(port);
// 初始化 fd_array 全部为 -1,表示空闲槽位
for (int i = 0; i < gsize; i++)
{
fd_array[i] = gdefaultfd;
}
// 将监听套接字的 fd 放入数组下标 0,交给 select 统一监控
fd_array[0] = _listensock->SockFd();
}
// 处理新连接:当监听套接字可读时调用
void Accepter()
{
InetAddr clientaddr;
// Accept 不会阻塞,因为 select 已经通知有连接就绪
int sockfd = _listensock->Accept(&clientaddr);
if (sockfd > 0)
{
// 找一个空闲槽位存放新连接的 fd
int pos = 0;
for (; pos < gsize; pos++)
{
if (fd_array[pos] == gdefaultfd)
{
fd_array[pos] = sockfd;
break;
}
}
// 如果数组满了,无法托管给 select,则直接关闭连接
if (pos == gsize)
{
close(sockfd);
LOG(LogLevel::WARNING) << "server is full, close new connection";
}
}
}
// 处理数据接收:当普通 socket 可读时调用
void Recver(int index)
{
int sockfd = fd_array[index];
char buffer[1024];
// recv 不会阻塞,因为 select 已经通知有数据可读
ssize_t n = recv(sockfd, buffer, sizeof(buffer) - 1, 0);
if (n > 0)
{
buffer[n] = '\0';
std::cout << "client say@ " << buffer << std::endl;
std::string echo_string = "server echo# ";
echo_string += buffer;
send(sockfd, echo_string.c_str(), echo_string.size(), 0);
}
else if (n == 0)
{
// 客户端关闭连接
fd_array[index] = gdefaultfd; // 清除槽位
close(sockfd);
}
else
{
// 出错
fd_array[index] = gdefaultfd;
close(sockfd);
}
}
// 就绪事件分发:根据 rfds 找出具体就绪的 fd
void EventDispatcher(fd_set &rfds)
{
for (int i = 0; i < gsize; i++)
{
if (fd_array[i] == gdefaultfd)
continue;
// 判断该 fd 是否在 rfds 集合中就绪
if (FD_ISSET(fd_array[i], &rfds))
{
// 如果是监听套接字就绪,说明有新连接到来
if (fd_array[i] == _listensock->SockFd())
{
Accepter(); // 连接管理器
}
else
{
Recver(i); // IO 处理器
}
}
}
}
// 主循环:运行 select 服务器
void Run()
{
while (true)
{
// ------------------- select 准备工作 -------------------
// 每次循环都需要重新构建 fd_set,因为 select 返回后集合会被内核修改
fd_set rfds;
FD_ZERO(&rfds);
int maxfd = gdefaultfd;
// 遍历 fd_array,将所有有效 fd 加入 rfds,同时计算最大 fd 值
for (int i = 0; i < gsize; i++)
{
if (fd_array[i] == gdefaultfd)
continue;
FD_SET(fd_array[i], &rfds);
if (maxfd < fd_array[i])
maxfd = fd_array[i];
}
// ------------------- 调用 select -------------------
// 参数1: maxfd+1 (监控的 fd 范围)
// 参数2: 读集合
// 参数3: 写集合 (nullptr 表示不关心)
// 参数4: 异常集合 (nullptr)
// 参数5: 超时时间 (nullptr 表示一直阻塞,直到有事件)
int n = select(maxfd + 1, &rfds, nullptr, nullptr, nullptr);
// --------------------------------------------------
switch (n)
{
case 0:
// 超时(本程序 timeout = nullptr,永远不会超时)
break;
case -1:
// select 调用出错
LOG(LogLevel::ERROR) << "select error";
break;
default:
// 有事件就绪,派发处理
EventDispatcher(rfds);
break;
}
}
}
private:
std::unique_ptr<Socket> _listensock; // 监听套接字(基类指针,实际指向 TcpSocket)
int fd_array[gsize]; // 辅助数组:存储所有需要监控的 fd
};

select 服务器的核心工作流
-
创建监听套接字(listen_fd),加入 select 的读集合。
-
调用 select 等待:
-
如果 listen_fd 就绪 → 调用 accept,得到新的 client_fd(已连接套接字)。
-
将这个 client_fd 也加入 select 的读集合(从此它也被监控)。
- 下次 select 返回时:
-
如果是 listen_fd 就绪 → 有新连接,继续 accept。
-
如果是某个 client_fd 就绪 → 调用 recv/read 读取数据,不会阻塞(因为已确认有数据)。
- 永不阻塞:因为 accept 只在有新连接时调用,recv 只在有数据时调用,不会在任何地方无故挂起。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐



所有评论(0)