select是 Linux 系统提供的多路 IO 复用系统调用,核心作用是:
- 让程序一次等待多个文件描述符(fd),而不是阻塞在单个read/write上
- 当被监听的fd中,有一个或多个发生状态变化(可读 / 可写 / 异常)时,select就会返回,通知程序处理就绪事件
- 本质是把 IO 的 "等" 和 "拷贝" 两步拆开:select负责 "等" 数据就绪,数据就绪后再调用read/write做 "拷贝"
1. select函数
1.1 函数原型
cpp
#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds,
struct timeval *timeout);
参数解释:
| 参数 | 含义 |
|---|---|
| nfds | 监听的最大文件描述符值 + 1,内核需要遍历到这个值 |
| readfds | 监听可读事件的 fd 集合(用户→内核传参,内核会修改) |
| writefds | 监听可写事件的 fd 集合 |
| exceptfds | 监听异常事件的 fd 集合 |
| timeout | 超时时间:NULL永久阻塞;0 非阻塞;>0 timeout时间内等待 |
关于timeval结构:
cpp
struct timeval
{
long tv_sec; // 秒
long tv_usec; // 微秒(1秒 = 1000000微秒)
};
返回值含义:
> 0:有事件就绪,返回值是就绪的 fd 总数= 0:超时,无任何事件就绪< 0:调用出错(如被信号中断、参数非法)
1.2 fd_set 位图操作宏
fd_set 是select用来批量管理文件描述符(fd)的核心数据结构,本质是一个固定大小的位图
- 每一个bit对应一个文件描述符编号
- bit 为
1:表示该 fd 被监听(用户关心它的事件) - bit 为
0:表示该 fd 不被监听
cpp
FD_ZERO(fd_set *set); // 清空集合,所有bit置0
FD_SET(int fd, fd_set *set); // 将fd加入集合,对应bit置1
FD_CLR(int fd, fd_set *set); // 将fd从集合移除,对应bit置0
FD_ISSET(int fd, fd_set *set); // 判断fd是否在集合中(是否就绪)
2. 理解select执行过程
- 准备阶段
- 用 FD_ZERO 清空 readfds/writefds/exceptfds 集合
- 用 FD_SET 把需要监听的 fd 加入对应的集合,同时记录最大的maxfd
- 用数组保存所有活跃 fd,避免每次重新扫描
- 调用 select
- 程序阻塞,内核遍历从 0 到maxfd的所有 fd,等待事件就绪或超时
- 内核会修改fd_set,清除无事件的 fd,仅保留就绪的 fd
- 返回处理
- select 返回后,用FD_ISSET遍历所有活跃 fd,找出就绪的 fd(位图bit位置:代表fd编号;位图bit值(0/1):代表 "是否关心这个 fd 的事件")
- 对就绪的 fd 调用 read/write 完成数据读写
- 重置集合
- 由于 fd_set 已被内核修改,下次调用 select 前必须重新FD_ZERO + FD_SET
3. socket就绪条件
这里重点研究读事件就绪
3.1 读就绪(POLLIN /readfds)
满足以下任意一种情况,fd 读就绪:
- 接收缓冲区数据 ≥ 低水位标记 SO_RCVLOWAT:可以无阻塞读,read返回值 > 0
- 对端关闭连接(FIN 包到来):此时read返回 0
- 监听 socket 有新连接请求:可以accept
- socket 上有未处理的错误
3.2 写就绪(POLLOUT /writefds)
满足以下任意一种情况,fd 写就绪:
- 发送缓冲区空闲空间 ≥ 低水位标记 SO_SNDLOWAT:可以无阻塞写,write返回值 > 0。
- socket 写端被关闭:再写会触发 SIGPIPE信号。
- 非阻塞 connect 成功 / 失败:连接建立完成。
- socket 上有未读取的错误
4. select的特点
特点
- fd 数量受限于
fd_set大小- 常见系统中sizeof(fd_set)字节,对应 4096 bit,因此默认最多监听 4096 个 fd
- 调整上限需修改内核并重新编译
- 必须额外维护 fd 数组
- select返回后,无事件的 fd 会被从集合中清空
- 必须用数组保存所有活跃 fd,每次调用前重新FD_SET,同时更新maxfd
- 兼容性强:几乎所有 Unix-like 系统都支持select,跨平台兼容性最好
5. select缺点
- 每次调用 select,都需要手动设置 fd 集合,从接口使用角度来说也非常不便
- 每次调用 select,都需要把 fd 集合从用户态拷贝到内核态,这个开销在 fd 很多时会很大
- 同时每次调用 select 都需要在内核遍历传递进来的所有 fd,这个开销在 fd 很多时也很大
- select 支持的文件描述符数量太小
6. select使用示例
检测标准输入输出
cpp
#include <stdio.h>
#include <unistd.h>
#include <sys/select.h>
int main()
{
fd_set read_fds;
FD_ZERO(&read_fds);
FD_SET(0, &read_fds);
for (;;)
{
printf("> ");
fflush(stdout);
int ret = select(1, &read_fds, NULL, NULL, NULL);
if (ret < 0)
{
perror("select");
continue;
}
if (FD_ISSET(0, &read_fds))
{
char buf[1024] = {0};
read(0, buf, sizeof(buf) - 1);
printf("input: %s", buf);
}
else
{
printf("error! invaild fd\n");
continue;
}
FD_ZERO(&read_fds);
FD_SET(0, &read_fds);
}
return 0;
}
当只检测文件描述符0(标准输入)时,因为输入条件只有在你有输入信息的时候,才成立,所以如果一直不输入,就会产生超时信息。
7. 实现SelectServer代码
思路:
SelectServer.hpp
cpp
#include <iostream>
#include <memory>
#include <sys/select.h>
#include "Socket.hpp"
using namespace SocketModule;
using namespace LogModule;
class SelectServer
{
public:
const static int size = sizeof(fd_set) * 8;
const static int defaultfd = -1;
SelectServer(int port)
: _listensock(std::make_unique<TcpSocket>()), _isrunning(false)
{
_listensock->BuildTcpSocketMethod(port);
for (int i = 0; i < size; i++)
{
_fd_array[i] = defaultfd;
}
_fd_array[0] = _listensock->Fd();
}
void Start()
{
_isrunning = true;
while (_isrunning)
{
// 把accept交给select
// 因为: listensockfd,也是一个fd,进程怎么知道listenfd上面有新连接到来了呢?
// auto res = _listensock->Accept(); // 我们在select这里,可以进行accept吗?
// 将listensockfd添加到select内部,让OS帮我关心listensockfd上面的读事件
// 每次进入循环,select都要把所有fd都加入位图
// 准备一个数组,存放之前所有
fd_set rfds;
FD_ZERO(&rfds);
int maxfd = defaultfd;
for (int i = 0; i < size; i++)
{
// 跳过无效/空位
if (_fd_array[i] == defaultfd)
{
continue;
}
FD_SET(_fd_array[i], &rfds);
if (maxfd < _fd_array[i])
{
maxfd = _fd_array[i];
}
}
PrintFd();
// 放入select
int n = select(maxfd + 1, &rfds, nullptr, nullptr, nullptr);
if (n < 0)
{
LOG(LogLevel::ERROR) << "select error";
continue;
}
else if (n == 0)
{
// 超时
LOG(LogLevel::INFO) << "time out...";
}
else
{
// 有事件就绪了,有可能是新到来的fd有可能是就绪
// 可以派发任务
Dispatcher(rfds);
}
}
_isrunning = false;
}
void Dispatcher(fd_set &rfds)
{
for (int i = 0; i < size; i++)
{
if (_fd_array[i] == defaultfd)
{
// 空的
continue;
}
if (FD_ISSET(_fd_array[i], &rfds)) // 检查这个fd是否已经读就绪了
{
if (_fd_array[i] == _listensock->Fd())
{
// 说明是新来的连接
Accepter();
}
else
{
// 派发任务
Recver(_fd_array[i], i);
}
}
}
}
void Accepter()
{
InetAddr client;
int sockfd = _listensock->Accept(&client);
if (sockfd < 0)
{
LOG(LogLevel::ERROR) << "accept error";
}
else
{
// accept到新连接,把新fd交给select
LOG(LogLevel::INFO) << "get a new link, sockfd: "
<< sockfd << ", client is: " << client.StringAddr();
int pos = 0;
for (; pos < size; pos++)
{
if (_fd_array[pos] == defaultfd)
break;
}
if (pos == size)
{
LOG(LogLevel::WARNING) << "select server full";
close(sockfd);
}
else
{
_fd_array[pos] = sockfd;
}
}
}
void Recver(int fd, int pos)
{
char buffer[1024];
ssize_t n = recv(fd, buffer, sizeof(buffer) - 1, 0);
if (n > 0)
{
buffer[n] = 0;
std::cout << "client say@ " << buffer << std::endl;
}
else if (n == 0)
{
LOG(LogLevel::INFO) << "clien quit...";
// 1. 把位图中fd不要就绪了
_fd_array[pos] = defaultfd;
// 2. 关闭fd
close(fd);
}
else
{
LOG(LogLevel::ERROR) << "recv error";
// 1. 把位图中fd不要就绪了
_fd_array[pos] = defaultfd;
// 2. 关闭fd
close(fd);
}
}
void PrintFd()
{
std::cout << "_fd_array[]:";
for (int i = 0; i < size; i++)
{
if (_fd_array[i] == defaultfd)
continue;
std::cout << _fd_array[i] << " ";
}
std::cout << "\r\n";
}
void Stop()
{
_isrunning = false;
}
~SelectServer()
{
}
private:
std::unique_ptr<Socket> _listensock;
bool _isrunning;
int _fd_array[size];
};