poll函数初识
poll是系统提供的一个多路转接接口
它的作用和select函数基本一致
在学习poll函数之前我们先回顾下select函数的缺点
- 为了维护第三方数组 select服务器充满大量的遍历
- 每一次都要对select参数进行重新设定
- 能够同时管理的fd有上限
- 每个参数都是输入输出形的 所以要经历大量的内核用户拷贝
- 编码比较复杂
对于我们的poll来说
- 它的输出输出参数是分离的 所以解决了缺点2
- poll能同时管理的参数个数是有上限了 所以解决了缺点3
poll函数
poll函数的函数原型如下:
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
下面我们依次介绍下这个函数的参数和返回值
返回值:int n
- 如果返回大于0的数字就表示 有n个文件描述符就绪
- 如果timeout时间到了还没有文件描述符就绪则返回0
- 如果出错则返回-1
参数:
参数一: struct pollfd *fds
一个结构体 里面包含着文件描述符表 我们需要监视的文件描述符合集 就绪的文件描述符合集
我们在下面会详细介绍该参数
参数二: nfds
fds数组的长度
参数三: timeout
超时时间
- 单位是毫秒 比如说我们设置为1000 就是等待1秒
- 如果设置为0 就表示非阻塞模式
- 如果设置为-1 就表示阻塞模式
struct pollfd详细介绍
既然poll和select的功能相同 那么它就应该回答和select相同的两个问题
- 用户如何告诉内核 我需要关心的文件描述符有哪些
- 内核如何告诉用户 你关心的文件描述符有哪些就绪了
而实际上这两个问题都在结构体 struct pollfd
中解决了
struct pollfd在linux中的定义如下图
- fd 特定的文件描述符值
- events 用户告诉内核 哪些事件需要关心
- revents 内核告诉用户 哪些事件就绪了
但是我们这里就有一个问题了 events是一个short类型的数据啊
我们怎么让一个short类型的数据 来表示很多时间是否就绪呢?
不知道大家还记不记得在基础IO章节我们曾经说过一句话 在Linux操作系统中 为了节省空间 要表示一个事件存不存在 我们只需要用一个比特位表示就够了
所以说 我们只要规定 short 类型数据的每个比特位的含义 就能使用该比特位的1或0来表示该事件是否存在了
以下是events和revents的取值
我们需要特别注意的有三个 分别是
- POLLIN 可读
- POLLOUT 可写
- POLLERR 错误
为什么说poll能够接收的fd是无上限的呢?
我们在学习完毕struct pollfd之后明白了 一个文件描述符实际上就是对应一个struct pollfd
所以说理论上 只要有多少个数组 我们的poll就能检测多少的文件描述符
poll服务器
我们选择将原来的select服务器改写成一个poll服务器 让大家更直观的感受到poll对比于select的优点
它的私有成员变化如下
int _port; int _listensock; struct pollfd *_rfds; func_t _func;
对比于我们select的第三方数组来说 我们这里多了一个数组指针和数组大小
在初始化的时候 我们首先new出一个 struct pollfd
数组出来 (大小自己指定)
并且遍历初始化一下
_rfds[i].fd = defaultfd; _rfds[i].events = 0; _rfds[i].revents = 0;
对于数据如何判断就绪 我们可以使用按位与来判断
_rfds[i].revents & POLLIN
托管文件描述符给poll也很简单 和select一样 找到没有被使用过的数组然后依次设置struct即可
整体代码如下
#pragma once #include <iostream> #include <string> #include <functional> #include "sock.hpp" namespace select_ns { static const int defaultport = 8081; static const int fdnum = sizeof(fd_set) * 8; static const int defaultfd = -1; using func_t = std::function<std::string (const std::string&)>; class SelectServer { public: SelectServer(func_t f, int port = defaultport) : func(f), _port(port), _listensock(-1), fdarray(nullptr) { } void initServer() { _listensock = Sock::Socket(); Sock::Bind(_listensock, _port); Sock::Listen(_listensock); fdarray = new int[fdnum]; for (int i = 0; i < fdnum; i++) fdarray[i] = defaultfd; fdarray[0] = _listensock; // 不变了 } void Print() { std::cout << "fd list: "; for (int i = 0; i < fdnum; i++) { if (fdarray[i] != defaultfd) std::cout << fdarray[i] << " "; } std::cout << std::endl; } void Accepter(int listensock) { logMessage(DEBUG, "Accepter in"); // 走到这里,accept 函数,会不会阻塞???1 0 // select 告诉我, listensock读事件就绪了 std::string clientip; uint16_t clientport = 0; int sock = Sock::Accept(listensock, &clientip, &clientport); // accept = 等 + 获取 if (sock < 0) return; logMessage(NORMAL, "accept success [%s:%d]", clientip.c_str(), clientport); // sock我们能直接recv/read 吗?不能,整个代码,只有select有资格检测事件是否就绪 // 将新的sock 托管给select! // 将新的sock托管给select的本质,其实就是将sock,添加到fdarray数组中即可! int i = 0; for (; i < fdnum; i++) { if (fdarray[i] != defaultfd) continue; else break; } if (i == fdnum) { logMessage(WARNING, "server if full, please wait"); close(sock); } else { fdarray[i] = sock; } Print(); logMessage(DEBUG, "Accepter out"); } void Recver(int sock, int pos) { logMessage(DEBUG, "in Recver"); // 1. 读取request // 这样读取是有问题的! char buffer[1024]; ssize_t s = recv(sock, buffer, sizeof(buffer) - 1, 0); // 这里在进行读取的时候,会不会被阻塞?1, 0 if (s > 0) { buffer[s] = 0; logMessage(NORMAL, "client# %s", buffer); } else if (s == 0) { close(sock); fdarray[pos] = defaultfd; logMessage(NORMAL, "client quit"); return; } else { close(sock); fdarray[pos] = defaultfd; logMessage(ERROR, "client quit: %s", strerror(errno)); return; } // 2. 处理request std::string response = func(buffer); // 3. 返回response // write bug write(sock, response.c_str(), response.size()); logMessage(DEBUG, "out Recver"); } // 1. handler event rfds 中,不仅仅是有一个fd是就绪的,可能存在多个 // 2. 我们的select目前只处理了read事件 void HandlerReadEvent(fd_set &rfds) { for (int i = 0; i < fdnum; i++) { // 过滤掉非法的fd if (fdarray[i] == defaultfd) continue; // 正常的fd // 正常的fd不一定就绪了 // 目前一定是listensock,只有这一个 if (FD_ISSET(fdarray[i], &rfds) && fdarray[i] == _listensock) Accepter(_listensock); else if(FD_ISSET(fdarray[i], &rfds)) Recver(fdarray[i], i); else{} } } void start() { for (;;) { fd_set rfds; // fd_set wfds; FD_ZERO(&rfds); int maxfd = fdarray[0]; for (int i = 0; i < fdnum; i++) { if (fdarray[i] == defaultfd) continue; FD_SET(fdarray[i], &rfds); // 合法 fd 全部添加到读文件描述符集中 if (maxfd < fdarray[i]) maxfd = fdarray[i]; // 更新所有fd中最大的fd } logMessage(NORMAL, "max fd is: %d", maxfd); // struct timeval timeout = {1, 0}; // int n = select(_listensock + 1, &rfds, nullptr, nullptr, &timeout); // ?? // 一般而言,要是用select,需要程序员自己维护一个保存所有合法fd的数组! int n = select(maxfd + 1, &rfds, nullptr, nullptr, nullptr); // ?? switch (n) { case 0: logMessage(NORMAL, "timeout..."); break; case -1: logMessage(WARNING, "select error, code: %d, err string: %s", errno, strerror(errno)); break; default: // 说明有事件就绪了,目前只有一个监听事件就绪了 logMessage(NORMAL, "have event ready!"); HandlerReadEvent(rfds); // HandlerWriteEvent(wfds); break; } // std::string clientip; // uint16_t clientport = 0; // int sock = Sock::Accept(_listensock, &clientip, &clientport); // accept = 等 + 获取 // if(sock<0) continue; // // 开始进行服务器的处理逻辑 } } ~SelectServer() { if (_listensock < 0) close(_listensock); if (fdarray) delete[] fdarray; } private: int _port; int _listensock; int *fdarray; func_t func; }; }
poll的优缺点
优点
- 效率高
- 适合有大量连接 少量活跃
- 输入输出分离 不需要大量的充值
- poll参数级别 没有可管理的fd上限
缺点
- poll依旧需要不少的遍历
- poll需要内核到用户的拷贝 – 这个少不了的
- poll的代码虽然比select容易 但是也很复杂