ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

多路转接 [ poll ]

多路转接 [ poll ] Linux 当中多路转接的常见方案有三种第一种是我们上一篇的select第二种就是本篇要讲的 ---pollpoll 的作用和定位IO 等 拷贝一样的poll只负责等一次可以等待等多个fd事件就绪就可以对上层进行事件通知是一种以等待多种文件描述符的事件的手段来达到对上层指定文件描述符事件是否就绪的通知机制---- 这一点的作用等同于selectpoll 的接口和 poll 解决了 select 的什么问题poll是一种用于多路复用 I/O 的系统调用它与select类似但有一些关键的区别和特点。以下是关于poll的详细介绍poll用于同时监视多个文件描述符以确定它们是否准备好进行 I/O 操作。它通过一个数组来管理文件描述符的状态而不是像select那样使用位掩码。poll 接口#include poll.h int poll(struct pollfd *fds, nfds_t nfds, int timeout);fds一个指向struct pollfd类型数组的指针每个数组元素包含一个文件描述符及其状态。nfds数组fds中的元素数量。timeout指定poll的超时时间单位是毫秒对于 select是简化的了而且单纯是一个输入性参数不是输入输出型参数如果设置为-1则poll会阻塞直到有文件描述符准备好如果设置为0则poll会立即返回即非阻塞poll。返回值0表示有文件描述符准备好。0超时没有文件描述符准备好。-1发生错误可能的原因包括无效的文件描述符或超时参数错误等。poll的第一个参数struct pollfd结构poll的第一个参数是一个指针其实我们可以将其当作数组的起始地址第二个参数就是fds指向的数组的元素的个数struct pollfd { int fd; // 文件描述符 short events; // 请求的事件掩码 short revents; // 实际发生的事件掩码 };fd需要监视的文件描述符。events指定要监视的事件类型例如POLLIN可读、POLLOUT可写等。reventspoll返回时该字段会包含实际发生的事件。从宏观上来说poll一次可以等待多个文件描述符。我们可以将对应的数组定义为 100 个元素数组中的每个元素都是一个结构体结构体中会包含需要等待处理的文件描述符fd。调用poll时结构体中的fd和events字段有效表示用户告知内核需要关注该fd上的指定events事件。poll成功返回时结构体中的fd和revents字段有效表示内核告知用户该fd上已发生对应就绪事件。所以对于poll来说他是通过数组来传结构体传结构体的时候关心哪一个文件描述符的哪一个事件返回哪一个文件描述符的哪一个事件就绪了是通过struct pollfd结构体返回的可是select包含读事件、写事件以及异常事件那么在poll这里所谓的事件又是什么含义呢事件类型事件描述是否可作为输入是否可作为输出POLLIN数据包括普通数据和优先数据可读是是POLLRDNORM普通数据可读是是POLLRDBAND优先级带数据可读Linux 不支持是是POLLPRI高优先级数据可读比如 TCP 带外数据是是POLLOUT数据包括普通数据和优先数据可写是是POLLWRNORM普通数据可写是是POLLWRBAND优先级带数据可写是是POLLRDHUPTCP 连接被对方关闭或者对方关闭了写操作。它由 GNU 引入是是POLLERR错误否是POLLHUP挂起。比如管道的写端被关闭后读端描述符上将收到 POLLHUP 事件否是POLLNVAL文件描述符没有打开否是这个表格总结了poll系统调用中events和revents参数可能的取值及其描述以及它们是否可以作为输入或输出事件。这些值本质都是宏所谓的events和revents参数就是事件位图一个一个的事件所对应的位图结构每一个事件都是其中一个比特位为 1便于我们后面通过操作符来进行检测事件是否就绪poll 相应的事件很多我们重点要知道两个POLLIN读事件POLLOUT写事件举个例子若调用poll时需要关注 3 号文件描述符的读事件只需设置fd 3并将events配置为POLLIN。当 3 号文件描述符读事件就绪后无需查看events直接检查revents即可 —— 内核会自动将revents设置为POLLIN。后续只需判断revents的对应比特位是否包含POLLIN标志位若包含则表示读事件已就绪。使用revents POLLIN为真就代表读事件就绪了【我们要求的和返回给我们的结果都匹配得上所以就绪】使用events | POLLIN;来让 poll 关心指定文件描述符的读事件【加上一个对指定文件描述符的事件关心】问题1poll 输入输出参数分离了所以不用再 poll 之前进行参数重置了在select调用中fd_set结构用于同时存储感兴趣的输入和输出文件描述符集合由于select系统调用完成后会清空这些集合因此在每次调用select之前必须重新初始化这些集合这是受限于输入输出型参数的【select 返回后fd_set 里剩下的就只有「事件已经就绪」的文件描述符】而poll通过struct pollfd结构来管理文件描述符每个pollfd元素分别对应一个文件描述符并包含events和revents两个字段。events字段用于指定对哪些事件感兴趣而revents字段则在调用返回时由内核填充指示实际发生的事件。由于poll的这种设计每次调用时events字段都可以直接设置为需要监视的事件而不需要像select那样在每次调用前重置整个集合因此poll在处理输入和输出参数时更为灵活和高效。【poll和select的核心差别之一】【不过也是一次性的生命周期并没有持久注册】问题2poll 等待的 fd 个数没有上限poll系统调用不限制等待的文件描述符fd数量因为它通过传递一个struct pollfd数组来管理文件描述符数组的大小由用户指定理论上只受限于系统内存和进程地址空间。有上限问题和我 poll 无关这与select不同select使用fd_set结构其大小固定通常由FD_SETSIZE定义因此能监视的文件描述符数量有限。由于poll调用时传递的是动态数组所以它能够处理的文件描述符数量没有硬性上限从而更适用于需要监视大量文件描述符的场景。poll 的优缺点优点无文件描述符数量限制与select不同poll不受文件描述符数量的限制可以处理更多的文件描述符。使用更灵活poll的接口相对更直观不需要像select那样进行复杂的位操作。注意poll和select每一次系统调用全都必须把用户态的 fd 集合完整拷贝进内核这一点二者没有区别这并不是二者效率差距的核心。先纠正事实select用户传入fd_set位图调用select时内核把整张 fd 位图从用户态拷贝到内核调用结束后再把修改后的就绪位图拷回用户态。poll用户传入struct pollfd[]数组调用poll时内核同样要把整个 pollfd 数组完整拷贝进内核内核填充revents之后整体拷贝回用户态。只要是单次调用二者都存在全量用户态↔内核态拷贝海量 fd 场景下这份拷贝开销两者都有这不是性能差距的根源。两者底层基本都是一样的select 与 poll 真实的核心性能差距来源1. 数据结构遍历开销最关键select位图 fd_set内核需要循环遍历整个位图的最大 fd 编号。 例如监听 fd1000哪怕只开了几个 fd内核依旧要循环检查 01000 全部下标无效遍历极多。 fd 数值越大无效循环越多。poll数组形式的 pollfd内核只需要遍历数组里实际传入的有效 fd 个数遍历次数精准等于你传给它的 nfds不会无脑遍历到最大 fd。 这是 poll 对比 select 天然的优势。2. 用户态使用成本select 返回后用户必须循环扫描整张 fd_set 位图挨个判断哪一位被置位poll 直接遍历数组判断每个元素的revents逻辑直观无效判断更少。3. fd 数量上限约束select 受内核默认FD_SETSIZE默认 1024硬性限制想要监听超过 1024 个 fd 必须重新编译内核 poll 没有这个编译期硬上限仅受进程最大打开文件数限制。缺点线性扫描poll仍然需要线性扫描整个文件描述符数组来确定哪些文件描述符准备好当文件描述符数量非常多时性能可能会受到影响。在面对大量客户端连接服务器的场景时如果只有少数客户端存在数据交互多路转接技术是较为适用的解决方案。像select和poll这两种多路转接方式虽然在一定程度上能满足需求但存在效率问题。随着监听的文件描述符数量不断增加其效率会呈线性下降趋势。文件描述符增多会导致轮询周期延长进而降低响应用户的效率这正是poll的一个明显缺陷。为克服这一缺陷epoll应运而生它作为第三种多路转接方式能有效解决select和poll在高并发场景下效率低下的问题。需要注意的一点当struct pollfd结构体中fd设置为-1那么内核不关心这类fd的events你要监听 3 个客户端连接struct pollfd fds[3]; fds[0].fd 3; // 客户端1 fds[1].fd 4; // 客户端2 fds[2].fd 5; // 客户端3突然客户端 2 断开了你不想监听它了fds[1].fd -1; // 内核不再监听这个位置资源消耗虽然poll没有文件描述符数量的限制但过多的文件描述符仍然会消耗大量系统资源。改写上一篇的 select 代码变成 poll 服务器PollServer.hpp#pragma once #include iostream #include memory #include unistd.h #include sys/poll.h #include Socket.hpp #include Log.hpp using namespace SocketModule; using namespace LogModule; class PollServer { const static int size 4096; // 最大事件数 const static int defaultfd -1; public: PollServer(int port) : _listensock(std::make_uniqueTcpSocket()), _isrunning(false) { _listensock-BuildTcpSocketMethod(port); for (int i 0; i size; i) { _fds[i].fd defaultfd; _fds[i].events 0; _fds[i].revents 0; } _fds[0].fd _listensock-Fd(); _fds[0].events POLLIN; // 监听套接字的读事件就绪 } ~PollServer() { } void Start() { int timeout -1; // 超时时间 _isrunning true; while (_isrunning) { PrintFd(); // 打印当前的 fd 状态 int n poll(_fds, size, timeout); // 阻塞等待事件就绪 switch (n) { case -1: LOG(LogLevel::ERROR) poll error; break; case 0: LOG(LogLevel::INFO) poll time out ...; break; default: // 有事件就绪了: 不仅仅是新连接到来了还有读事件就绪了未来还可以有写事件就绪 LOG(LogLevel::DEBUG) poll has events ... event num: n; sleep(1); // 处理事件 Dispatcher(); // 处理就绪的事件 break; } } _isrunning false; } void Stop() { _isrunning false; } void PrintFd() { for (int i 0; i size; i) { if (_fds[i].fd defaultfd) { continue; } LOG(LogLevel::DEBUG) fd: _fds[i].fd , events: _fds[i].events , revents: _fds[i].revents; } } private: // 连接管理器 void Accepter() // 新连接到来处理 { InetAddr client; int sockfd _listensock-Accept(client); // 这里的 Accept 就不会阻塞了因为 listen 套接字已经就绪了这就是把等过程和拷贝的过程分离了 if (sockfd 0) { // 获取新连接成功 LOG(LogLevel::DEBUG) get a new link, sockfd: sockfd , client is: client.StringAddr(); // 获取新连接到来成功然后呢可以直接进行 read/recv() 操作吗 // 可不敢我们获得新连接下一步要做的是将新的 sockfd 托管给 select // 如何托管 ---- 将新连接的 sockfd 添加到 _fd_array 中然后 while 循环中将再次调用 select int pos 0; for (; pos size; pos) { if (_fds[pos].fd defaultfd) { break; } } if (pos size) { LOG(LogLevel::ERROR) poll server is full!; close(sockfd); // 关闭新连接 } else { _fds[pos].fd sockfd; // 将新连接的 sockfd 添加到 _fd_array 中 _fds[pos].events POLLIN; // 监听新连接的读事件 _fds[pos].revents 0; // 可做可不做 } } } // IO 处理器 void Recver(int pos) // 普通fd收到数据的读事件处理 { // 处理 sockfd 读事件 // 我们在这里读取的时候就不会阻塞了 --- 因为 select 已经完成等操作了 char buf[1024]; ssize_t n recv(_fds[pos].fd, buf, sizeof(buf - 1), 0); // recv 读的时候会有BUG因为无法保证能够收到一个完整的请求--- TCP 是流式协议 // 我们目前先不做处理等到 epoll 的时候再做处理 if (n 0) { buf[n] 0; LOG(LogLevel::DEBUG) Client say# buf; } else if (n 0) { // 客户端关闭连接 LOG(LogLevel::DEBUG) Client close the link, sockfd: _fds[pos].fd; close(_fds[pos].fd); // 关闭连接 _fds[pos].fd defaultfd; // 将 sockfd 从 _fd_array 中移除 _fds[pos].events 0; // 取消监听 _fds[pos].revents 0; // 可做可不做 } else { // 读错误 LOG(LogLevel::ERROR) recv error, sockfd: _fds[pos].fd; close(_fds[pos].fd); // 关闭连接 _fds[pos].fd defaultfd; // 将 sockfd 从 _fd_array 中移除 _fds[pos].events 0; // 取消监听 _fds[pos].revents 0; // 可做可不做 } } // 事件派发器 void Dispatcher() { // 就不仅仅是处理新连接到来还可以处理读事件就绪 // 只要指定的文件描述符在 rfds 中就证明该 fd 就绪了 for (int i 0; i size; i) { if (_fds[i].fd defaultfd) { continue; } // fd 合法并不一定就绪 if (_fds[i].revents POLLIN) // 判断一个文件描述符是否在 rfds 中在就证明该 fd 就绪了 { // listensockfd 新连接到来也是读事件就绪 // sockfd 数据到来也是读事件就绪 // 怎么区分 if (_fds[i].fd _listensock-Fd()) { // 新连接到来 Accepter(); } else { // sockfd 数据到来 // 处理 sockfd 读事件 Recver(i); } } // else if (_fds[i].revents POLLOUT) } } private: std::unique_ptrSocket _listensock; bool _isrunning; struct pollfd _fds[size]; // 我们这里写成指针也是可以的这样就可以进行扩容了 };Main.cc#include PollServer.hpp int main(int argc, char *argv[]) { if (argc ! 2) { std::cout Usage: argv[0] port std::endl; exit(USAGE_ERR); } Enable_Console_Log_Strategy(); uint16_t port std::stoi(argv[1]); std::unique_ptrPollServer svr std::make_uniquePollServer(port); svr-Start(); return 0; }初始化和配置在构造函数中PollServer初始化一个监听套接字_listensock并设置其监听端口。同时它创建一个pollfd数组_fds来存储需要监控的文件描述符及其相关事件。监听套接字的文件描述符被添加到_fds数组的第一个元素中并设置为监控读事件POLLIN。事件循环Start()方法启动服务器的主事件循环。它调用poll系统调用来等待文件描述符上的事件。poll的超时时间设置为-1表示无限等待。根据poll的返回值服务器处理不同的事件如果poll返回-1表示发生错误服务器记录错误日志。如果poll返回0表示超时服务器记录超时日志。如果poll返回大于0的值表示有文件描述符准备好了服务器调用Dispatcher()方法来处理这些事件。事件处理Dispatcher()方法遍历_fds数组检查每个文件描述符的revents字段以确定哪些事件已经发生。如果监听套接字的文件描述符准备好了即revents包含POLLIN则调用Accepter()方法来接受新的客户端连接。如果其他文件描述符准备好了即revents包含POLLIN则调用Recver(int pos)方法来处理接收到的数据。连接管理Accepter()方法接受新的客户端连接并将新的文件描述符添加到_fds数组中设置为监控读事件。如果_fds数组已满即没有可用的空位则关闭新连接并记录错误日志。数据处理Recver(int pos)方法从指定的文件描述符读取数据并根据读取结果进行相应的处理。如果读取成功它记录接收到的数据如果读取失败或客户端关闭连接则关闭文件描述符并从_fds数组中移除。停止服务器Stop()方法停止服务器的主事件循环将_isrunning设置为false。这段代码提供了一个基于poll的服务器的基本框架可以根据需要进行扩展和优化。它展示了如何使用poll系统调用来处理多个文件描述符的 I/O 事件并提供了基本的连接管理和数据处理功能。由于 poll 的缺点我们下一篇开启 epoll --- 多路转接的最后一个话题
返回列表