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;

实现思路:

  1. constructor:
    1.  创建套接字、监听、绑定;
    2.  动态申请数组 (struct pollfd* ),并初始化;
    3.  约定监听套接字为数组的第一个元素;
    4.  设置超时时间 (如果你愿意的话)。
  2. start:
    1. 因为 poll() 将输入参数 (events) 和 输出参数 (revents) 分离,故用户在轮询时不需要对数据进行重新设定;
    2. 直接调用 poll(), 通过返回值,确定不同的执行策略;
    3. 当 poll() return > 0 时,代表着有文件描述符的IO事件就绪,此时调用处理事件函数,HandleEvent;
    4. 遍历整个数组,找到事件就绪的文件描述符,判断是监听套接字,还是服务套接字;
    5. 如果是监听套接字,调用accept,获取新连接,并将新连接 Load 到数组中;
    6. 如果是服务套接字,调用read/recv,拷贝数据。 注意:当read/recv返回0时,代表着对端关闭连接,服务端需要 close 该连接,并将这个套接字在数组中清除。
  3. 以上就是整体实现思路,在实现过程中,注意耦合度,适当解耦,提高代码的可读性。
#ifndef _POLLSERVER_HPP_
#define _POLLSERVER_HPP_

#include "Date.hpp"
#include "Log.hpp"
#include "Sock.hpp"
#include <sys/poll.h>

namespace Xq
{
	const static int NUM = 1;
	const static int FD_NONE = -1;
	const static int timeout = 1000;   // 以毫秒为单位
	class PollServer
	{
	public:
		PollServer(uint16_t port = 8080)
			:_port(port)
			, _nfds(NUM)
		{
			_sock.Socket();
			_sock.Bind("", _port);
			_sock.Listen();

			_pollfd = new struct pollfd[_nfds];
			// 初始化数组
			for (nfds_t i = 0; i < _nfds; ++i)
			{
				_pollfd[i].fd = FD_NONE;
				_pollfd[i].events = _pollfd[i].revents = 0;
			}
			// 约定监听套接字为数组的第一个元素
			_pollfd[0].fd = _sock._sock;
			_pollfd[0].events = POLLIN;

			LogMessage(DEBUG, "server init success");
		}

		void start(void)
		{
			while (true)
			{

#ifdef DEBUG_SHOW
				debug_show();
#endif 

				int n = poll(_pollfd, _nfds, -1);
				if (n == 0)
				{
					LogMessage(NORMAL, "time out ... ");
				}
				else if (n < 0)
				{
					LogMessage(ERROR, "errno: %d, errno message: %s", errno, strerror(errno));
				}
				else
				{
					LogMessage(DEBUG, "IO event already, can handle");
					HandleEvent();
				}

			}
		}


		void HandleEvent()
		{
			for (nfds_t pos = 0; pos < _nfds; ++pos)
			{
				// 用户不关心的文件描述符跳过
				if (_pollfd[pos].fd == FD_NONE) continue;
				else
				{
					// 如果读事件就绪
					if (_pollfd[pos].revents & POLLIN)
					{
						// 如果是listen sock, accept 获取新连接
						if (_pollfd[pos].fd == _sock._sock)
						{
							Accepter();
						}
						// 如果是server sock, read / recv, 拷贝数据
						else
						{
							Recver(pos);
						}
					}
					// 读事件未就绪
					else
					{
#ifdef DEBUG_SHOW
						LogMessage(DEBUG, "%d file descriptor IO event not ready", _pollfd[pos].fd);
#endif
					}
				}
			}
		}

		// 扩容
		void broaden_capacity()
		{   // 1. 确定新容量
			nfds_t new_capacity = 2 * _nfds;
            // 2. 开辟新空间
			struct pollfd* newpollfd = new struct pollfd[new_capacity];
            // 3. 拷贝数据
			for (nfds_t pos = 0; pos < new_capacity; ++pos)
			{
				if (pos < _nfds)
				{
					newpollfd[pos].fd = _pollfd[pos].fd;
					newpollfd[pos].events = _pollfd[pos].events;
					newpollfd[pos].revents = _pollfd[pos].revents;
				}
				else
				{
					newpollfd[pos].fd = FD_NONE;
					newpollfd[pos].events = 0;
					newpollfd[pos].revents = 0;
				}
			}
            // 4. 释放旧空间
			delete[] _pollfd;
            // 5. 更新
			_pollfd = newpollfd;
			_nfds = new_capacity;
		}

		void Accepter()
		{
			std::string client_ip;
			uint16_t client_port;
			int server_sock = _sock.Accept(client_ip, &client_port);
			nfds_t pos = 1;
			for (; pos < _nfds; ++pos)
			{
				if (_pollfd[pos].fd == FD_NONE) break;
			}

			// 走到这里有两种情况
			// case 1: 数组已满, 将这个新连接关掉
			//if(pos == _nfds)
			//{
			//  LogMessage(WARNING, "poll array full");
			//  close(server_sock);
			//}
			// case 2: break 跳出循环, 添加即可
			//else
			//{
			//    将服务套接字添加到这个数组即可
			//  _pollfd[pos].fd = server_sock;
			//  _pollfd[pos].events = 1;
			//}


			// 用户也可以采用扩容机制
			// 如果数组满了, 进行扩容
			if (pos == _nfds)
			{
				std::cout << "old_capacity: " << _nfds << std::endl;
				broaden_capacity();
				std::cout << "new_capacity: " << _nfds << std::endl;
			}
			// 此时再添加服务套接字
			_pollfd[pos].fd = server_sock;
			_pollfd[pos].events = 1;


		}
		void Recver(int pos)
		{
			char buffer[1024] = { 0 };
			// 此时一定不会被阻塞
			ssize_t real_size = recv(_pollfd[pos].fd, buffer, sizeof buffer - 1, 0);
			if (real_size < 0)
			{
				LogMessage(ERROR, "errno: %d, errno message: %s, server close the link: %d", errno, strerror(errno), _pollfd[pos]);
				// 1. 关闭这个服务套接字
				close(_pollfd[pos].fd);
				// 2. 将这个位置的数据还原
				_pollfd[pos].fd = FD_NONE;
				_pollfd[pos].events = _pollfd[pos].revents = 0;
			}
			else if (real_size == 0)
			{
				LogMessage(NORMAL, "client [%d] close link, me too", _pollfd[pos]);
				// 1. 关闭这个服务套接字
				close(_pollfd[pos].fd);
				// 2. 将这个位置的数据还原
				_pollfd[pos].fd = FD_NONE;
				_pollfd[pos].events = _pollfd[pos].revents = 0;
			}
			else
			{
				buffer[real_size - 1] = 0;
				LogMessage(NORMAL, "client [%d] echo$ %s", _pollfd[pos], buffer);
			}
		}

		void debug_show(void)
		{
			std::cout << "poll array: ";
			for (nfds_t pos = 0; pos < _nfds; ++pos)
			{
				if (_pollfd[pos].fd == FD_NONE) continue;
				else
				{
					std::cout << _pollfd[pos].fd << " ";
				}
			}

			std::cout << "\n";
		}


		~PollServer(void)
		{
			// 释放用户动态申请的资源
			if (_pollfd)
				delete[] _pollfd;
		}



	private:
		uint16_t _port;
		nfds_t _nfds;
		struct pollfd* _pollfd;
		Sock _sock;
	};
}

#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 。

  • 12
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值