一、初识 select
系统提供 select 函数来实现多路复用输入/输出模型.
- select 系统调用是用来让我们的程序监视多个文件描述符的状态变化的;
- 程序会停在 select 这里等待,直到被监视的文件描述符有一个或多个发生了状态改变;
select 函数原型
C
#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set
*exceptfds, struct timeval *timeout);
参数解释:
nfds
是文件描述符集合中最大文件描述符加1。readfds
是指向需要监视读操作的文件描述符集合的指针。writefds
是指向需要监视写操作的文件描述符集合的指针。exceptfds
是指向需要监视异常条件的文件描述符集合的指针。timeout
是指定select
调用应该阻塞的最长时间。
函数返回值:
- 执行成功则返回文件描述词状态已改变的个数
- 如果返回 0 代表在描述词状态改变前已超过 timeout 时间,没有返回
- 当有错误发生时则返回-1,错误原因存于 errno,此时参数 readfds,writefds, exceptfds 和 timeout 的值变成不可预测。
错误值可能为:
- EBADF 文件描述词为无效的或该文件已关闭
- EINTR 此调用被信号所中断
- EINVAL 参数 n 为负值。
- ENOMEM 核心内存不足
参数 timeout 取值:
- NULL:则表示 select()没有 timeout,select 将一直被阻塞,直到某个文件 描述符上发生了事件;
- 0:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生。
- 特定的时间值:如果在指定的时间段里没有事件发生,select 将超时返回。
关于 fd_set 结构
其实这个结构就是一个整数数组, 更严格的说, 是一个 "位图". 使用位图中对应的位来表示要监视的文件描述符.
提供了一组操作 fd_set 的接口, 来比较方便的操作位图.
C
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。
二、理解 select 执行过程
想要理解select最主要要理解fd_set结构,fd_set的本质其实是一张位图,他的每一个比特位可以表示一个文件描述符
我们使用select时,需要手动输入告诉内核,要关心哪一些fd,比特位的位置表示文件描述符的编号,比特位的内容表示是否关心这个fd,例如我们要select等待编号为4 5 6号的fd,此时的fd_set内容 ...0111 0000
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set
*exceptfds, struct timeval *timeout);
其实select中的中间几个参数既是输入型参数,也是输出型参数,输入很容易理解,例如上述的例子,用户需要告诉内核哪些需要关心。对于输出来说,select会等待用户关心的fd,等其中的一个或多个事件就绪的话,select就可以通过这些参数来返回,告诉用户具体是关心的哪些fd事件就绪了,它具体的做法是将传入的fd_set结构除了就绪的fd为1,其他的置为0。
这样可能就会出现一种情况,可能关心的有的fd还没有就绪,但是它却被置为0了,所以每次在调用select时就需要我们再次设置要关心的fd,通常我们需要依赖一种数据结构,通常为数组
三、select的特点
可监控的文件描述符个数取决于 sizeof(fd_set)的值. 我这边服务器上 sizeof(fd_set)=512,每 bit 表示一个文件描述符,则我服务器上支持的最大文件描述 符是 512*8=4096.
将 fd 加入 select 监控集的同时,还要再使用一个数据结构 array 保存放到 select 监控集中的 fd,
- 一是用于再 select 返回后,array 作为源数据和 fd_set 进行 FD_ISSET 判 断。
- 二是 select 返回后会把以前加入的但并无事件发生的 fd 清空,则每次开始 select 前都要重新从 array 取得 fd 逐一加入(FD_ZERO 最先),扫描 array 的同时 取得 fd 最大值 maxfd,用于 select 的第一个参数。
fd_set 的大小可以调整,可能涉及到重新编译内核. 感兴趣可以自己去收集相关资料.
select 缺点
- 每次调用 select, 都需要手动设置 fd 集合, 从接口使用角度来说也非常不便.
- 每次调用 select,都需要把 fd 集合从用户态拷贝到内核态,这个开销在 fd 很 多时会很大
- 同时每次调用 select 都需要在内核遍历传递进来的所有 fd,这个开销在 fd 很多时也很大
- select 支持的文件描述符数量太小
四、select 使用示例
#include <iostream>
#include <sys/select.h>
#include <string>
#include "socket.hpp"
using namespace socket_ns;
class SelectServer
{
const static int gnum = sizeof(fd_set) * 8;
const static int gdefaultfd = -1;
public:
SelectServer(uint16_t port)
: _port(port), _listensock(std::make_unique<TcpSocket>())
{
_listensock->BuildListenSocket(_port);
}
~SelectServer()
{
}
void Init()
{
//初始化select辅助数组
for (int i = 0; i < gnum; i++)
{
_select_array[i] = gdefaultfd;
}
//这里是直接将listen套接字的fd加入到数组中
_select_array[0] = _listensock->Sockfd();
}
void Accepter()
{
InetAddr addr;
int sockfd = _listensock->Accepter(&addr);
if (sockfd > 0)
{
LOG(DEBUG, "get a new link, client info %s:%d\n", addr.IP().c_str(), addr.Port());
// 将sockfd添加到select辅助数组中
bool flag = false;
for (int pos = 1; pos < gnum; pos++)
{
if (_select_array[pos] == gdefaultfd)
{
flag = true;
_select_array[pos] = sockfd;
LOG(INFO, "add %d to fd_array success!\n", sockfd);
break;
}
}
// select可以等待的fd是有限的
if (!flag)
{
LOG(WARNING, "Server Is Full!\n");
::close(sockfd);
}
}
}
void HanderIO(int i)
{
char buffer[1024];
ssize_t n = ::recv(_select_array[i], buffer, sizeof(buffer) - 1, 0);
if (n > 0)
{
buffer[n] = 0;
std::cout << "client say# " << buffer << std::endl;
std::string responsestr = "HTTP/1.1 200 OK\r\n";
responsestr += "Content-Type: text/html\r\n";
responsestr += "\r\n";
responsestr += "<html><h1>hello Linux</h1></html>";
::send(_select_array[i], responsestr.c_str(), responsestr.size(), 0);
}
else if (n == 0)
{
LOG(INFO, "client quit...\n");
// 关闭fd
::close(_select_array[i]);
// select 不要在关心这个fd了
_select_array[i] = gdefaultfd;
}
else
{
LOG(ERROR, "recv error\n");
// 关闭fd
::close(_select_array[i]);
// select 不要在关心这个fd了
_select_array[i] = gdefaultfd;
}
}
void HandlerEvent(fd_set rfds)
{
//事件派发(遍历rfds看哪些fd就绪了,根据fd不同的类型处理不同的事件)
for (int i = 0; i < gnum; i++)
{
if (_select_array[i] == gdefaultfd)
continue;
// 是关心的fd,但不一定就绪了,接下来检测他是否在rfds中
if (FD_ISSET(_select_array[i], &rfds))
{
// 检测是listenfd还是普通的fd
if (_listensock->Sockfd() == _select_array[i])
{
// 此时可以accept了,一定不会等了
Accepter();
}
else
{
HanderIO(i);
}
}
}
}
void Loop()
{
while (true)
{
// 设置文件描述符
fd_set rfds;
FD_ZERO(&rfds);
int max_fd = gdefaultfd;
for (int i = 0; i < gnum; i++)
{
if (_select_array[i] != gdefaultfd)
{
FD_SET(_select_array[i], &rfds);
if (_select_array[i] > max_fd)
{
max_fd = _select_array[i];
}
}
}
struct timeval timeout = {30, 0};
int n = ::select(max_fd + 1, &rfds, nullptr, nullptr, nullptr);
switch (n)
{
case 0:
LOG(DEBUG, "time out, %d.%d\n", timeout.tv_sec, timeout.tv_usec);
break;
case -1:
LOG(ERROR, "select error\n");
break;
default:
LOG(INFO, "haved event ready, n : %d\n", n); // 如果事件就绪,但是不处理,select会一直通知我,直到我处理了!
HandlerEvent(rfds);
break;
}
}
}
private:
uint16_t _port;
SockSPtr _listensock;
int _select_array[gnum];
};