本文主要是介绍IO多路转接之poll,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
目录
1. poll 的基本认识
2. poll 基于 select 的突破
3. poll() 系统调用
3.1. struct pollfd 结构
4. poll() 的 demo
5. poll 的总结
1. poll 的基本认识
poll 是一种多路转接的方案, 它的核心功能和 select 一模一样,我们知道 IO = 等待事件就绪 + 拷贝数据, 而它们只负责IO过程中的等待事件就绪;
用户和内核通过 poll 想告诉对方:
- 用户告诉内核 (调用 poll() 时):内核帮用户关心哪些文件描述符的哪些事件;
- 内核告诉用户 (poll() 返回时):哪些文件描述符的哪些事件已经就绪;
2. poll 基于 select 的突破
因为 select 服务器有如下缺点:
- 因为 select 服务器需要维护一个第三方数组,因此,select 服务器会充斥着大量的遍历操作 (时间复杂度O(N));
- 我们知道 fd_set 是一个固定大小的位图,因此也就决定了 select 服务器所能监测的文件描述符的数量是有上限的;
- 除开第一个参数,剩下的后四个参数,都是输入输出型参数,每调用一次 select,用户需要对这些参数进行重新设定;
- 同时,也因为它们是输入输出型参数,即内核和用户都需要对其进行修改,因此,select 会进行频繁的用户到内核,内核到用户的数据拷贝;
- 上面几个问题,也间接导致了 select 服务器的编码比较复杂。
因此,设计者们提出了 poll ,poll 基于 select 的突破:
- poll 将输入型参数和输出型参数进行了分离:这也就意味着用户不用对参数进行重新设定, 这是其一; 同时,也因为输入参数和输出参数分离,poll 不会进行频繁的用户到内核的数据拷贝, 但是内核到用户的数据拷贝是不可少的,这是其二;
- poll 没有最大文件描述符数量的限制:这里的文件描述符的数量由用户决定,poll 自身没有限制,只要服务器有能力承载更多的连接,poll 就可以监测更多的文件描述符。
3. poll() 系统调用
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
首先解释后两个参数。
timeout: timeout 代表超时时间,但这里的 timeout 是以毫秒 (ms) 为单位的。
- 如果 timeout = 0, 代表 poll() 非阻塞等待;
- 如果 timeout = -1, 代表 poll() 阻塞等待;
- 如果 timeout > 0,比如 1000,那么代表,在1000毫秒内,阻塞等待,如果超时,没有事件就绪,那么 poll () 返回0。
nfds:代表 fds 这个数组的长度;
poll() 返回值:
- 如果大于 0, 代表事件已就绪的文件描述符的个数;
- 如果等于 0, time out,没有事件发生;
- 如果小于 0, poll() error,可通过 errno 查看错误原因。
3.1. struct pollfd 结构
struct pollfd 结构如下:
/* Type used for the number of file descriptors. */
typedef unsigned long int nfds_t;/* Data structure describing a polling request. */
struct pollfd
{int fd; /* File descriptor to poll. */short int events; /* Types of events poller cares about. */short int revents; /* Types of events that actually occurred. */
}
int fd:
在使用 poll() 时,无论是用户告诉内核 ( 调用poll() ),还是内核告诉用户 ( poll() 返回时),它们都不会对文件描述符的值做修改,即这个值只要用户设定一次就好了。
- 当用户告诉内核时,这里的 fd 就代表,内核需要关心这个文件描述符的某个IO事件;
- 当内核告诉用户时,这里的 fd 就代表,这个文件描述符的某个IO事件已经就绪了。
光有一个 fd 不够,因为无论是用户告诉内核,还是内核告诉用户,都需要知道这个文件描述符所关心的IO事件是什么。
因此有了 events 和 revents;
- events:代表请求事件。用户告诉内核,用户所关心的这个文件描述符的 IO 事件都在 events 里;
- revents:代表返回事件。内核告诉用户,这个文件描述符上的哪些IO事件 (revents) 已经就绪了;
因此,可以看到, poll 是如何做到将输入型参数和输出型参数分离的呢?
本质上是通过 events 和 revents 这两个参数做到的。
- 用户告诉内核时,只会修改 events 这个数据,而不会对 revents 做任何修改;
- 内核告诉用户时,只会修改 revents 这个数据,而不会对 events 做任何修改;
可是现在有一个问题, events 和 revents 的类型都是 short int 啊,而 short int 才2字节,怎样用 short int 来表示不同的IO事件呢?
这个问题,我们以前就遇到过,在学习基础IO的时候,我们学习的 open 系统调用,也使用了同样的方式,用一个 int 来表示不同的打开方式,那是怎样做到的呢?
用 short int 的不同比特位来表示不同的 IO 事件,具体如下:
事 件 | 描 述 | 是否可作为输入 | 是否可作为输出 |
POLLIN | 数据 ( 包括普通数据和优先数据 ) 可读 | 是 | 是 |
POLLOUT | 数据 ( 包括普通数据和优先数据 ) 可写 | 是 | 是 |
POLLERR | 错误 | 否 | 是 |
POLLPRI | 高优先级数据可读,比如TCP的紧急数据(URG) | 是 | 是 |
上面列举了一些标志位,但它们本质上都是宏,如下:
#define POLLIN 0x001 /* There is data to read. */
#define POLLPRI 0x002 /* There is urgent data to read. */
#define POLLOUT 0x004 /* Writing now will not block. */
#define POLLERR 0x008 /* Error condition. */
关于标志位就说到这里。
我们说过,poll 较于 select 解决了一个问题,poll 没有最大文件描述符数量的限制, 可是,我们发现,poll 的第一个参数是 struct pollfd *fds,这不就是一个数组吗,而数组是有范围的,那么为什么说 poll 没有最大文件描述符数量的限制呢?
- 首先,select 是存在最大文件描述符数量的限制的,因为它受限于 fd_set 位图结构;
- 而 poll 是不存在最大文件描述符数量限制的,它可以是 1024,也可以是 2048,甚至是4096,只要服务器有能力能够承载更多的连接, poll 就可以监测更多的文件描述符, 换言之,poll () 自身没有文件描述符数量的限制,实际上,文件描述符的数量是受用户和服务器的承受能力的限制,只要用户认为有必要且服务器有能力,poll() 就可以监视更多的文件描述符。
4. poll() 的 demo
demo 所需要的小组件,例如 Sock.hpp、Date.hpp、Log.hpp 在 IO多路转接之poll 文章中有。
声明:poll() demo 只处理读事件,即POLLIN,暂时不考虑POLLOUT;
实现思路:
- constructor:
- 创建套接字、监听、绑定;
- 动态申请数组 (struct pollfd* ),并初始化;
- 约定监听套接字为数组的第一个元素;
- 设置超时时间 (如果你愿意的话)。
- start:
- 因为 poll() 将输入参数 (events) 和 输出参数 (revents) 分离,故用户在轮询时不需要对数据进行重新设定;
- 直接调用 poll(), 通过返回值,确定不同的执行策略;
- 当 poll() return > 0 时,代表着有文件描述符的IO事件就绪,此时调用处理事件函数,handleEvent;
- 遍历整个数组,找到事件就绪的文件描述符,判断是监听套接字,还是服务套接字;
- 如果是监听套接字,调用accept,获取新连接,并将新连接 Load 到数组中 (如果可以的话);
- 如果是服务套接字,调用read/recv,拷贝数据。 注意:当read/recv返回0时,代表着对端关闭连接,服务端需要 close 该连接,并将这个套接字在数组中清除。
- 以上就是整体实现思路,在实现过程中,注意耦合度,适当解耦,提高代码的可读性。
#ifndef _POLL_SERVER_HPP_
#define _POLL_SERVER_HPP_#include "Log.hpp"
#include "Date.hpp"
#include "Sock.hpp"
#include <poll.h>
#include <sys/socket.h>
#include <sys/types.h>#define NUM 1024
#define FD_NONE -1namespace Xq
{class PollServer{public:PollServer(uint16_t port = 8080){_sock.Socket();_sock.Bind("", port);_sock.Listen();_nfds = NUM;_poll_fd = new struct pollfd[_nfds];// 初始化数组for(size_t i = 0; i < _nfds; ++i){_poll_fd[i].fd = FD_NONE;_poll_fd[i].events = _poll_fd[i].revents = 0;}// 约定监听套接字为数组的第一个元素_poll_fd[0].fd = _sock._sock;_poll_fd[0].events = POLLIN;_timeout = 1000; // 以毫秒为单位LogMessage(DEBUG, "poll server init success");}void start(void){while(true){
#ifdef DEBUG_SHOWdebug_show();
#endifint n = poll(_poll_fd, _nfds, _timeout);if(n == 0){LogMessage(DEBUG, "time out...");}else if(n < 0){LogMessage(ERROR, "error: %d, error message: %s", errno, strerror(errno));}else{LogMessage(DEBUG, "ready IO enevt num: %d", n);handleEvent();}}}void handleEvent(void){for(nfds_t pos = 0; pos < _nfds; ++pos){// 用户不关心的文件描述符跳过if(_poll_fd[pos].fd == FD_NONE) continue;// 如果读事件就绪if(_poll_fd[pos].revents & POLLIN){// 如果是listen sock, accept 获取新连接if(_poll_fd[pos].fd == _sock._sock){Accepter();}// 如果是server sock, read/recv, 拷贝数据else{Recver(pos);}}// 读事件未就绪else{
#ifdef DEBUG_SHOWLogMessage(DEBUG, "%d file descriptor IO enent not ready", _poll_fd[pos].fd);
#endif}}}void Accepter(void){std::string client_ip;uint16_t client_port;int server_sock = _sock.Accept(client_ip, &client_port);if(server_sock < 0){LogMessage(ERROR, "error: %d, error message: %s", errno, strerror(errno));return ;}// accept successLogMessage(DEBUG, "get a new link, [%s:%d] server sock: %d",\client_ip.c_str(), client_port, server_sock);size_t pos = 1;for(; pos < _nfds; ++pos){if(_poll_fd[pos].fd == FD_NONE)break;}// 走到这里有两种情况// case 1: 数组已满, 将这个新连接关掉if(pos == _nfds){close(server_sock);LogMessage(WARNING, "poll array full");}// case 2: break 跳出循环, 添加即可else{// 将服务套接字添加到这个数组即可_poll_fd[pos].fd = server_sock;_poll_fd[pos].events = POLLIN;}}void Recver(int pos){char buffer[NUM] = {0};// 此时一定不会被阻塞ssize_t real_size = recv(_poll_fd[pos].fd, buffer, sizeof buffer - 1, 0);if(real_size < 0){LogMessage(ERROR, "errno: %d, errno message: %s", errno, strerror(errno));// 1. 关闭这个服务套接字close(_poll_fd[pos].fd);// 2. 将这个位置的数据还原_poll_fd[pos].events = _poll_fd[pos].revents = 0;_poll_fd[pos].fd = FD_NONE;}else if(real_size == 0){ LogMessage(DEBUG, "client close the link [fd : %d], me too...", _poll_fd[pos].fd);// 1. 关闭这个服务套接字close(_poll_fd[pos].fd);// 2. 将这个位置的数据还原_poll_fd[pos].events = _poll_fd[pos].revents = 0;_poll_fd[pos].fd = FD_NONE;}else {// recv successbuffer[real_size - 1] = 0;LogMessage(DEBUG, "[fd : %d] echo$ %s", _poll_fd[pos].fd, buffer);LogMessage(DEBUG, "server get a message of client success");}}void debug_show(void){std::cout << "fd_array[]: ";for(size_t i = 0; i < _nfds; ++i){if(_poll_fd[i].fd == FD_NONE) continue;else std::cout << _poll_fd[i].fd << " ";}std::cout << std::endl;}~PollServer(){// 释放我们动态申请的资源if(_poll_fd)delete[] _poll_fd;}private:Sock _sock;nfds_t _nfds;struct pollfd* _poll_fd;int _timeout;};
}
#endif
5. poll 的总结
poll 的优点:
- IO效率高:因为 poll 服务器可以一次性等待多个套接字就绪,而IO过程 = 等待事件就绪 + 拷贝数据,而 poll 会将多个套接字的等待时间进行重叠,换言之,在单位时间内,poll 服务器等待的比重是比较低的,因此,它的IO效率就高;
- 有适合 poll 的应用场景:当有大量的连接,但只有少量连接是活跃的。因为 poll 服务器是单进程的,因此,对于 poll 服务器的维护成本非常低 (不需要维护过多的执行流),哪怕有非常多的连接,poll 服务器的成本也微乎其微,即节省资源。
- 输入输出参数分离:用户不需要对参数进行重复设定;
- poll 没有最大文件描述符数量的限制:站在 poll 自身视角,它没有文件描述符的上限,只要服务器的资源足够,能够承载更多的连接,它就可以监测更多的文件描述符;
poll 的缺点:
- poll 依旧需要遍历操作 (时间复杂度 O(N)): 无论是用户层面,还是内核层面,都需要对数组进行遍历, 特别是连接非常多的情况,此时 poll 就可能会高频的检测到IO事件就绪,进而导致高频的遍历操作,导致效率降低;
- poll 需要内核到用户的拷贝:确切的说,因为输入输出参数分离,故用户不需要每次重新设定参数 (只需要第一次用户将数据拷贝给内核),而大部分都是内核将数据拷贝给用户,这是少不了的;
poll 最核心的缺点是第一点,即用户还是需要维护一个数组,无论是用户和内核,都需要对这个数组进行遍历操作。
那么 poll 的缺点如何解决呢? 因此我们需要学习 epoll (event poll) ,具体细节在下篇文章 IO多路转接之epoll 。
这篇关于IO多路转接之poll的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!