目录
select的作用和定位
select函数
理解select执行过程
select的特点
select的缺点
select使用示例
poll
select的作用和定位
定位:在IO中,只负责进行等,不负责拷贝。
多路转接的作用是,为了等待多个fd,等待fd上面的新事件就绪(OS底层有数据--读事件就绪,或OS底层有空间--写事件就绪),通知程序员,事件已经就绪,就可以进行IO拷贝了。
select函数
- 参数nfds是用户等待的多个fd中的最大值+1。(假如fd是1、3、5、7、9,那nfds是9+1=10)
- 参数timeout是一个输入输出型参数,是一个struct timeval的结构体,里面有两个成员,是一个时间戳类的结构体。假设定义一个timeval的对象,timeval timeout = {5, 0},意思就是告诉select,多路复用啊你现在帮我监视多个文件描述符,策略是5s以内一直阻塞,5s以内如果没有任何一个文件描述符就绪,给我返回一次;如果5s以内,比如第2s,等待的多个文件描述符有多个就绪了,你也给我返回,并且你的timeval里一定要记录下来剩余还有多少时间,假设还剩3s,timeval里面就是{3,0},也就是5s以内阻塞等待,5s过后非阻塞轮询一次。如果timeval timeout = {0, 0},就是让select去等,如果没有就绪立马返回,这就是非阻塞轮询。如果timeval的值设为NULL,表示让select永久阻塞。
- 返回值,大于0,表示有几个就绪;等于0,表示超时;小于0,表示select出错。
第2、3、4个参数的类型是fd_set,这个数据类型是OS提供的,表示文件描述符集。实际上,fd_set是一个位图结构,那就存在两方面内容,1.比特位的位置:表示的就是文件fd的值,2.比特位的内容,0还是1。
- 第2、3、4个参数,是输入输出型参数,readfds只关心读事件,writefds只关心写事件,exceptfds只关心异常事件。对于某一个文件描述符,看关心读事件、写事件、异常事件从而加入到对应的fd_set。
我们先关心读事件文件描述符集,当输入这个参数时,就是用户告诉内核,你要帮我关心fd_set集合中的所有fd读事件哦。比特位的位置:文件描述符的编号;比特位的内容:是否关心该fd的事件。当这个参数输出时,就是内核告诉用户,你让我关心的fd_set集合中,都有哪些已经就绪了。比特位的位置:文件描述符的编号;比特位的内容:对应的fd,事件是否发生。如果只有某些文件描述符就绪,那下次select时,可能要求我们每次调用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。
常用的代码片段如下:
fd_set rfds;
FD_ZERO(&rfds);
FD_SET(fd, &rfds);
::select(fd+1, &rfds, nullptr, nullptr, nullptr);
if(FD_ISSET(fd, &rfds)){......}
理解select执行过程
关键在于理解fd_set,为方便理解,取fd_set长度为1字节,fd_set中的每一bit可以对应一个文件描述符fd。则1字节长的fd_set最大可以对应8个fd。
- 执行fd_set set; FD_ZERO(&set);则set用位表示是0000 0000。
- 若fd=5,则执行FD_SET(fd, &set);然后set变为0001 0000(第5位置1)。
- 若再加入fd=2,fd=1,则set变为0001 0011。
- 执行select(6, &set, nullptr, nullptr, nullptr)。
- 若fd=1,fd=2上都发生可读事件,则select返回,此时set变为0000 0011。
select要正常工作,需要借助一个辅助数组,来保存所有合法fd。
select的特点
- 可监控的文件描述符个数取决于sizeof(fd_set)*8的值,可能是1024,每一个bit表示一个文件描述符。
- 将fd加入select监控集的同时,还要再使用一个fd_array数组保存放到select中的fd。这个fd_array的作用一是用于在select返回后,fd_array作为源数据和fd_set进行FD_ISSET判断;二是select返回后会把以前加入的但无事发生的fd清空,则每次开始select前都要重新从array取得fd逐一加入,扫描array的同时取得fd最大值maxfd,用于select的第一个参数。
select的缺点
- 每次调用select,都要手动设置fd集合,非常不便。
- 每次调用select,都要把fd集合从用户态拷贝到内核态,开销很大。
- 每次调用select都需要在内核遍历传来的所有fd,开销很大。
- select支持的文件描述符数量太少。
select使用示例
#pragma once
#include <iostream>
#include <memory>
#include <sys/select.h>
#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), _listensocket(std::make_unique<TcpSocket>()){_listensocket->BuildListenSocket(_port);}void InitServer(){for (int i = 0; i < gnum; i++){fd_array[i] = gdefaultfd;}fd_array[0] = _listensocket->Sockfd(); // 默认添加listensock到数组中}// 处理新连接void Accepter(){// 我们叫做连接事件就绪,等价于读事件就绪InetAddr addr;int sockfd = _listensocket->Accepter(&addr); // 会不会被阻塞?一定不会!if (socket > 0){LOG(DEBUG, "get a new link, client info %s : %d\n", addr.Ip(), addr.Port());// 已经获得了一个新的sockfd// 接下来可以读取吗?绝对不能读!读取的时候,条件不一定满足// 谁最清楚底层fd的数据是否已经就绪了呢?通过select!// 想办法把新的fd添加给select,由select统一进行监管// select为什么等待的fd越来越多?// 只要将新的fd,添加到fd_array中即可!bool flag = false;for (int pos = 1; pos < gnum; pos++){if (fd_array[pos] == gdefaultfd){flag = true;fd_array[pos] = sockfd;LOG(INFO, "add %d to fd_array success!\n", sockfd);break;}}if (!flag){LOG(WARNING, "Server is Full!\n");::close(sockfd);}}else{return;}}// 处理新IOvoid HandlerIO(int i){// 普通的文件描述符,正常读写char buffer[1024];ssize_t n = ::recv(fd_array[i], buffer, sizeof(buffer) - 1, 0); // 这里读取会阻塞吗?不会if (n > 0){buffer[n] = 0;std::cout << "client say#" << buffer << std::endl;std::string echo_str = "[ server echo info ]";echo_str += buffer;::send(fd_array[i], echo_str.c_str(), sizeof(echo_str), 0);}else if (n == 0){LOG(INFO, "client quit...\n");// 关闭fd// select下次不要再关心这个fd了::close(fd_array[i]);fd_array[i] = gdefaultfd;}else{LOG(ERROR, "recv error\n");// 关闭fd// select下次不要再关心这个fd了::close(fd_array[i]);fd_array[i] = gdefaultfd;}}// 一定会存在大量的fd就绪,可能是普通sockfd,也可能是listensockfdvoid HandlerEvent(fd_set &rfds){//事件派发for (int i = 0; i < gnum; i++){if (fd_array[i] == gdefaultfd)continue;// fd一定是合法的fd// 合法的fd不一定就绪,判断fd是否就绪?if (FD_ISSET(fd_array[i], &rfds)){// 读事件就绪// 1.listensockfd 2.normal sockfdif (_listensocket->Sockfd() == fd_array[i]){Accepter();}else{HandlerIO(i);}}}}void Loop(){while (true){// 1.文件描述符进行初始化fd_set rfds;FD_ZERO(&rfds);int max_fd = gdefaultfd;// 2. 合法的fd,添加到rfds集合中for (int i = 0; i < gnum; i++){if (fd_array[i] == gdefaultfd)continue;FD_SET(fd_array[i], &rfds);// 2.1更新出最大的文件fd值if (max_fd < fd_array[i]){max_fd = fd_array[i];}}struct timeval timeout = {3, 0};//_listensocket->Accepter(); // 不能,listensock && accept 我们把他看成IO类的函数,只关心新连接的到来,等价于读事件就绪int n = ::select(max_fd + 1, &rfds, nullptr, nullptr, &timeout); // 临时switch (n){case 0:LOG(DEBUG, "timeout, %d.%d\n", timeout.tv_sec, timeout.tv_usec);break;case -1:LOG(ERROR, "select error\n");break;default:LOG(INFO, "have event ready, n : %d\n", n); // 如果事件就绪,但是不处理,select就会一直通知我,知道处理了。HandlerEvent(rfds);PrintDebug();sleep(1);break;}}}void PrintDebug(){std::cout << "fd list: ";for (int i = 0; i < gnum; i++){if (fd_array[i] == gdefaultfd)continue;std::cout << fd_array[i] << " ";}std::cout << "\n";}~SelectServer() {}private:uint16_t _port;std::unique_ptr<Socket> _listensocket;// select要正常工作,需要借助一个辅助数组,来保存所有合法fdint fd_array[gnum];
};
poll
poll的出现解决了select的两个问题:1.支持的文件描述符太少;2.每次都要对文件描述符集重置。
作用:和select一样。
定位:只负责等,一旦等待就绪,就会事件派发。
参数timeout和select中的类似,但是以毫秒为单位设定的超时时间,这个参数只做输入,并不能传出剩余时间;timeout=0表示非阻塞,timeout=-1表示阻塞;返回值ret>0,表示有几个fd就绪了,ret=0,表示超时,ret<0,poll出错。第一个参数fds表示“数组”起始地址,第二个参数表示该“数组”元素个数。 struct pollfd是什么呢?是一个结构体:
这个结构体有3个字段,short events表示事件类型,有16个bit位,下面的宏名称占据不同的比特位,未来想让该文件描述符设置对某些事件的关心,就可以这样,events=POLLIN|POLLOUT。 poll也要做到,1.用户告诉内核,你要帮我关心哪些fd上的哪些时间(int fd,short events) 2.内核告诉用户,你让我关心的哪些fd上的哪些事件已经就绪了(int fd,short revents)。这样就不需要因为接口设计的缺陷,对fd和关心的事件进行重新设定了。
events和revents的取值是:
这些取值就是宏。
“数组”大小nfds可以自己随意设置,所以poll等待的文件描述符理论上没有上限,解决了文件描述符数量太少的问题。
但是poll同样有一个严重的缺点,poll底层也需要遍历所有的fd,来获取就绪的fd和它的事件。