目录
poll
引入
我们前面介绍了select -- 多路转接之select(fd_set介绍,参数详细介绍,优缺点),实现非阻塞式网络通信(代码+思路)-CSDN博客
- 随着文件数量增多(有新客户端来连接),文件上事件就绪的概率也就增大了
- 而等待就绪->通知用户层有事件就绪的过程涉及到多次遍历和拷贝,就绪次数多了,遍历和拷贝就多了
- 所以,会慢慢让效率变低
为了解决这些问题,出现了新的多路转接的方案 -- poll
介绍
是一种用于多路复用 I/O 事件的系统调用,它允许程序监视多个文件描述符,并等待其中的某些事件发生
- 它和select一样,只负责等待
- 它主要解决了select的两个弊端 -- fd有上限 和 参数重置问题
函数原型
fds
和select中三个位图的作用相同 -- 用于用户和内核之间的信息交流,但原理不同
struct pollfd
- 用户给内核传入要关注文件的fd和要关注的事件类型 -- 使用了pollfd中的fd,events字段
- 内核给用户返回该文件上的哪个事件已就绪 -- 使用fd,revents字段
特点
将输入和输出事件分离
- 而不是像select一样,用户和内核使用的是同一个结构(位图)
- 这里使用了两个变量给两方分别使用
nfds
fds中的元素个数
- 相当于需要关注的fd个数
timeout
等待事件的超时时间
- 以毫秒为单位,1000ms=1s
取值
- >0 -- 超时时间
- =0 -- 非阻塞,函数会立即返回
- -1
返回值
和select作用相同
- >0 -- 就绪的fd个数
- =0 -- 超时,没有事件就绪
- <0 -- 等待的文件中有已经关闭的文件
原理
如何实现关注多个fd?
这里的fds参数是一个结构体类型的指针
- 所以可以传入结构体数组 / 指向堆上空间的指针,后续可以动态扩容
所以,我们可以添加多个pollfd结构到数组中
如何确定哪个fd上有事件就绪?
遍历数组,检查每个结构的revents字段状态
如何区分事件类型?
将events/revents字段看作16个bit位,1个bit位可以对应一个事件类型
- 和标志位一样
判断某事件是否就绪的方法
- 以读事件为例:
- (某个指定pollfd结构中的revents & POLLIN) 是否等于1,等于1说明该事件已经就绪
代码
我们可以直接改select代码为poll版本,很简单:
- 不需要在循环内重复设置参数
- 把那些删掉后,修改调用select为poll即可,最多就创建一个pollfd结构体
#include "Log.hpp"
#include "socket.hpp"
#include <poll.h>
static const int def_port = 8080;
static const int def_max_num = 1024;
static const int def_data = -1;
static const int no_data = 0;
class poll_server
{
public:
poll_server()
{
for (int i = 0; i < def_max_num; ++i)
{
fds_[i].fd = def_data;
fds_[i].events = no_data;
fds_[i].revents = no_data;
}
}
~poll_server()
{
listen_socket_.Close();
}
void start()
{
listen_socket_.Socket();
listen_socket_.Bind(def_port);
listen_socket_.Listen();
int timeout = 1;
// 固定数组第一项是监听套接字
struct pollfd tl = {listen_socket_.get_fd(), POLLIN, no_data};
fds_[0] = tl;
while (true)
{
int ret = poll(fds_, def_max_num, timeout);
if (ret > 0) // 有事件就绪
{
handle();
}
else if (ret == 0) // 超时
{
continue;
}
else
{
perror("poll");
break;
}
}
}
private:
void receiver(int fd, int i)
{
char in_buff[1024];
int n = read(fd, in_buff, sizeof(in_buff) - 1);
if (n > 0)
{
in_buff[n - 1] = 0;
std::cout << "get message: " << in_buff << std::endl;
}
else if (n == 0) // 客户端关闭连接
{
close(fd);
lg(DEBUG, "%d quit", fd);
fds_[i].fd = -1; // 重置该位置
fds_[i].events = no_data;
fds_[i].revents = no_data;
}
else
{
lg(ERROR, "fd: %d ,read error");
}
}
void accepter()
{
std::string clientip;
uint16_t clientport;
int sock = listen_socket_.Accept(clientip, clientport);
if (sock == -1)
{
return;
}
else // 把新fd加入数组
{
struct pollfd t;
int pos = 1000; //1s
for (; pos < def_max_num; ++pos)
{
if (fds_[pos].fd == def_data) // 找到空位,但不能直接添加
{
break;
}
}
if (pos != def_max_num)
{
t.fd = sock;
}
else // 满了
{
//这里可以扩容
lg(WARNING, "server is full,close %d now", sock);
close(sock);
}
t.events = POLLIN;
t.revents = no_data;
fds_[pos] = t;
}
}
void handle()
{
for (int i = 0; i < def_max_num; ++i) // 遍历数组
{
int fd = fds_[i].fd;
if (fd != def_data) // 有效fd
{
if (fds_[i].revents & POLLIN) // 有事件就绪
{
if (fd == listen_socket_.get_fd()) // 获取新连接
{
accepter();
}
else // 读事件
{
receiver(fd, i);
}
}
}
}
}
private:
MY_SOCKET listen_socket_;
struct pollfd fds_[def_max_num];
};
示例
总结
为什么说它解决了fd上限问题?
因为poll里的数组大小由用户决定,而fd_set的大小已经被系统定死了,无法改变
缺点
但是,poll依然没有解决多次遍历的问题
- 用户层需要查看数组中每个结构的revents,看哪些文件的哪些事件就绪了
- 内核也需要遍历数组,查看需要关注哪些文件的哪些事件,查看是否有文件就绪
遍历成本由文件个数决定
- 虽然poll没有限制,但一旦数量过多,会影响遍历效率
所以,为了解决这个问题,提出了新的方案 -- epoll
它是目前效率最高的多路转接方案