Linux-高级IO之poll、epoll

举报
End、断弦 发表于 2022/04/08 21:17:36 2022/04/08
【摘要】 poll基本知识 poll的优缺点 epoll epoll工作原理 epoll优点 epoll工作方式 简单的epoll LT服务器

@TOC

poll基本知识

前一篇:高级IO之select(点击直达)讲解了select,下面来看一下poll.
poll的机制和select,管理多个描述符也是进行轮询,根据描述符的状态进行处理,区别是poll没有最大文件描述符数量的限制。

poll函数接口

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

参数说明:

  • fds是一个poll函数监听的结构列表. 每一个元素中, 包含了三部分内容: 文件描述符, 监听的事件集合, 返回的事件集合.
  • nfds表示fds数组的长度.
  • timeout表示poll函数的超时时间, 单位是毫秒(ms)

pollfd结构:

 struct pollfd {
 int fd; /* file descriptor */
 short events; /* requested events */
 short revents; /* returned events */
};

fd是哪个文件描述符,events:用户告诉内核你要帮我关心哪个文件描述符上的什么事件,revent:内核告诉用户,文件描述符上的事件已经就绪了。

events和revents的取值:

事件 描述 是否可作为输入 是否可作为输出
POLLIN 数据(包括普通数据和优先数据)
POLLRDNORM 普通数据可读
POLLRDBAND 优先数据可读(Linux不支持)
POLLPRI 高优先级数据可读,比如TCP外带数据
POLLOUT 数据(包括普通数据和优先数据)
POLLWRNORM 普通数据可写
POLLWRBAND 优先级带数据可写
POLLRDHUP TCP连接被对方关闭,或者对方关闭了写操作
POLLERR 错误
POLLHUP 挂起
POLLNVAL 文件描述符没有打开

我们常用的就是读和写事件。

返回值

  • 返回值小于0, 表示出错;
  • 返回值等于0, 表示poll函数等待超时;
  • 返回值大于0, 表示poll由于监听的文件描述符就绪而返回

poll的优缺点

poll优点

  • pollfd结构包含了要监视的event和发生的event,不再使用select“参数-值”传递的方式. 接口使用比
    select更方便.
  • poll并没有最大数量限制 (但是数量过大后性能也是会下降).

poll缺点

poll中监听的文件描述符数目增多时

  • 和select函数一样,poll返回后,需要轮询pollfd来获取就绪的描述符.
  • 每次调用poll都需要把大量的pollfd结构从用户态拷贝到内核中.
  • 同时连接的大量客户端在一时刻可能只有很少的处于就绪状态, 因此随着监视的描述符数量的增长, 其效率也会线性下降.

用poll简单监控标准输入

  1 #include <poll.h>                                                                                             
  2 #include <unistd.h>
  3 #include <iostream>
  4 using namespace std;
  5 int main() {
  6    struct pollfd poll_fd;
  7     poll_fd.fd = 0;
  8      poll_fd.events = POLLIN;
  9       
 10      for (;;) {
 11         int ret = poll(&poll_fd, 1, 1000);
 12         if (ret < 0) {
 13            cerr<<"poll"<<endl;
 14             continue;
 15         }
 16         if (ret == 0) {
 17            cout<<"poll timeout"<<endl;
 18             continue;
 19         }
 20         if (poll_fd.revents == POLLIN) {
 21            char buf[1024] = {0};        
 22             read(0, buf, sizeof(buf) - 1);
 23              cout<<buf<<endl;             
 24         }                                 
 25      }                       
 26 }   

在这里插入图片描述

epoll

它几乎具备了之前所说的一切优点,被公认为Linux2.6下性能最好的多路I/O就绪通知方法.epoll总共有3个接口。

epoll的相关接口

int epoll_create(int size);

参数说明:

  • 自从linux2.6.8之后,size参数是被忽略的. 用完之后, 必须调用close()关闭.
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); 

参数说明:

  • 它不同于select()是在监听事件时告诉内核要监听什么类型的事件, 而是在这里先注册要监听的事件类型.
  • 第一个参数是epoll_create()的返回值(epoll的句柄).
  • 第二个参数表示动作,用三个宏来表示.
  • 第三个参数是需要监听的fd.
  • 第四个参数是告诉内核需要监听什么事

第二个参数

  • EPOLL_CTL_ADD :注册新的fd到epfd中;
  • EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
  • EPOLL_CTL_DEL :从epfd中删除一个fd

struct epoll_event结构:

用 vim /usr/include/sys/epoll.h 看epoll代码

 79 typedef union epoll_data
 80 {
 81   void *ptr;
 82   int fd;
 83   uint32_t u32;
 84   uint64_t u64;
 85 } epoll_data_t;
 86 
 87 struct epoll_event
 88 {
 89   uint32_t events;  /* Epoll events */
 90   epoll_data_t data;  /* User data variable */
 91 } __EPOLL_PACKED;
 92 

在这里插入图片描述

可以看到events是一些宏值

  • EPOLLIN : 表示对应的文件描述符可以读 (包括对端SOCKET正常关闭);
  • EPOLLOUT : 表示对应的文件描述符可以写;
  • EPOLLPRI : 表示对应的文件描述符有紧急的数据可读 (这里应该表示有带外数据到来);
  • EPOLLERR : 表示对应的文件描述符发生错误;
  • EPOLLHUP : 表示对应的文件描述符被挂断;
  • EPOLLET : 将EPOLL设为边缘触发(Edge Triggered)模式, 这是相对于水平触发(Level Triggered)来说的.
  • EPOLLONESHOT:只监听一次事件, 当监听完这次事件之后, 如果还需要继续监听这个socket的话, 需要再次把这个socket加入到EPOLL队列里

epoll_wait

int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

收集在epoll监控的事件中已经发送的事件。
参数说明:

  • 参数events是分配好的epoll_event结构体数组.
  • epoll将会把发生的事件赋值到events数组中 (events不可以是空指针,内核只负责把数据复制到这个events数组中,不会去帮助我们在用户态中分配内存).
  • maxevents告之内核这个events有多大,这个 maxevents的值不能大于创建epoll_create()时的size.
  • 参数timeout是超时时间 (毫秒,0会立即返回,-1是永久阻塞). 如果函数调用成功,返回对应I/O上已准备好的文件描述符数目,如返回0表示已超时, 返回小于0表示函数失败

epoll工作原理

在这里插入图片描述

  • 当进程调用epoll_create时,内核创建一个红黑树,红黑树的结点包含对应的事件,重复添加的事件就可以通过红黑树而高效的识别出来(红黑树的插入时间效率是lgn,其中n为树的高度)
  • 而所有添加到epoll中的事件都会与设备(网卡)驱动程序建立回调关系,也就是说,当响应的事件发生时会调用这个回调方法,这个回调方法在内核中叫ep_poll_callback,它会将发生的事件添加到就绪队列中
  • 在epoll中,对于每一个事件,都会建立一个epitem结构体.
  • epoll_ctrl将要监控的文件描述符进行注册;
  • epoll_wait, 则把发生的事件复制到用户态,同时将事件数量返回给用户. 这个操作的时间复杂度
    是O(1).只需检查就绪队列是否有结点。

epoll优点

  • 接口使用方便: 虽然拆分成了三个函数, 但是反而使用起来更方便高效. 不需要每次循环都设置关注的文件描述符, 也做到了输入输出参数分离开
  • 数据拷贝轻量: 只在合适的时候调用 EPOLL_CTL_ADD 将文件描述符结构拷贝到内核中, 这个操作并不频繁(而select/poll都是每次循环都要进行拷贝)
  • 事件回调机制: 避免使用遍历, 而是使用回调函数的方式, 将就绪的文件描述符结构加入到就绪队列中, epoll_wait 返回直接访问就绪队列就知道哪些文件描述符就绪. 这个操作时间复杂度O(1). 即使文件描述符数目很多, 效率也不会受到影响.
  • 没有数量限制: 文件描述符数目无上限

select、poll、epoll对比
在这里插入图片描述

epoll工作方式

epoll有2种工作方式-水平触发(LT)和边缘触发(ET)

水平触发Level Triggered 工作模式

epoll默认状态下就是LT工作模式.

  • 当epoll检测到socket上事件就绪的时候, 可以不立刻进行处理. 或者只处理一部分.
    例:只读了1K数据, 缓冲区中还剩1K数据, 在第二次调用 epoll_wait 时, epoll_wait仍然会立刻返回并通知socket读事件就绪.
  • 直到缓冲区上所有的数据都被处理完, epoll_wait 才不会立刻返回.
  • 支持阻塞读写和非阻塞读写

边缘触发Edge Triggered工作模式

如果我们在第1步将socket添加到epoll描述符的时候使用了EPOLLET标志, epoll进入ET工作模式.

  • 当epoll检测到socket上事件就绪时, 必须立刻处理
  • 如上面的例子, 虽然只读了1K的数据, 缓冲区还剩1K的数据, 在第二次调用 epoll_wait 的时候, epoll_wait 不会再返回了
  • 也就是说, ET模式下, 文件描述符上的事件就绪后, 只有一次处理机会
  • ET的性能比LT性能更高( epoll_wait 返回的次数少了很多). Nginx默认采用ET模式使用epoll.
  • 只支持非阻塞的读写

select和poll其实也是工作在LT模式下. epoll既可以支持LT, 也可以支持ET

在ET模式下必须要将fd设置位非阻塞

epoll的使用场景

epoll的高性能, 是有一定的特定场景的. 如果场景选择的不适宜, epoll的性能可能适得其反.对于多连接, 且多连接中只有一部分连接比较活跃时, 比较适合使用epoll.

简单的epoll LT服务器

#include "Sock.hpp"

#define SIZE 64


class bucket {
public:
    char buffer[20]; //request
    int pos;
    int fd;

    bucket(int sock) :fd(sock), pos(0)
    {
        memset(buffer, 0, sizeof(buffer));
    }
    ~bucket()
    {}
    
};

class EpollServer {
private:
    int lsock;
    int port;
    int epfd;
public:
    EpollServer(int _p = 8081) :port(_p)
    {}

    void AddEvent2Epoll(int sock, uint32_t event)
    {
        struct epoll_event ev;
        ev.events = event;
        if (sock == lsock) {
            ev.data.ptr = nullptr;
        }
        else {
            ev.data.ptr = new bucket(sock);
        }
        epoll_ctl(epfd, EPOLL_CTL_ADD, sock, &ev);
    }
    void DelEventFromEpoll(int sock)
    {
        close(sock);
        epoll_ctl(epfd, EPOLL_CTL_DEL, sock, nullptr);
    }
    void InitServer()
    {
        lsock = Sock::Socket();
        Sock::Setsockopt(lsock);
        Sock::Bind(lsock, port);
        Sock::Listen(lsock);

        epfd = epoll_create(256);
        if (epfd < 0) {
            cerr << "epoll_create error" << endl;
            exit(5);
        }
        cout << "listen sock: " << lsock << endl;
        cout << "epoll  fd:   " << epfd << endl;
    }
    void HandlerEvents(struct epoll_event revs[], int num)
    {
        for (int i = 0; i < num; i++) {
            uint32_t ev = revs[i].events;
            if (ev & EPOLLIN) {
                if (revs[i].data.ptr != nullptr) {
                    bucket* bp = (bucket*)revs[i].data.ptr;
                    ssize_t s = recv(bp->fd, bp->buffer + bp->pos, sizeof(bp->buffer) - bp->pos, 0);
                    if (s > 0) {
                        bp->pos += s;
                        cout << "client# " << bp->buffer << endl;
                        if (bp->pos >= sizeof(bp->buffer)) {
                            struct epoll_event temp;
                            temp.events = EPOLLOUT;
                            temp.data.ptr = bp;
                            epoll_ctl(epfd, EPOLL_CTL_MOD, bp->fd, &temp);
                        }
                    }
                    else if (s == 0)
                    {
                        DelEventFromEpoll(bp->fd);
                        delete bp;
                    }
                    else {
                       
                    }
                }
                else {
                    
                    int sock = Sock::Accept(lsock);
                    if (sock > 0) {
                        AddEvent2Epoll(sock, EPOLLIN);
                    }
                }
            }
            else if (ev & EPOLLOUT) {
               
                bucket* bp = (bucket*)revs[i].data.ptr;
               
                send(bp->fd, bp->buffer, sizeof(bp->buffer), 0);
                DelEventFromEpoll(bp->fd);
                delete bp;
            }
            else {
               
            }
        }
    }
    void Start()
    {
        AddEvent2Epoll(lsock, EPOLLIN);
        int timeout = -1;
        struct epoll_event revs[SIZE];
        for (;;) {
            int num = epoll_wait(epfd, revs, SIZE, timeout);
            switch (num) {
            case 0:
                cout << "time out ..." << endl;
                break;
            case -1:
                cerr << "epoll_wait error" << endl;
                break;
            default:
                HandlerEvents(revs, num);
                break;
            }
        }
    }
    ~EpollServer()
    {
        close(lsock);
        close(epfd);
    }
};

在这里插入图片描述

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。