欢迎光临
我们一直在努力

【Linux】多路转接select/poll

目录

  • 一、多路转接select
    • 1.2 select是什么?
    • 1.2 初识select
  • 二、select版本EchoServer服务器
    • 2.1 搭建框架
    • 2.2 初步完善Loop接口
    • 2.3 初步完善HandlerEvent接口
    • 2.4 事件式编程
    • 2.5 select原理及优缺点
      • 原理
      • select的缺点
  • 三、多路转接poll
    • 3.1 poll版本EchoServer服务器

在这里插入图片描述

个人主页:矢望 个人专栏:C++、Linux系统编程、Linux网络编程、C语言、数据结构、Coze-AI、MySQL

一、多路转接select

1.2 select是什么?

上期博客讲解了五种IO模型,其中就包括多路转接/多路复用。 在这里插入图片描述 如上图,IO=等+拷贝。 IO多路复用:应用程序将多个文件描述符注册到选择器,阻塞在select/poll/epoll上等待任意一个就绪,就绪后再逐个阻塞拷贝数据。

多路复用技术就是一些系统调用只负责等的过程,然后拷贝的过程交给read/write/recvfrom系统调用来做。 而负责等待的系统调用如select能一次性等待多个fd是否可读或可写,它们的核心工作就是等,目标是检测到读写事件就绪。 因此它们能够减少IO过程中,单位时间内等待的比重,提高IO效率。

1.2 初识select

select系统调用让进程同时监听多个文件描述符,当其中任意一个或多个读/写/异常事件就绪时立即返回,并告知哪些fd已就绪,进程随后进行阻塞式拷贝。

在这里插入图片描述

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

参数类型作用
nfds int 所有被监听fd中的最大值 + 1,用于限定内核轮询范围,提升效率。
readfds fd_set * 指向可读事件文件描述符集合的指针。传入时指定要监听的fd,返回时内核修改此集合,只保留就绪的fd。
writefds fd_set * 指向可写事件文件描述符集合的指针。传入时指定要监听的fd,返回时内核修改此集合,只保留就绪的fd。
exceptfds fd_set * 指向异常事件文件描述符集合的指针(如带外数据)。传入时指定要监听的fd,返回时内核修改此集合,只保留就绪的fd。
timeout struct timeval * 设置超时时间。传入NULL表示无限阻塞;传入0表示非阻塞轮询;传入具体值表示等待指定时间后超时返回。

  • 返回值:>0表示就绪fd总数;==0表示超时;==-1表示出错。
  • fd_set:本质是位图,通过FD_SET、FD_CLR、FD_ISSET、FD_ZERO四个宏操作。
  • 输入输出二合一:三个fd_set参数既是输入(告诉内核要监听谁),也是输出(内核告诉进程谁就绪),所以每次调用select前都必须重新设置所有关心的fd。

select的第一个参数表示被监听的多个文件描述符fd的最大值加1。 第二、三、四个参数表示文件描述符集,分别是读文件描述符集、写文件描述符集、异常文件描述符集。是什么集就是关心什么事件就绪。并且第二、三、四个参数还是输入输出型参数。 它们是通过位图的形式添加文件描述符的。比如给1、3、5、7设置检测读事件,那么位图readfds就是10101010,如果检测之后发现只有1、7读事件就绪,那么readfds就是10000001。并且你设置了那些,它就会关心那些,即使其它的没有设置的fd就绪了,它也不会在位图中标注的,只会标注你关心的文件描述符里就绪的。 第五个参数表示的是等待的方式,传NULL表示阻塞等待,传0表示非阻塞等待,传入特定值表示等待指定时间后超时返回。

此外因为要设置位图,向位图结构中添加文件描述符等操作,所以就必须给我们提供另一些接口来设置位图: 在这里插入图片描述 这4个宏专门用于操作fd_set位图,是管理select监听集合的必备工具:

  • FD_ZERO(fd_set *set):将整个位图全部清零,用于初始化一个空的fd集合。
  • FD_SET(int fd, fd_set *set):将位图中指定fd对应的比特位置1,表示将其加入监听集合。
  • FD_CLR(int fd, fd_set *set):将位图中指定fd对应的比特位置0,表示将其移出监听集合。
  • FD_ISSET(int fd, fd_set *set):检查位图中指定fd对应的比特位是否为1,用于在select返回后判断该fd是否就绪。

ZERO清零,SET添加,CLR移除,ISSET检查。

注意:一般我们不会同时关心读写,因为写事件一般都是就绪的,以为缓冲区一开始有空间。所以一般比较关心先读后写,或先写后读。写事件一般按需设置。

对于一个fd,如果设置到readfds中表示关心它的读事件,设置到writefds表示关心它的写事件,设置到exceptfds表示关心它的异常事件。可以同时把fd添加到多个位图中,表示同时关心。

注意:每次调用select,都需要重新设置fd事件集。

二、select版本EchoServer服务器

通过多路转接技术写一个基于tcp的echo server服务器,之前写过很多次了。 tcp进行网络通信,最开始的时候只有一个fd就是监听socket,随着客户端连接的增多,fd也会逐渐增多。

而在刚开始的时候,服务端不能直接调用accept获取连接。因为要使用多路转接技术,所以 select的定位就是一切fd上需要等待的事情都由select来做,事件就绪之后再直接调用接口拷贝即可。 accept=等+获取连接,等的工作由select来进行,获取连接这件事,是读事件。

2.1 搭建框架

之前的博客写过很多的日志模块,封装的Socket模块等,这里就直接拿来用了。

Main.cc:

int main(int argc, char *argv[])
{
if(argc != 2)
{
std::cerr << "Usage:\\n\\t";
std::cerr << argv[0] << " port" << std::endl;
}

uint16_t port = std::stoi(argv[1]);

// 定义网络对象
std::unique_ptr<SelectServer> svr = std::make_unique<SelectServer>(port);

// 启动服务器
svr->Loop();

return 0;
}

如上,是主函数的逻辑,创建网络对象,启动服务器即可。

SelectServer.hpp:

class SelectServer
{
public:
SelectServer(uint16_t port = 8080)
: _port(port)
, _isquit(false)
, _listensock(std::make_unique<TcpSocket>())
{
_listensock->BuildTcpSocketMethod(_port); // 调用模板方法,创建套接字、绑定、监听
LOG(LogLevel::INFO) << "create listensock success, fd: " << _listensock->Sockfd();
}

void Loop()
{
while(!_isquit)
{

}
}

~SelectServer()
{}
private:
uint16_t _port; // 端口号
bool _isquit; // 标识服务器是否退出
std::unique_ptr<Socket> _listensock; // 监听socket
};

如上,调用了之前写的创建tcp socket的模板方法。

框架搭建完成了,接下来就是完善Loop接口

2.2 初步完善Loop接口

之前分析过了,创建好监听socket之后,接下来不能直接调用accept接口,因为它的等待工作需要select处理,读事件就绪之后再调用accept获取连接。

使用select关心listenfd的读事件,需要创建fd_set对象,然后使用提供的接口添加标志位到fd_set对象中。

void Loop()
{
while(!_isquit)
{
fd_set rfds; // 读事件集
FD_ZERO(&rfds); // 清零

FD_SET(_listensock->Sockfd(), &rfds); // 添加fd
// 只关心读事件且第五个参数等待方式设为阻塞等待
int n = select(_listensock->Sockfd() + 1, &rfds, nullptr, nullptr, nullptr);

if(n == 0) // 超时,等待方式设为超时模式时使用
{
LOG(LogLevel::INFO) << "timeout…";
}
else if(n == 1) // 出错
{
LOG(LogLevel::WARNING) << "select error!";
}
else // 就绪 n 个
{
LOG(LogLevel::INFO) << "event ready! n = " << n;
HandlerEvent(rfds); // 处理
}
}
}

如上,while循环进行select检测,由于开始只有一个fd,所以select的第一个参数就是fd+1。然后检测它的读事件,如果事件就绪就调用HandlerEvent接口处理读事件。 暂且把处理接口注释掉然后编译测试一下:

没有连接到来时: 在这里插入图片描述 如上,没有连接到来时,服务器的select接口阻塞等待。 在这里插入图片描述 如上,有连接到来后,读事件就绪,由于没有调用accept获取上来,所以select一直检测到读事件就绪,疯狂打印提醒。

2.3 初步完善HandlerEvent接口

调用HandlerEvent接口要获取新连接了。

void HandlerEvent(fd_set &rfds)
{
if(FD_ISSET(_listensock->Sockfd(), &rfds))
{
// 监听套接字读事件就绪了
LOG(LogLevel::INFO) << "listensockfd event ready!";
InetAddr clientaddr;
int sockfd = _listensock->Accepter(clientaddr); // 获取连接
LOG(LogLevel::INFO) << "get a new link, sockfd is :" << sockfd << clientaddr.ToString();
// …
}
}

通过FD_ISSET接口判断返回后的rfds事件集里listenfd的位置是不是1,如果是1表示listenfd的事件就绪了,此时再调用accept就不需要等待了,直接获取连接即可。

编译运行: 在这里插入图片描述 如上,在监听sockfd读事件就绪后,立即调用accept获取了连接。

接下来该进行的是和客户端通信,但是通信依旧要根据新获取的sockfd读或写数据,那么调用系统调用send、recv也是等+拷贝呀,等待的工作需要select来做!所以当前不能直接和客户端通信,需要把新获取的sockfd添加到事件集中由select进行等待工作! 所以代码就需要更改了。

所以在当前类中就需要一个数组来存储所有的fd,等到检测的时候一同添加到读事件集中,让select同时检测数组中的所有fd。

const int gfdnums = sizeof(fd_set)*8; // 事件集标志位的总数量

class SelectServer
{
public:
SelectServer(uint16_t port = 8080)
: _port(port)
, _isquit(false)
, _listensock(std::make_unique<TcpSocket>())
{
_listensock->BuildTcpSocketMethod(_port); // 调用模板方法,创建套接字、绑定、监听
LOG(LogLevel::INFO) << "create listensock success, fd: " << _listensock->Sockfd();

// 设置fd数组
for(int i = 0; i < gfdnums; i++)
_rfdset[i] = 1;

// 添加listenfd
_rfdset[0] = _listensock->Sockfd();
}

// …
private:
uint16_t _port; // 端口号
bool _isquit; // 标识服务器是否退出
std::unique_ptr<Socket> _listensock; // 监听socket

// 保存所有合法文件fd
int _rfdset[gfdnums];
};

在我的Ubuntu-22.04系统上sizeof(fd_set)*8的值是1024,这个不同的平台大小可能不同。 有了这个fd数组之后再Loop接口中的添加逻辑就应该把数组中的fd全部添加到事件集中。都添加为读事件。

void Loop()
{
while(!_isquit)
{
fd_set rfds; // 读事件集
FD_ZERO(&rfds); // 清零

// FD_SET(_listensock->Sockfd(), &rfds); // 添加fd
int maxfd = 0;
for(int i = 0; i < gfdnums; i++)
{
if(_rfdset[i] == 1)
continue; // fd 不合法

// 合法fd
FD_SET(_rfdset[i], &rfds); // 添加fd
if(maxfd < _rfdset[i]) maxfd = _rfdset[i]; // 更新最大fd
}
// 只关心读事件且第五个参数等待方式设为阻塞等待
int n = select(maxfd + 1, &rfds, nullptr, nullptr, nullptr);

if(n == 0) // 超时,等待方式设为超时模式时使用
{
LOG(LogLevel::INFO) << "timeout…";
}
else if(n == 1) // 出错
{
LOG(LogLevel::WARNING) << "select error!";
}
else // 就绪 n 个
{
LOG(LogLevel::INFO) << "event ready! n = " << n;
HandlerEvent(rfds); // 处理
}
}
}

如上,不仅要把所有的fd添加到读事件集中,还要找出最大的fd值。

这就是select的逻辑,这个_rfdset数组中就会存在accept获取到的fd,在fd上的读事件就绪时,可能是listenfd上的读事件就绪,也可能是普通fd的读事件就绪,即客户端给服务端发消息了。所以在HandlerEvent接口中需要处理两种情况。

HandlerEvent:

void HandlerEvent(fd_set &rfds)
{
for(int i = 0; i < gfdnums; i++) // 遍历所有fd,看是否事件就绪
{
if(_rfdset[i] == 1)
continue; // fd 不合法

//合法fd
if(FD_ISSET(_rfdset[i], &rfds))
{
// fd 就绪,区分是listenfd还是普通fd
if(FD_ISSET(_listensock->Sockfd(), &rfds)) // listenfd
{
// 监听套接字读事件就绪了
LOG(LogLevel::INFO) << "listensockfd event ready!";
InetAddr clientaddr;
int sockfd = _listensock->Accepter(clientaddr); // 获取连接
LOG(LogLevel::INFO) << "get a new link, sockfd is :" << sockfd << clientaddr.ToString();

// 添加fd到数组中
int pos = 0;
for(; pos < gfdnums; pos++)
{
if(_rfdset[pos] == 1)
break;
}

if(pos == gfdnums) // 数组满了,服务器到达极限
{
LOG(LogLevel::WARNING) << "server is full!";
close(sockfd);
}
else
{
_rfdset[pos] = sockfd;
}
}
else // 普通文件描述符, 处理读事件
{
char inbuffer[1024];
ssize_t n = recv(_rfdset[i], inbuffer, sizeof(inbuffer), 0);
if(n > 0)
{
inbuffer[n] = 0;
LOG(LogLevel::INFO) << "client #" << inbuffer;

std::string echo_str = "server echo #";
echo_str += inbuffer;
send(_rfdset[i], echo_str.c_str(), echo_str.size(), 0);
}
else if(n == 0) // 读到末尾
{
LOG(LogLevel::INFO) << "client quit, fd: " << _rfdset[i];
// 关闭fd
close(_rfdset[i]);
// 从数组中移除fd
_rfdset[i] = 1;
}
else // 其它情况
{
LOG(LogLevel::WARNING) << "client error, fd: " << _rfdset[i];
// 关闭fd
close(_rfdset[i]);
// 从数组中移除fd
_rfdset[i] = 1;
}
}
}
}
}

如上,当事件就绪时,由于fd很多所以需要遍历所有fd看是哪个事件就绪了,如果是listenfd就获取连接,并把新获取上来的fd添加到数组中,如果是普通fd,就读取客户端发来的消息,然后再发给它。 注意:这里send的时候是直接发的,没有检测写事件就绪,因为写事件一般都是就绪状态,只有特殊情况会添加到写事件集中。

另外如果是listenfd,在获取并添加fd的时候满了,就直接关闭新获取上来的fd。如果是普通fd就绪,这个fd读到末尾,或者读出错,也直接关闭fd,并把fd从_rfdset数组中移除。

运行测试: 在这里插入图片描述 如上,服务端能够同时连接多个客户端。

在这里插入图片描述 如上图,并且在客户端退出的时候,fd也是读事件就绪的状态,因为底层自动做的四次挥手,也要给服务端发报文。

这样就通过select多路转接的方式实现了一个进程多条连接。

2.4 事件式编程

上面我们的代码确实可以运行了,但是代码耦合在了一起,又继续改代码了。

select所处的函数是Loop它的主要工作是检测事件就绪,事件就绪之后,交给别人处理,这里可以直接交给一个事件派发接口让它处理。我们之前的HandlerEvent接口做的工作太多了,拆分成一个事件派发接口和几个具体的事件处理接口。

派发接口负责区分具体是什么事件就绪,交给特定的事件处理接口处理,事件处理接口,例如接受连接的、处理IO的等等。

Loop:

void Loop()
{
while(!_isquit)
{
// …

// 只关心读事件且第五个参数等待方式设为阻塞等待
int n = select(maxfd + 1, &rfds, nullptr, nullptr, nullptr);

if(n == 0) // 超时,等待方式设为超时模式时使用
{
LOG(LogLevel::INFO) << "timeout…";
}
else if(n == 1) // 出错
{
LOG(LogLevel::WARNING) << "select error!";
}
else // 就绪 n 个
{
LOG(LogLevel::INFO) << "event ready! n = " << n;
// 事件派发器
Dispatcher(rfds);
}
}
}

事件派发及事件处理:

// 连接管理器
void Accepter()
{
// 监听套接字读事件就绪了
LOG(LogLevel::INFO) << "listensockfd event ready!";
InetAddr clientaddr;
int sockfd = _listensock->Accepter(clientaddr); // 获取连接
LOG(LogLevel::INFO) << "get a new link, sockfd is :" << sockfd << clientaddr.ToString();

// 添加fd到数组中
int pos = 0;
for(; pos < gfdnums; pos++)
{
if(_rfdset[pos] == 1)
break;
}

if(pos == gfdnums) // 数组满了,服务器到达极限
{
LOG(LogLevel::WARNING) << "server is full!";
close(sockfd);
}
else
{
_rfdset[pos] = sockfd;
}
}

// IO 处理器
void Recver(int i)
{
char inbuffer[1024];
ssize_t n = recv(_rfdset[i], inbuffer, sizeof(inbuffer), 0);
if(n > 0)
{
inbuffer[n] = 0;
LOG(LogLevel::INFO) << "client #" << inbuffer;

std::string echo_str = "server echo #";
echo_str += inbuffer;
send(_rfdset[i], echo_str.c_str(), echo_str.size(), 0);
}
else if(n == 0) // 读到末尾
{
LOG(LogLevel::INFO) << "client quit, fd: " << _rfdset[i];
// 关闭fd
close(_rfdset[i]);
// 从数组中移除fd
_rfdset[i] = 1;
}
else // 其它情况
{
LOG(LogLevel::WARNING) << "client error, fd: " << _rfdset[i];
// 关闭fd
close(_rfdset[i]);
// 从数组中移除fd
_rfdset[i] = 1;
}
}

// 事件派发器
void Dispatcher(fd_set &rfds)
{
for(int i = 0; i < gfdnums; i++) // 遍历所有fd,看是否事件就绪
{
if(_rfdset[i] == 1)
continue; // fd 不合法

//合法fd
if(FD_ISSET(_rfdset[i], &rfds))
{
// fd 就绪,区分是listenfd还是普通fd
if(FD_ISSET(_listensock->Sockfd(), &rfds)) // listenfd
{
Accepter(); // 连接管理器
}
else // 普通文件描述符, 处理读事件
{
Recver(i); // IO处理器
}
}
}
}

如上,Dispatcher根据不同的事件调用不同的事件处理接口,当前这两个接口在同一个类中。将来它们可能是两个函数调用,具体的函数处理在其它的处理层。

这样修改代码后逻辑很清晰,实现了事件检测与业务逻辑的解耦。

运行测试: 在这里插入图片描述

2.5 select原理及优缺点

原理

用户调用select,传入要监听的fd集合。内核将当前进程(task_struct)分别添加到每一个被监听fd对应的struct socket层的等待队列中。 在这里插入图片描述

同时,内核会进行一轮初始轮询,检查这些fd是否已经就绪。select的底层也是通过遍历的方式检测fd是否就绪的,所以传递的第一个参数需要是maxfd+1,这样便于遍历检测。

内核调用每个fd的poll()函数(通过struct file中的函数指针表f_op),遍历检测从0到maxfd的所有fd,检查其收发队列是否有数据或空间。 在这里插入图片描述

如果没有任何一个fd就绪,内核会将当前进程从运行队列(runqueue)移除,使其进入阻塞状态,等待事件唤醒。

当被监听的任意一个fd发生关注的事件(如数据到达、缓冲区可写)时,该fd会唤醒其等待队列上的所有进程(包括我们的select相关进程)。进程被唤醒后,会重新进入运行队列。并且内核会再次遍历从0到maxfd的所有fd,调用它们的poll()函数。这次遍历的目的是收集所有当前已经就绪的fd,将它们对应的比特位在fd_set位图中置1。 最后,内核将当前进程从所有被监听fd的等待队列中移除(清理工作),并将就绪的fd_set位图返回给用户态。

select的缺点

  • 每次调用select,都需要手动设置fd集合,从接口使用角度来说非常不便;
  • 每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大;
  • 同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大;
  • select支持的文件描述符数量太小,并且输入输出参数的耦合度太高,三个fd_set*参数既是输入又是输出。

三、多路转接poll

poll 是一个与 select 功能类似的系统调用,它通过允许用户传入一个动态大小的 struct pollfd 数组来监听多个文件描述符,从而突破了 select 的 1024 限制,并通过独立的 events(输入)和 revents(输出)字段解耦了事件描述与事件返回。

因此poll解决了select的参数耦合和fd有上限的两个缺陷。poll的作用等同于select。

它的函数参数比select要简洁: 在这里插入图片描述

int poll(struct pollfd *fds, nfds_t nfds, int timeout);

在这里插入图片描述

参数类型作用
fds struct pollfd * 指向一个 动态数组 的指针,数组元素为 struct pollfd,每个元素描述一个待监听的文件描述符及其事件。
nfds nfds_t (即 unsigned long) 指定 fds 数组的 实际长度,即要监听的文件描述符个数。
timeout int 超时时间(单位:毫秒)。• -1:永久阻塞• 0:立即返回(非阻塞轮询)• >0:等待指定毫秒数后超时返回

返回值:poll 返回发生事件的文件描述符数量>0、超时0或错误-1。 和select返回值相同。timeout参数也和select相同。

struct pollfd 结构体:

字段类型作用
fd int 要监听的文件描述符。若设为负数,则内核会忽略该元素,不进行监听。
events short 输入位掩码:告诉内核要关心哪些事件(如 POLLIN、POLLOUT)。
revents short 输出位掩码:内核返回时填充,表示实际发生了哪些事件。

events和revents的取值: 在这里插入图片描述 如上图,这些事件都是宏定义,如下。 在这里插入图片描述

输入输出参数的解耦就体现在events和revents上。

poll注意:

  • 数组可复用:与 select 不同,poll 的 fds 数组是独立的,revents 不会覆盖 events,因此无需在每次调用前重新设置整个数组。
  • 事件类型:常用事件包括 POLLIN(数据可读)、POLLOUT(数据可写)、POLLERR(发生错误,仅出现在 revents 中)。
  • 性能特点:poll 虽然解决了1024限制和参数耦合,但内核仍需要线性遍历 fds 数组,因此当连接数非常大时,性能会线性下降O(n)。
  • 3.1 poll版本EchoServer服务器

    poll的使用方式和select函数的使用方式上十分相似,所以在写代码的时候就不从零开始了,直接修改并呈现最终的代码,其实就是把select函数改成poll,然后改一改数组而已。

    poll函数在使用上比select函数要简单。

    PollServer.hpp:

    const int gnum = 4096; // 由用户设置fd数量

    class PollServer
    {
    public:
    PollServer(uint16_t port = 8080)
    : _port(port)
    , _isquit(false)
    , _listensock(std::make_unique<TcpSocket>())
    {
    _listensock->BuildTcpSocketMethod(_port); // 调用模板方法,创建套接字、绑定、监听
    LOG(LogLevel::INFO) << "create listensock success, fd: " << _listensock->Sockfd();

    for(int i = 0; i < gnum; i++)
    {
    // 初始化数组
    _fds[i].fd = 1;
    _fds[i].events = _fds[i].revents = 0;
    }

    // 添加listenfd
    _fds[0].fd = _listensock->Sockfd();
    _fds[0].events = POLLIN; // 关心fd的读事件
    }

    // 连接管理器
    void Accepter()
    {
    // 监听套接字读事件就绪了
    LOG(LogLevel::INFO) << "listensockfd event ready!";
    InetAddr clientaddr;
    int sockfd = _listensock->Accepter(clientaddr); // 获取连接
    LOG(LogLevel::INFO) << "get a new link, sockfd is :" << sockfd << clientaddr.ToString();

    // 添加fd到数组中
    int pos = 0;
    for(; pos < gnum; pos++)
    {
    if(_fds[pos].fd == 1)
    break;
    }

    if(pos == gnum) // 数组满了,服务器到达极限
    {
    LOG(LogLevel::WARNING) << "server is full!";
    close(sockfd);
    }
    else // 新增fd
    {
    _fds[pos].fd = sockfd;
    _fds[pos].events = POLLIN;
    _fds[pos].revents = 0;
    }
    }

    // IO 处理器
    void Recver(int i)
    {
    char inbuffer[1024];
    ssize_t n = recv(_fds[i].fd, inbuffer, sizeof(inbuffer), 0);
    if(n > 0)
    {
    inbuffer[n] = 0;
    LOG(LogLevel::INFO) << "client #" << inbuffer;

    std::string echo_str = "server echo #";
    echo_str += inbuffer;
    send(_fds[i].fd, echo_str.c_str(), echo_str.size(), 0);
    }
    else if(n == 0) // 读到末尾
    {
    LOG(LogLevel::INFO) << "client quit, fd: " << _fds[i].fd;
    // 关闭fd
    close(_fds[i].fd);
    // 从数组中移除fd
    _fds[i].fd = 1;
    _fds[i].events = _fds[i].revents = 0;
    }
    else // 其它情况
    {
    LOG(LogLevel::WARNING) << "client error, fd: " << _fds[i].fd;
    // 关闭fd
    close(_fds[i].fd);
    // 从数组中移除fd
    _fds[i].fd = 1;
    _fds[i].events = _fds[i].revents = 0;
    }
    }

    // 事件派发器
    void Dispatcher()
    {
    for(int i = 0; i < gnum; i++) // 遍历所有fd,看是否事件就绪
    {
    if(_fds[i].fd == 1)
    continue; // fd 不合法

    //合法fd
    if(_fds[i].revents & POLLIN)
    {
    // fd 就绪,区分是listenfd还是普通fd
    if(_fds[i].fd == _listensock->Sockfd()) // listenfd
    {
    Accepter(); // 连接管理器
    }
    else // 普通文件描述符, 处理读事件
    {
    Recver(i); // IO处理器
    }
    }
    }
    }

    void Loop()
    {
    int timeout = 1; // poll 阻塞等待
    while(!_isquit)
    {
    int n = poll(_fds, sizeof(_fds)/sizeof(_fds[0]), timeout);

    if(n == 0) // 超时,等待方式设为超时模式时使用
    {
    LOG(LogLevel::INFO) << "timeout…";
    }
    else if(n == 1) // 出错
    {
    LOG(LogLevel::WARNING) << "poll error!";
    }
    else // 就绪 n 个
    {
    LOG(LogLevel::INFO) << "event ready! n = " << n;
    // 事件派发器
    Dispatcher();
    }
    }
    }

    ~PollServer()
    {}
    private:
    uint16_t _port; // 端口号
    bool _isquit; // 标识服务器是否退出
    std::unique_ptr<Socket> _listensock; // 监听socket

    struct pollfd _fds[gnum];
    };

    如上,代码的层状结构还是那样,只是把相关的select函数部分和存放fd的数组部分修改了。poll的使用和select的使用非常相似。

    运行测试: 在这里插入图片描述 如上,这就是poll的使用。

    poll的缺点:调用开销 O(n):每次调用要把整个 pollfd 数组从用户态拷贝到内核态,连接多时开销大。遍历 O(n):和select函数一样,返回就绪事件后,用户态要遍历所有 fd 逐个检查 revents 才能知道谁就绪。

    现在n是4096还好一些,如果n是几十万上百万开销就很大了。

    总结: 以上就是本期博客分享的全部内容啦!如果觉得文章还不错的话可以三连支持一下,你的支持就是我前进最大的动力! 技术的探索永无止境! 道阻且长,行则将至!后续我会给大家带来更多优质博客内容,欢迎关注我的CSDN账号,我们一同成长! (~ ̄▽ ̄)~

    赞(0)
    未经允许不得转载:171主机测评 » 【Linux】多路转接select/poll
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址