一、初识 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还是普通的fdif (_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];
};