目录
poll
引入
介绍
函数原型
fds
struct pollfd
特点
nfds
timeout
取值
返回值
原理
如何实现关注多个fd?
如何确定哪个fd上有事件就绪?
如何区分事件类型?
判断某事件是否就绪的方法
代码
示例
总结
为什么说它解决了fd上限问题?
缺点
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; //1sfor (; 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
它是目前效率最高的多路转接方案