IO多路转接之poll

2024-04-16 18:36
文章标签 io 转接 多路 poll

本文主要是介绍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 _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的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/909574

相关文章

Python实现多路视频多窗口播放功能

《Python实现多路视频多窗口播放功能》这篇文章主要为大家详细介绍了Python实现多路视频多窗口播放功能的相关知识,文中的示例代码讲解详细,有需要的小伙伴可以跟随小编一起学习一下... 目录一、python实现多路视频播放功能二、代码实现三、打包代码实现总结一、python实现多路视频播放功能服务端开

Java IO 操作——个人理解

之前一直Java的IO操作一知半解。今天看到一个便文章觉得很有道理( 原文章),记录一下。 首先,理解Java的IO操作到底操作的什么内容,过程又是怎么样子。          数据来源的操作: 来源有文件,网络数据。使用File类和Sockets等。这里操作的是数据本身,1,0结构。    File file = new File("path");   字

springboot体会BIO(阻塞式IO)

使用springboot体会阻塞式IO 大致的思路为: 创建一个socket服务端,监听socket通道,并打印出socket通道中的内容。 创建两个socket客户端,向socket服务端写入消息。 1.创建服务端 public class RedisServer {public static void main(String[] args) throws IOException {

多路转接之select(fd_set介绍,参数详细介绍),实现非阻塞式网络通信

目录 多路转接之select 引入 介绍 fd_set 函数原型 nfds readfds / writefds / exceptfds readfds  总结  fd_set操作接口  timeout timevalue 结构体 传入值 返回值 代码 注意点 -- 调用函数 select的参数填充  获取新连接 注意点 -- 通信时的调用函数 添加新fd到

Java基础回顾系列-第七天-高级编程之IO

Java基础回顾系列-第七天-高级编程之IO 文件操作字节流与字符流OutputStream字节输出流FileOutputStream InputStream字节输入流FileInputStream Writer字符输出流FileWriter Reader字符输入流字节流与字符流的区别转换流InputStreamReaderOutputStreamWriter 文件复制 字符编码内存操作流(

C++ I/O多路复用 select / poll / epoll

I/O多路复用:在网络I/O中,用 1个或1组线程 管理 多个连接描述符。             如果有至少一个描述符准备就绪,就处理对应的事件             如果没有,就会被阻塞,让出CPU给其他应用程序运行,直到有准备就绪的描述符 或 超时

android java.io.IOException: open failed: ENOENT (No such file or directory)-api23+权限受权

问题描述 在安卓上,清单明明已经受权了读写文件权限,但偏偏就是创建不了目录和文件 调用mkdirs()总是返回false. <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/><uses-permission android:name="android.permission.READ_E

JavaEE-文件操作与IO

目录 1,两种路径 二,两种文件 三,文件的操作/File类: 1)文件系统操作 File类 2)文件内容操作(读文件,写文件) (1)打开文件 (2)关闭文件 (3)读文件/InputStream (4)写文件/OutputStream (5)读文件/reader (6)写文件/writer (7)Scanner 四,练习: 1,两种路径 1)绝对路径

Python---文件IO流及对象序列化

文章目录 前言一、pandas是什么?二、使用步骤 1.引入库2.读入数据总结 前言 前文模块中提到加密模块,本文将终点介绍加密模块和文件流。 一、文件流和IO流概述         在Python中,IO流是用于输入和输出数据的通道。它可以用于读取输入数据或将数据写入输出目标。IO流可以是标准输入/输出流(stdin和stdout),也可以是文件流,网络流等。

标准IO与系统IO

概念区别 标准IO:(libc提供) fopen fread fwrite 系统IO:(linux系统提供) open read write 操作效率 因为内存与磁盘的执行效率不同 系统IO: 把数据从内存直接写到磁盘上 标准IO: 数据写到缓存,再刷写到磁盘上