
目录1. poll 的定位2. poll 函数poll 事件的认识3. 修改 select - poll4. Poll 的缺点poll 是一种改良的select但是改良的不彻底只解决了select的两个缺点每次调用select, 都需要⼿动设置fd集合, 从接⼝使⽤⻆度来说也非常不便。(最重要的缺点1)select支持的文件描述符数量太小。(最重要的缺点2)1. poll 的定位poll 是一个多路复用多路转接的一种具体方案poll 核心定位IO 等 拷贝一次等待多个fd - 任意一个或者多个fd就绪通知用户 - 通过等待多个fd(手段)通知用户哪些fd就绪了目的select / poll / epoll 的核心定位就是就绪事件通知机制poll 只解决等的问题2. poll 函数poll 事件的认识返回值 0有多少个 fd 就绪 0超时没有就绪也没有报错 0select 自己调用出错int timeout 参数同select一样但是只是输入型参数是以毫秒计时的timeout 1000表示1000以内阻塞等待没有就绪直到超时超时之后自动进行timeouttimeout -1代表的是阻塞等待除非有文件描述符就绪否则不返回timeout 0代表的是非阻塞式等待struct pollfd 结构体是输入输出型参数但是是一个结构体可以将struct pollfd *fds, nfds_t nfds这两个参数当成一个整体来看当成一个数组来看可以是固定的也可以是realloc动态调整的。调用poll需要传递 struct pollfd[ ] 数组1. 输入数组的时候用户告诉内核你要帮我关心哪些fd上的哪些事件哪些 fd - struct pollfd 中的 int fd哪些事件 - struct pollfd 中的 short events1个文件描述符对应1个结构体变量值一填事件一填10个文件描述符都要关心struct pollfd[10]每个fd每个事件往里面一填poll就自动关心了2. poll成功返回的时候内核告诉用户你让我关心的哪些fd上面的哪些事件已经发生了哪些fd - struct pollfd 中的 int fd哪些事件 - struct pollfd 中的 short revents结论不管输入输出都关心 fd fd 是只读的变量不用重复改了。就绪事件通过结构体定义两个变量解决了输入输出参数需要被重置的问题因为将输入输出参数分离了所以解决了select的每次调用select, 都需要⼿动设置fd集合的问题events 和 revents 都是 short 类型的事件是分为读、写、异常三大类事件的如何来表示呢所以 poll 是有维护的一张表格的events 和 revents 都是 short 类型的所以关心的事件最多16个字节events 和 revents 也是位图POLLIN读、POLLOUT写等都是宏值只有一个比特位为1的标记位关心读写 POLLIN | POLLOUT。nfds_t nfds 参数也是有上限的但是和 select 的上限是不一样的poll 中的 nfds_t nfds 是由用户自己设置的是由用户传参导致的select 是将对应的文件描述符的上限作为数据类型植入到 select 数据成员了想调整 select 的文件描述符的总数只能重新编译内核select 有文件描述符总数的上限解决不了poll 因为是接口类型的一部分而 poll 对应的数组是由用户动态自由的去传入指定元素和指定长度。操作系统一个进程能打开的文件描述符的个数本身就是是有上限的文件描述符本质就是数组下标获得文件描述符的时候就是打开一个文件文件就是数组下标所以一个进程能够打开的文件也一定是有上限的Linux一次能够打开32/64个文件描述符作为云服务器来用的话支持对文件描述符个数做动态扩容文件描述符表支持动态扩展。在云服务器中单台机器上能够过6万多个连接3. 修改 select - poll// PollServer.hpp #pragma once #include iostream #include string #include memory #include poll.h #include Socket.hpp #include Logger.hpp #include InetAddr.hpp const static int gsize sizeof(fd_set) * 8; const static int gdefaultfd -1; // TCP server class PollServer { public: PollServer(uint16_t port) : _listensock(std::make_uniqueTcpSocket()) { _listensock-BuildListenSocketMethod(port); for (int i 0; i gsize; i) { fd_array[i].fd gdefaultfd; fd_array[i].events fd_array[i].revents 0; // 默认不关心 } fd_array[0].fd _listensock-SockFd(); fd_array[0].events POLLIN; // 关心读事件 } ~PollServer() { } // 1. 新连接到来 连接断开也属于新事件到来 // 2. 读事件就绪给server发送数据让指定的fd就绪也是新事件到来 void Accepter() { // 获取新连接了 InetAddr clientaddr; // “拷贝” // accept不会被阻塞因为select已经告诉我了已经就绪了 int sockfd _listensock-Accept(clientaddr); if (sockfd 0) { LOG(LogLevel::INFO) get new sockfd: sockfd , clientaddr: clientaddr.ToString(); // 可以直接调用recv(sockfd...)吗不能 // 那应该怎么办sockfd 托管给 select // 如何将新的 fd 托管给 select // 读参数是输入输出型参数这样会导致该变量里面的内容被内核修改设置所以每次调用都需要进行重置 // 所以历史的合法的fd就必须得记录下来 // 所以 select 文件描述符数组 用这个数组重置参数 // 如何防止只要把新的sockfd放入辅助数组中即可 int pos 0; for (; pos gsize; pos) { if (fd_array[pos].fd gdefaultfd) { fd_array[pos].fd sockfd; fd_array[pos].events POLLIN; // POLLIN | POLLOUT //关心读写事件 break; } } if (pos gsize) { LOG(LogLevel::WARNING) server is full!; close(sockfd); // 扩容 } } } void Recver(int index) { int sockfd fd_array[index].fd; char buffer[1024]; // 这里读取会不会阻塞不会因为select等过事件就绪了 // 这里的读取有问题buffer中数据是面向字节流的不一定会读取完整的报文要将协议带进来才可以解决 ssize_t n recv(sockfd, buffer, sizeof(buffer), 0); if (n 0) { // 这里先当成字符串 buffer[n] 0; std::cout client say buffer std::endl; std::string echo_string server echo ; echo_string buffer; send(sockfd, echo_string.c_str(), echo_string.size(), 0); // } else if (n 0) { LOG(LogLevel::INFO) client qiut , me too: fd_array[index].fd; fd_array[index].fd gdefaultfd; // 文件描述符在数组中移除 fd_array[index].events fd_array[index].revents // 对端将连接关闭了自己也要关闭 close(sockfd); } else { // 读取失败 LOG(LogLevel::WARNING) recv error: fd_array[index].fd; fd_array[index].fd gdefaultfd; // 文件描述符在数组中移除 fd_array[index].events fd_array[index].revents 0; // 对端将连接关闭了自己也要关闭 close(sockfd); } } void EventDispatcher() { LOG(LogLevel::INFO) fd就绪,有新事件到来; for (int i 0; i gsize; i) { if (fd_array[i].fd gdefaultfd) { continue; } // 到这就是合法的但是合法的不一定是就绪的可读也不一定是listen套接字 // 读事件就绪 if (fd_array[i].revents POLLIN) { // 是listen套接字就绪还是普通事件就绪 if (fd_array[i].fd _listensock-SockFd()) { // listen套接字就绪意味着要获取新连接 Accepter(); // 连接管理器 } else { // 普通的套接字 Recver(i); // IO处理器 } } // else if(FD_ISSET(fd_array[i],wfds)) // { // } } } void Run() { while (true) { int timeout 1000; int n poll(fd_array, gsize, timeout); switch (n) { case 0: LOG(LogLevel::DEBUG) timeout……; break; case -1: LOG(LogLevel::ERROR) poll error; break; default: // 一定有事件就绪了就绪事件派发到不同的处理模块 -- 事件派发器 EventDispatcher(); break; } } } private: std::unique_ptrSocket _listensock; struct pollfd fd_array[gsize]; // struct pollfd *fd_array; -- malloc --- realloc };4. Poll 的缺点poll 底层也是要通过遍历检测所有的fd有哪些就绪然后修改参数中的 revents。fd 增多poll 遍历一遍周期就会变长poll 主要是解决输入输出参数重置的问题 和 文件描述符上限的问题对于效率方面的问题没有做太多的考量。因为 poll 还有缺点所以提出 epoll