目录
- 一、多路转接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注意:
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账号,我们一同成长! (~ ̄▽ ̄)~





