【Linux】多路转接select
目录
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;
}
}
// 处理新IO
void 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,也可能是listensockfd
void 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 sockfd
if (_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要正常工作,需要借助一个辅助数组,来保存所有合法fd
int 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和它的事件。