多路转接之poll与epoll

poll

相比于select函数,poll的跨平台移植性不如select,因为poll函数只能在linux环境中使用,也是采用轮询遍历的方式。

与select函数的不同之处:

  1. poll不会限制文件描述符的个数
  2. 文件描述符对应一个事件结构,这个结构中有两个事件,一个是要监控的文件描述符,另一个是这个文件描述符所对应的事件

函数接口

#include <poll.h>

int poll(struct pollfd *fds, nfds_t nfds, int timeout);
  • fds:事件结构数组
  • nfds:fds数组中有效的元素个数
  • timeout:超时时间

在这里插入图片描述
struct pollfd 结构体

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.  */      
  };

其中:

  • fd:该结构体所关心的文件描述符数值
  • events:所关心这个文件描述符的那些事件,事件与事件之间采用按位或的方式连接
  • revents:当关心的文件描述符产生对应的关心的事件时,返回给调用者发生的事件(每次监控的时候,就会被初始化为空)

在这里插入图片描述
events 和 revents的取值 :

事件 描述 是否可以作为输入 是否可以作为输出
POLLIN 普通或优先级带数据可读
POLLRDNORM 普通数据可读
POLLRDBAND 优先级带数据可读(linux不支持)
POLLPRI 高优先级数据可读,比如TCP带外数据
POLLOUT 数据可写(普通数据和优先数据)
POLLWRNORM 普通数据可写
POLLWRBAND 优先级带数据可写
POLLERR 发生错误
POLLHUP 发生挂起,管道的写端关闭后,读端描述符将受到POLLHUP事件
POLLNVAL 描述字不是一个打开的文件

编程实例

#include <iostream>
#include <cstdio>
#include <unistd.h>
#include <poll.h>

using namespace std;

int main()
{
	//创建一个struct pollfd结构体,关心0号文件描述符,标准输入
	//监视可读事件
    struct pollfd poll_fd;
    poll_fd.fd = 0;
    poll_fd.events = POLLIN;
	
	//轮询遍历
    while(1)
    {
        int ret = poll(&poll_fd,1,1000);
        if(ret < 0)//poll出错
        {
            perror("poll");
            sleep(3);
            continue;
        }
        else if(ret == 0)//监控超时
        {
            cout<<"poll timeout"<<endl;
            continue;
        }
        else if(poll_fd.revents == POLLIN)//当发送的事件时可读事件时
        {
			//读取数据
            char buf[1024] = {'\0'};
            read(0,buf,sizeof(buf) - 1);
            cout<<"input : "<<buf<<endl;
        }
    }

    return 0;
}

在这里插入图片描述
如果我们一次所传入的数据超过了一次可以接受的数据大小时,就会别分批进行接收

poll 的优缺点:
优点:

  1. poll采用了事件结构的方式,简化了程序,并且他不限制文件描述符的个数
  2. 不需要我们重新添加文件描述符到事件的结构数组当中

缺点:

  1. poll也是需要轮询遍历事件结构数组,那么当文件描述符增多的时候,性能可能不会那么好了
  2. poll不支持跨平台操作
  3. poll不会告诉我们哪一个文件描述符就绪了,想要找到这个就绪的文件描述符,就需要我们进行遍历操作
  4. poll在操作的过程中,也是需要时间将结构数组从用户空间拷贝到内核空间中,再将内核空间拷贝到用户空间

epoll

man手册中,对于epoll的描述是:

The  epoll API performs a similar task to poll(2): monitoring multiple file descriptors to see if
       I/O is possible on any of them. 

也就是:

epoll API执行与poll(2)类似的任务:监视多个文件描述符,看看是否存在
I/O是可能的任何一个

epoll是为了处理大量的文件描述符而改进的poll函数

函数接口

创建epoll操作句柄:

#include <sys/epoll.h>

int epoll_create(int size);
  • size:定义的是epoll中最大可以监控的文件描述符个数,但是在linux2.6.8之后,size参数是被忽略的,变成了可以扩容的方式。(size 不可以传入一个负数)

在使用完之后,必须使用close函数进行关闭

epoll 的操作:

#include <sys/epoll.h>

int epoll_ctl(int epfd, int op, int fd, 
		struct epoll_event *event);
  • epfd:之前使用epoll_create函数返回的epoll操作句柄
  • op:想让epoll_stl函数所做的哪些事

在这里插入图片描述

  • fd:告诉epoll函数,我们所关心的文件描述符
  • event:他是一个struct epoll_event类型的结构体,表示的是epoll事件的结构
typedef union epoll_data    
{    
  void *ptr;    
  int fd;    
  uint32_t u32;    
  uint64_t u64;    
} epoll_data_t;    
    
struct epoll_event    
{    
  uint32_t events;  /* Epoll events */    
  epoll_data_t data;    /* User data variable */    
} __EPOLL_PACKED;  

struct epoll_event :

  1. 第一个参数:uint32_t events,是我们想让文件描述符关心的事件集合
    其中,EPOLLIN,是可读事件;EPOLLOUT,是可写事件

  2. 第二个参数 epoll_data_t data,他是一个epoll_data类型的联合结构体

typedef union epoll_data :

  1. void* ptr:这里可以传递的信息,就是当epoll监控的文件描述符就绪的时候,等到函数返回,我们可以通过ptr拿到这些信息;其在使用的时候,传入一个结构体,(自定义结构体)struct my_epoll_data{int fd},必须在结构体中包含文件描述符
  2. int fd:我们所关心的文件描述符,可以当做文件描述符中的事件就绪之后,返回给我们时查看;其取值为文件描述符的数值
  3. ptr和fd,两者在使用的时候,只能选其中的一个。

在这里插入图片描述

监控:

 #include <sys/epoll.h>

int epoll_wait(int epfd, struct epoll_event *events,
         int maxevents, int timeout);
  1. epfd:epoll操作句柄
  2. events:epoll事件结构数组。作为出参,他返回的是就绪的事件结构(每一个事件结构都对应一个文件描述符)
  3. maxevents:最大可以拷贝的事件结构数量
  4. timeout:超时时间

在这里插入图片描述
epoll 的工作原理:

在这里插入图片描述

  1. 当某一进程调用epoll_create方法的时候,linux内核会创建一个eventpoll结构体,这个结构体中有两个成员与epoll使用方式相关,分别是红黑树和双向链表
  2. 那些没有就绪的事件,用于存放通过epoll_ctl方法向epoll对象添加进来的事件,都会被挂载在红黑树中(这样就可以识别出重复的事件,并且还让查找效率提高)
  3. 当红黑树中某个事件就绪后,就会被拷贝到双向链表中(这是一个内核行为)。所以说,双向链表中的数据都是就绪的
  4. 当调用epoll_wait函数的时候,双向链表向参数中进行拷贝的方式,是通过改变页表,直接指向物理内存中的双向链表中就绪事件所占用的物理内存

在这里插入图片描述

epoll的两种工作方式

水平触发 LT模式

在这种模式下,当epoll中检测到了等待触发的事件就绪后,可以不立即进行处理,而是只处理一部分。等到第二次调用epoll_wait函数的时候,可以接着操作刚才没有处理完的数据。他支持阻塞读写与非阻塞读写

这是epoll函数的默认工作方式,另外select 和 poll都是水平触发

  • 对于可读事件,只要接收缓冲区中的存在数据,就会一直触发该接收该可读事件,直到没有数据可读

  • 对于可写事件,只要发送缓冲区中存在数据,就会一直触发该可写事件就绪,直到发送缓冲区中没有数据

函数模拟

/*================================================================
*   Copyright (C) 2020 Sangfor Ltd. All rights reserved.
*   
*   文件名称:test.cpp
*   创 建 者:dcl
*   创建日期:2020年06月20日
*   描    述:
*
================================================================*/

#include <unistd.h>
#include <cstdio>
#include <sys/epoll.h>
#include <iostream>
using namespace std;

int main()
{
    int epollfd = epoll_create(10);
    if(epollfd < 0)
    {
        perror("epoll_create");
        return 0;
    }

    struct epoll_event ev;
    ev.events = EPOLLIN;//可读事件
    ev.data.fd = 0;
    //int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
    //添加一个文件描述符到红黑树中
    int ret = epoll_ctl(epollfd,EPOLL_CTL_ADD,0,&ev);
    if(ret < 0)
    {
        perror("epoll_ctl");
        return 0;
    }

    while(1)
    {
        struct epoll_event fd_arr[10];
        //int epoll_wait(int epfd, struct epoll_event *events,
        //                      int maxevents, int timeout);
        int ret = epoll_wait(epollfd,fd_arr,sizeof(fd_arr) / sizeof(fd_arr[0]),3000);
        if(ret < 0)
        {
            perror("epoll_wait");
            continue;
        }
        else if(ret == 0)
        {
            cout<<"timeout"<<endl;
            sleep(1);
            continue;
        }
        cout<<"---"<<ret<<endl;
        //文件描述符被触发
        for(int i = 0; i < ret; i++)
        {
            if(fd_arr[i].data.fd == 0)
            {
                char buf[3] = {'\0'};
                read(fd_arr[i].data.fd,buf,sizeof(buf) - 1);
                cout<<"input : "<<buf<<endl;
            }
        }
    }
    

    return 0;
}

在设置接收数据的大小的时候,我们给数组设置的比较小,当我们发送大量的数据,就会出现下面这种情况
在这里插入图片描述

边缘触发 ET模式

在ET模式下,如果我们一次只是处理了部分数据,那么剩下的数据在下一次触发的时候才会被处理的。

也就是说,ET模式下,当文件描述符就绪后,就只有一次处理文件中数据的机会,所以我们需要使用循环的方式进行接收数据。他支持非阻塞读写

程序模拟:

/*================================================================
*   Copyright (C) 2020 Sangfor Ltd. All rights reserved.
*   
*   文件名称:test.cpp
*   创 建 者:dcl
*   创建日期:2020年06月20日
*   描    述:
*
================================================================*/

#include <errno.h>
#include <unistd.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <cstdio>
#include <iostream>
#include <string>

using namespace std;

inline void slove(struct epoll_event* fd_arr,int ret)
{
    for(int i = 0; i < ret; i++)
        if(fd_arr[i].data.fd == 0)
        {
            string ans = "";
            while(1)
            {
                char buf[3] = {'\0'};
                ssize_t readsize = read(0,buf,sizeof(buf) - 1);
                if(readsize < 0)
                {
                    //说明数据读完了
                    if(errno == EAGAIN || errno == EWOULDBLOCK)
                        break;
                    
                    perror("read");
                    break;
                }

                ans += buf;
                break;
                if(readsize < (ssize_t)sizeof(buf) - 1)
                   break; //数据读完了
            }

            if(ans != "")   cout<<"read data: "<<ans<<endl;
        }

}

int main()
{
    //设置文件描述符属性为非阻塞
    //int fcntl(int fd, int cmd, ... /* arg */ );
    int flag = fcntl(0,F_GETFL);
    fcntl(0,F_SETFL,flag | O_NONBLOCK);

    //创建epoll操作句柄
    int epoll_fd = epoll_create(10);
    if(epoll_fd < 0)
    {
        perror("epoll_create");
        return 0;
    }

    //添加文件描述符
    struct epoll_event ev;
    ev.events = EPOLLIN | EPOLLET;
    ev.data.fd = 0;
    int ret = epoll_ctl(epoll_fd,EPOLL_CTL_ADD,0,&ev);
    if(ret < 0)
    {
        perror("epoll_ctl");
        return 0;
    }

    while(1)
    {
        struct epoll_event fd_arr[10];
        ret = epoll_wait(epoll_fd,fd_arr,sizeof(fd_arr) / sizeof(fd_arr[0]),-1);
        if(ret < 0)
        {
            perror("epoll_wait");
            continue;
        }

        slove(fd_arr,ret);

    }


    return 0;
}


没有循环接收数据时的情况
在这里插入图片描述
数据的接收过程:
在这里插入图片描述
在循环接收的过程中,当某一次我们接收的数据的大小小于我们的最大接收能力的时候,就说明数据读取完了

还有一种意外情况,就是当我们所发送的数据正好是我们最大接收能力的整数倍时,比如说:我们发送一个 123\r\n,我们一次只能接收两个数据,那么当最后一次接收到数据的时候,其实数据读取完毕了,但是read函数还是处于一个等待数据的阶段,就会卡死,造成一个饥饿的情况

  • ET模式,在加上循环后很容易造成饥饿状态

解决方法:将文件描述符设置为非阻塞状态,我们可以调用fcntl函数

int fcntl(int fd, int cmd, ... /* arg */ );
  • fd:所要操作的哪一个文件描述符
  • cmd:fcntl函数需要做的事情
    F_GETFL,获取文件描述符属性,文件描述符通过返回值返回给我们,arg不需要设置
    F_SETFD,设置文件描述符属性,arg就是文件描述符需要设置的属性,类型为int

在这里插入图片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章