高效IO——多路转接之poll

Posted 两片空白

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了高效IO——多路转接之poll相关的知识,希望对你有一定的参考价值。

目录

前言

一.poll函数接口

        1.1 struct pollfd 结构介绍

        1.2 poll简单使用

二.socket就绪条件

三.poll优缺点

        3.1 poll优点

        3.2 poll缺点

四.poll的使用


前言

        poll是针对select的缺点,做了改进。

        主要针对了select的两个缺点:

  • select等待的文件描述符有上限,为sizeof(fd_set)*8。
  • selectfd_set参数是输入输出参数,每次select前都需要重新设定fd_set变量。

        poll改进为:

  • 可以等待的文件描述符没有上限。
  • 文件描述符事件输入和输出用的不是一个变量,每次poll前不需要重新设定。

        但是poll和select功能是一样的,主要的工作还是等多个文件描述符的时间就绪。

一.poll函数接口

#include <poll.h>

int poll(struct pollfd *fds, nfds_t nfds, int timeout);

接口作用:监听多个文件的事件是否就绪。 

参数作用
fdspoll函数监听的结构列表。一个元素中,报汉三个部分:文件描述符,监听事件集合,返回事件集合。指针指向struct pollfd数组。下面有详细介绍。
nfds表示fds数组的长度
timeout表示poll函数的超时时间范围毫秒。设置为-1:阻塞等待;0:非阻塞等待;具体某一值:等待时间。

返回值:

  • 小于0:poll调用出错
  • 等于1:超过等待事件
  • 大于0:表示等待的文件中有多少个文件事件就绪。

        1.1 struct pollfd 结构介绍

struct pollfd {
	int   fd;         /* file descriptor */
	short events;     /* requested events */
	short revents;    /* returned events */
};
成员含义
fd需要监听文件的文件描述符
events用户通知内核,需要监听的事件,这个是由用户设置
revents内核通知用户,监听事件是否就绪,这个是有内核设置

events和revents取值:

我们主要用到的是POLLIN和POLLOUT两个。

在struct pollfd中,events和revents的类型是short类型。上面的每一个值都是一个宏,在二进制中都只有一个位为1,并且为1的那一位位置不同。

当需要监视一个文件的多个事件时,我们可以将不同宏按位或起来,这个方法为多标志位法。

当需要监视一个文件的多个事件时,可以使用多标志法,将不同的宏或起来。

比如:需要监视一个文件的读和写事件,对应宏为POLLIN和POLLOUT。

假设:POLLIN的二进制表示为0000 0000 0000 0001

           POLLOUT的二进制表示为0000 0000 0000 0010

按位或起来后:即events = POLLIN | POLLOUT  = 0000 0000 0000 0011

内核怎么是知道要监视文件的哪个事件:

        操作系统只要将事件按位与上对应宏,如果为真,就说明需要监视对应的事件。

        即:if(events & POLLIN)为真说明需要监视读事件。if(events & POLLOUT)说明需要监视写事件。

用户怎么确认什么事件就绪:

        判断返回的pollfd结构的revents,与对应事件按位与即可。

        如:if(revents & POLLIN)说明读事件就绪。

        1.2 poll简单使用

编写一个从标准输入读数据 的poll

#include <stdio.h>
#include <unistd.h>
#include <poll.h>


int main(){
  struct pollfd fds[1];
  //监视标准输入的读
  fds[0].fd = 0;
  fds[0].events = POLLIN;//由用户填写
  fds[0].revents = 0;//由内核填写
  int timeout = -1;
  char c = 0;
  while(1){
    int n = poll(fds,1,timeout);
    if(n > 0){
      //就绪
      if(fds[0].revents & POLLIN){
        printf("read ready...\\n");
        int s = read(0, &c, 1);
        if(s > 0){
          printf("%c",c);
        }
        else{
          perror("read error\\n");
        }
      }
    }
    else if(n == 0){
      //超时
      printf("poll timeout...\\n");
    }
    else{
      //出错
      perror("poll error\\n");

    }
  }
  return 0;
}

        注意:如果一个事件就绪,但是不进行处理,该事件会一直是就绪的。

比如:监听标准输入的读事件,当输入字符,此时标准输入的读事件是就绪的,但是,如果用户不去读,标准输入的读事件就一直是就绪的。

二.socket就绪条件

读就绪

  • 在socket内核中,接收缓存区的字节数,大于等于低水位标记SO_RCVLOWAT,此时可以无阻塞的读取该文件。
  • 监听的socket上有了新的连接请求,socket连接请求也是以读的方式获取的。
  • socketTCP通信,对端关闭连接,此时对该socket读返回0.
  • socket上有未处理的错误。

写就绪

  • socket内核中,发送缓冲区中的空闲位置大小,大于或者等于低水位标记。
  • socket使用非阻塞connect连接成功或失败之后。
  • socket的写操作被关闭,对于写操作被关闭的socket进行写操作,会触发SIGPIPE信号。
  • socket上有为读取的错误。

异常就绪:

  • socket收到带外数据。

三.poll优缺点

        3.1 poll优点

对比select:

  • 可以等待的文件描述符没有上限。
  • oillfd结构包含了要监视的event和就绪的revents,输入输出变量不是一个,在调用poll前不需要再重新设定。

        3.2 poll缺点

poll监听的文件描述符数量增多时:

  • 在内核,要知道那些文件的事件是就绪的,需要对所有要监控的文件进行轮询检测,这样当监听的文件过多时,效率不会很高。
  • 每次调用poll都需要把大量的pollfd结构从用户拷贝到内核中。每次调用poll都需要重新将要监视文件事件重新从用户拷贝到内核。

四.poll的使用

  • 要用一个pollfd数组来保存要监视的文件,但是不需要在poll前重新设定数组。
  • poll可以等待的文件是无限的,pollfd数组有大小限制,但是这是用户限制的,并不影响poll可以等待无限个文件。
  • 判断一个文件是哪个事件,用pollfd的revents与对应的事件按位与判断。
  • 如果就绪文件为是socket的返回值,说明有连接来了,要将accpet返回值加入到等待数组中,反之说明IO就绪,进行读或者写。
  • 读就绪,当读返回值为0,说明对端关闭,不需要等待,close对应文件,并且在数组中删除。

连接:

#pragma once 

#include <iostream>
#include <stdlib.h>
#include <unistd.h>

#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <poll.h>

#define BACKLOG 5
using namespace std;

class Sock{
  public:
    static int Socket(){
      int sock = 0;
      sock = socket(AF_INET, SOCK_STREAM, 0);
      if(sock < 0){
        cerr<<"socket error"<<endl;
        exit(1);
      }
      return sock;
    }
    static void Bind(int sock, int port){
      struct sockaddr_in local;
      local.sin_family = AF_INET;
      local.sin_port = htons(port);
      local.sin_addr.s_addr = htons(INADDR_ANY);
      if(bind(sock, (struct sockaddr *)&local, sizeof(local)) < 0){
        cerr << "bind error"<<endl;
        exit(2);
      }
    }

    static void Listen(int sock){
      if(listen(sock,BACKLOG) < 0){
        cerr << "listen error"<<endl;
        exit(3);
      }
    }
    static int Accept(int sock){
      struct sockaddr_in peer;
      socklen_t len = sizeof(peer);
      return accept(sock, (struct sockaddr *)&peer, &len);
    }
    static void SetSockOpt(int sock){
      int opt =1;
      setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
    }
};

服务器代码: 

#pragma once 

#include "Sock.hpp"

#define SIZE 20

class PollServer{
  private:
    int _lsock;
    int _port;
    struct pollfd fds[SIZE];//保存监视的文件
  public:
    PollServer(int lsock = -1, int port = 8081)
      :_lsock(lsock)
      ,_port(port)
    {}

    void InitServer(){
      _lsock = Sock::Socket();
      Sock::SetSockOpt(_lsock);
      Sock::Bind(_lsock, _port);
      Sock::Listen(_lsock);
      //初始化监视的文件
      for(int i=0; i < SIZE; i++){
        fds[i].fd = -1;
        fds[i].events = 0;
        fds[i].revents = 0;
      }
      //加入连接套接字
      fds[0].fd = _lsock;
      fds[0].events = POLLIN;
      fds[0].revents = 0;

    }
    //找到文件描述符为-1的加入到数组中
    void AddSock2Fds(int sock){
      int i =0;
      for(; i < SIZE; i++){
        if(fds[i].fd == -1){
          break;
        }
      }
      if(i >= SIZE){
        cout<<"full"<<endl;
        close(sock);
      }
      else{
        fds[i].fd = sock;
        fds[i].events = POLLIN;//设置为读事件
        fds[i].revents = 0;

      }

    }
    //删除一个不用监视的文件
    void DelSock(int i){
      fds[i].fd = -1;
      fds[i].events = 0;
      fds[i].revents = 0;
    }
    void Handler(){
      for(int i = 0; i < SIZE; i++){
        //用revents判断是否就绪
        if(fds[i].revents & POLLIN){
      
          if(fds[i].fd == _lsock){
            //有连接来了,加入到fds中
            int sock = Sock::Accept(_lsock);
            if(sock >= 0){
              cout <<"get a link..."<<endl;
              AddSock2Fds(sock);
            }
            else{
              cerr << "accept error"<<endl;
            }

          }
          else{
            //IO就绪
            char buf[10240] ={0};
            int n = recv(fds[i].fd, buf, sizeof(buf),0);
            if(n > 0){
              cout << "client# "<<buf<<endl;
            }
            else if(n == 0){
				//对端关闭
              cout<<"client close..."<<endl;
              close(fds[i].fd);
              DelSock(i);
            }
            else{
              cerr << "recv error"<<endl;
              close(fds[i].fd);
              DelSock(i);
            }

          }
        }
      }

    }
    void Start(){
      int timeout = -1;
      while(1){
        int num = poll(fds, SIZE, timeout);
        if(num > 0){
          Handler();

        }
        else if(num == 0){
          //超时
          cerr << "timeout..."<<endl;
        }
        else{
          cerr << "poll error..."<<endl;
        }
      }

    }

    ~PollServer(){
      for(int i =0; i < SIZE; i++){
        if(fds[i].fd != -1){
          close(fds[i].fd);
        }
      }
    }
};
#include "PollServer.hpp"
void Notice(){
  cout << "Notice:\\n\\t"<<"please port"<<endl;
}

int main(int argc, char* argv[]){
  if(argc != 2){
    Notice();
    exit(6);
  }
  PollServer *ps = new PollServer(atoi(argv[1]));
  ps->InitServer();
  ps->Start();
  delete ps;

  return 0;
}

以上是关于高效IO——多路转接之poll的主要内容,如果未能解决你的问题,请参考以下文章

五种高阶IO模型以及多路转接技术(selectpoll和epoll)及其代码验证

五种高阶IO模型以及多路转接技术(selectpoll和epoll)及其代码验证

五种高阶IO模型以及多路转接技术(selectpoll和epoll)及其代码验证

Linux 高级IO

Linux 高级IO

多路转接(IO复用)接口介绍