如果说select是第一代多路复用技术那么poll就是第二代。select详解【Linux网络】select详解1.poll的函数函数原型包含头文件#include sys/poll.hint poll(struct pollfd *fds, nfds_t nfds, int timeout);参数详解1.struct pollfd *fdsselect使用位图而poll使用一个由struct pollfd组成的数组。struct pollfd { int fd; // 文件描述符你要监控谁 short events; // 期待的事件你想监控啥读写 short revents; // 实际发生的事件内核返回给你的结果 };没有 1024 的限制了select的位图大小是写死在内核里的通常 1024。而poll传的是数组指针和数组长度。只要你的内存够大你想监控 2000 个、5000 个连接都没问题。输入输出分离了 还记得select会破坏你的清单吗poll把“期待的事件”(events) 和“结果”(revents) 分开了。调用前你设置events POLLIN我想读。调用后内核只修改revents。你的events依然是POLLIN。好处你不需要像select那样每一轮循环都重新FD_ZERO和FD_SET了忽略 fd如果想临时不监控某个成员只需将其fd设置为负数如-1内核会自动跳过它。有哪些事件这些事件其实就是只有一个比特位为1的宏值POLLIN和POLLOUT是最常用的就记住这两个就够了。事件宏(Event)类别详细触发场景POLLIN读有新数据可读、有新连接进入listen、或对端关闭连接。POLLOUT写发送缓冲区不满可以立刻写入数据而不会阻塞。POLLRDHUP读TCP 连接的对端关闭了连接或者关闭了写半部需要定义_GNU_SOURCE。POLLERR异常指定的文件描述符发生错误由内核自动返回无需设置。POLLHUP异常管道或 Socket 被挂断。如果是 Socket通常表示连接已断开。POLLNVAL异常文件描述符无效比如没打开或者已经 close 了。调用poll时pollfd中的fd和events有效忽略revents表示用户告诉内核帮我关心这个fd上的events事件。poll返回时pollfd中的fd和revents有效忽略events表示内核告诉用户你让我关心的这些事件中有哪些就绪了。2.nfds_t nfds这个参数告诉内核第一个参数数组中有多少个结构体元素。含义数组的长度。突破限制与select固定的 1024 限制不同poll的数量取决于这个参数的大小。只要内存允许你可以监控成千上万个文件描述符。类型通常是一个无符号长整型。3.int timeout控制poll在没有事件发生时的等待行为。单位毫秒 (ms)。相比select的timeval结构体poll的整数表示更加简洁。取值含义取值模式行为描述-1永久阻塞只要没事件发生就死等直到被信号中断或有 fd 就绪。0立即返回检查完当前状态立刻返回不进行任何等待非阻塞/轮询。 0限时等待例如输入1000表示等待 1 秒。时间内有动静则提前返回到时则返回 0。2. 实际运用这个地方的代码根据select的运用代码进行改编。2.1 初始化class PollServer { // const static int size sizeof(fd_set) * 8; const static int size 4096; const static int defaultfd -1; public: PollServer(uint16_t port) : _listensock(std::make_uniqueTcpSocket()), _isrunning(false) { _listensock-BuildListenSocketMethod(port); for (int i 0; i size; i) // 初始化结构体 { _fd[i].fd defaultfd; _fd[i].events 0; _fd[i].revents 0; } _fd[0].fd _listensock-Fd(); // listensockfd还是放在0下标处 _fd[0].events | POLLIN; // 事件设为读事件 } private: std::unique_ptrSocket _listensock; bool _isrunning; // int _fd_array[size]; // select的辅助数组 struct pollfd _fd[size]; // poll的第一个参数结构体 };这里我们就把结构体定为定长的方便一些其实这里是可以用动态开辟的。2.2 Startselect的start还要对辅助数组进行处理这里就不需要了直接赋值。void Start() { if (!_isrunning) _isrunning true; while (true) { //int timeout 1000; // 1秒 //int timeout 0; // 轮询 int timeout -1; // 阻塞 int n poll(_fd, size, timeout); switch (n) { case -1: LOG(LogLevel::ERROR) poll error; break; case 0: LOG(LogLevel::WARNING) poll time out ...; break; default: LOG(LogLevel::DEBUG) 事件就绪..., n n; DispatchEvent(); // 处理事件 break; } } _isrunning false; }2.3 处理事件这里的逻辑和select是一样的主要是看一下用法。void DispatchEvent() { for (int i 0; i size; i) { if (_fd[i].fd defaultfd) // 判断fd是否合法 continue; // fd合法但是不一定就绪 // 这里 revents 事件用来判断事件是否就绪 if (_fd[i].revents POLLIN) // 处理读事件 { if (_fd[i].fd _listensock-Fd()) { // listensocket新连接到来 Accepter(); } else { // socketfd普通读事件就绪 Recver(i); } } else if (_fd[i].revents POLLOUT) // 处理写事件 { // 此处不做过多处理感兴趣的自行编写 } // ... } }2.4 连接管理Accepter在这里其实就是一个连接管理器这个部分的代码逻辑不变主要展示用法。void Accepter() { InetAddr client; // 此时accept获取链接的时候就不是阻塞的了 int sockfd _listensock-Accept(client); if (sockfd 0) { LOG(LogLevel::DEBUG) 获得一个链接sockfd sockfd; int pos 0; // 记录一下新的sockfd存放的下标 while (pos size) { // 找到辅助数组的空位置存放新的sockfd if (_fd[pos].fd defaultfd) break; } if (pos size) { LOG(LogLevel::WARNING) poll full...; // 这里poll满了之后如果之前的数组是变长的可以进行扩容 // 我们这里就直接关闭fd close(sockfd); } else { _fd[pos].fd sockfd; // 将新的sockfd加入到辅助数组中 _fd[pos].events POLLIN; _fd[pos].revents 0; // 可不写 } } }2.5 IO处理Recver函数其实也就是IO处理器这里的逻辑还是和select一样主要是演示poll用法。void Recver(int pos) { // 此时用recv/read读取数据的时候就不会被阻塞了 char buffer[1024] {0}; int n recv(_fd[pos].fd, buffer, sizeof(buffer) - 1, 0); if (n 0) { // 将数据都当字符串处理 buffer[n] 0; LOG(LogLevel::INFO) client say buffer; // 回显一下 } else if (n 0) { // 此时就是客户端关闭了 LOG(LogLevel::INFO) client quit...; close(_fd[pos].fd); // 关闭文件描述符 _fd[pos].fd defaultfd; // 清理结构体, 让poll不要再关心此fd _fd[pos].events 0; _fd[pos].revents 0; } else { // 读出错 LOG(LogLevel::ERROR) recv error...; close(_fd[pos].fd); // 关闭文件描述符 _fd[pos].fd defaultfd; // 清理结构体, 让poll不要再关心此fd _fd[pos].events 0; _fd[pos].revents 0; } }2.6 Main.cc#include PollServer.hpp int main(int argc, char *argv[]) { if (argc ! 2) { std::cout Usage: argv[0] port std::endl; exit(ExitCode::USAGE_ERR); } uint16_t port std::stoi(argv[1]); LogToConsole(); std::unique_ptrPollServer svr std::make_uniquePollServer(port); svr-Start(); return 0; }运行结果3. poll的优缺点3.1 poll的优点没有最大文件描述符限制原理select受到FD_SETSIZE通常为 1024的限制而poll使用的是链表/动态数组结构。价值只要系统内存足够你可以监控 1 万甚至更多的连接不会像select那样直接崩溃或报错。输入输出分离减少重复操作原理pollfd结构体将期待事件events和实际发生事件revents分开。价值内核只修改revents而不会破坏你的events。这意味着在while循环中你不需要每一轮都重新初始化监控列表除非你要增删 fd代码比select简洁得多。3.2 poll的缺点线性扫描效率低下 (O(n))问题即使只有 1 个连接活跃poll返回后你依然需要用for循环遍历整个数组通过fds[i].revents去挨个检查。后果当连接数n很大时这种“挨个问”的操作会极大地浪费 CPU。用户态与内核态的数据拷贝开销问题每次调用poll都需要将整个pollfd数组从用户空间全量拷贝到内核空间。后果在高频调用、海量连接的情况下内存拷贝的带宽和 CPU 开销会成为性能瓶颈。水平触发 (Level Triggered) 的局限指由于它是水平触发 (LT)如果你不处理完数据它会一直提醒导致大量的上下文切换和冗余通知。