初识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()的等待时间
除过第一个参数,其他的都是输入输出型参数,需要提前设置输入值,也会返回调用后的输出值
timeval结构
描述一段时间长度,时间戳函数就是这种结构,需要监视的描述符没有事件发生函数返回,返回值为0
timeout取值
如果设置了,就是输入输出型参数
NULL,没有timeout,将一直没阻塞,直到某个文件描述符发生了事件
0:仅检测描述符集合的状态,然后立即返回,不等待外部事件的发生
特定的时间值:如果在指定的时间段里没有事件发生,将超时返回,有发生立即返回。如【5,0】,表示每隔5秒timeout一次返回,如果发生立即返回,返回值是剩余的时间
fd_set结构
这个结构是一个整数数组,更严格的说是位图,比特位的位置表示文件描述符编号,可以关注的事件有读写异常,每个都是fd_set位图结构,值是0或1。输入时,用户告诉内核,给一个或多个fd,需要关心他们的事件,如果就绪了通知我。将对应比特位设置为1表示需要关注的fd编号。输出时,内核告诉用户,让自己关系你的多个fd中,哪些就绪了,可以读写。注定会有大量的位图操作,所以提供了一些接口
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的全部位
函数返回值:
执行成功返回文件描述词状态已改变的个数
如果返回0表示改变前已超过timeout时间,没有返回
有错误发生返回-1,原因存于errno,测试readfds,writefds,exceptfds和timeout值不可预测
错误值可能为:
EBADF,文件描述符无效或已关闭
EINTR,此调用被信号所终端
EINVAL,参数n为负值
ENOMEM,核心内存不足
执行过程
理解select模型的关键在于理解fd_set,为了方便说明,长度取1字节,fd_set每个bit可以对应一个文件描述符fd,1字节最大可以对应8个fd
(1)执行fd_set set; FD_ZERO(&set);则set用位表示是0000,0000。 *(2)若fd=5,执行FD_SET(fd,&set);后set变为0001,0000(第5位置为1) *(3)若再加入fd=2,fd=1,则set变为0001,0011 *(4)执行select(6,&set,0,0,0)阻塞等待 *(5)若fd=1,fd=2上都发生可读事件,则select返回,此时set变为0000,0011。注意:没有事件发生的fd=5被清空。
就绪条件
读就绪
socket内核中,接收缓冲区的字节数,大于等于低水位标记SO_RCVLOWAT,此时可以无阻塞的读该文件描述符,并且返回值大于0
socket tcp通信中,对端关闭连接,此时对socket读,返回0
监听的socket上有新的连接请求
socket有未处理的错误
写就绪
socket内核中,发送缓冲区的可用数字(发送缓冲区的空闲位置大小),大于等于低水位标记SO_SNDLOWAT,此时可以无阻塞的写,并且返回值大于0
socket的写操作被关闭(close或者shutdown),对一个写操作被关闭的socket写,会触发SIGPIPE信号
socket使用非阻塞connet连接成功或失败后
socket有未读取的错误
异常就绪
socket上收到带外数据,带外数据和tcp紧急模式相关
特点
可监控的文件描述符个数取决于sizeof(fd_set)的值,可能是512,每bit表示一个文件描述符,所以最大支持的是512*8=4096
将fd加入select监控集的同时,还要使用一个数据结构array保存放到select监控集合的fd
- 一是用于再select返回后,array作为原数据和fd_set进行FD_INSET判断
- 二是select返回后会把先前加入的但并无事件发生的fd情况,则每次开始select前都要重新从array取得fd逐一加入(FD_ZERO最先),扫描array的同时取得最大值maxfd,用于select第一个参数
select服务器
实现一个简单的服务器,监听套接字,运行的时候设置读位图,用select轮询,有新链接的时候处理
新连接如果不拿取会反复通知
用一个数组记录需要监听的套接字,初始值都设为-1,开始时将监听套接字加入0号下标
开始轮询,遍历数组需要设置的套接字放入fd_set,同时更新最大的套接字maxfd。select判断套接字状态,大于0时说明有事件就绪,开始处理。这时需要遍历输出参数fd_set,和套接字数组对比,哪个就绪了处理哪个。
a. 如果是监听套接字,说明有新连接,用accept函数获取新套接字,加入到套接字数组中。
b. 另一种情况是读套接字就绪,read读数据并打印,如果出错或者返回0,关闭套接字,从套接字数组中移除
SelectServer.hpp
#include "Socket.hpp"
#include "log.hpp"static const uint16_t defaultport = 8000;
static const int fd_max = (sizeof(fd_set) * 8);
int defaultfd = -1;class SelectServer
{
public:SelectServer(){for (int i = 0; i < fd_max; i++){_fd_ary[i] = defaultfd;}}bool Init(){_listensocket.Socket();int opt = 1;setsockopt(_listensocket._sockfd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));_listensocket.Bind(defaultport);_listensocket.Listen();return true;}void Accepter(){cout << "get a new link" << endl;std::string clientip;uint16_t clinetport = 0;int sock = _listensocket.Accept(&clientip, &clinetport);if (sock < 0){return;}lg.logmessage(info, "accept success,%s:%d.%d", clientip.c_str(), clinetport, sock);// 添加sockint pos = 1;for (; pos < fd_max; pos++){if (_fd_ary[pos] != defaultfd){continue;}else{break;}}if (pos == fd_max){lg.logmessage(warning, "server is full,close %d", sock);close(sock);}else{_fd_ary[pos] = sock;// 打印查看fdaryPrintFd();}}void Recver(int fd, int pos){char buff[1024];ssize_t n = read(fd, buff, sizeof(buff) - 1);if (n > 0){buff[n] = 0;cout << "get message:" << buff << endl;}else if (n == 0){lg.logmessage(info, "client quit, me too, fd:", fd);close(fd);_fd_ary[pos] = defaultfd; // 这里本质是移除}else{lg.logmessage(warning, "recv error,fd:", fd);close(fd);_fd_ary[pos] = defaultfd; // 这里本质是移除}}void Dispatcher(fd_set &rfds){// 遍历找出就绪描述符for (int i = 0; i < fd_max; i++){int fd = _fd_ary[i];if (fd == defaultfd)continue;if (FD_ISSET(fd, &rfds)){// 监听就绪if (fd == _listensocket._sockfd){Accepter();}else // 读就绪{Recver(fd, i);}}}}void Start(){// listen套接字设置第一个int listensock = _listensocket.Fd();_fd_ary[0] = listensock;for (;;){fd_set rfds;FD_ZERO(&rfds);// 取maxfdint maxfd = _fd_ary[0];for (int i = 0; i < fd_max; i++) // 第一次循环{if (_fd_ary[i] == defaultfd)continue;// 有值,设置进fd_setFD_SET(_fd_ary[i], &rfds);if (maxfd < _fd_ary[i]){maxfd = _fd_ary[i];lg.logmessage(info, "maxfd:%d", maxfd);}}// 不能直接accpet,检测listensocket的事件,新链接到来等价于读就绪struct timeval timeout = {0, 0}; // 输入输出需要重新设置int n = select(maxfd + 1, &rfds, nullptr, nullptr, nullptr);// 就绪不处理,会一直通知,这时读取不会阻塞switch (n){case 0:cout << "timeout," << timeout.tv_sec << "." << timeout.tv_usec << endl;break;case -1:cerr << "select error" << endl;break;default:// 有事件就绪,todoDispatcher(rfds);break;}}}void PrintFd(){cout << "online fd list: ";for (int i = 0; i < fd_max; i++){if (_fd_ary[i] == defaultfd)continue;cout << _fd_ary[i] << " ";}cout << endl;}~SelectServer(){_listensocket.Close();}private:Sock _listensocket;int _fd_ary[fd_max];
};
SelectServer.cc
#include "SelectServer.hpp"
#include <memory>int main()
{std::unique_ptr<SelectServer> svr(new SelectServer());svr->Init();svr->Start();
}
优缺点
优点
跨平台性好,几乎所有的操作系统都支持 select。
简单易用,接口简洁,易于理解和上手
缺点
输入输出型参数较多,每次调用,都需要手动设置fd集合,从接口使用角度来说非常不便
输入输出型参数较多,每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时很大
同时每次调用都需要在内核遍历传递进来的所有fd,内核中检测也需要遍历,开销在fd很多时很大
select支持的文件描述符数量太小,有上限