欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

poll与epoll

程序员文章站 2022-03-14 12:13:37
...

poll


poll函数接口

#include <poll.h>

int poll(struct pollfd *fds,nfds_t nfds,int timeout);
//参数1 结构体指针(结构体数组的首地址)  
//参数2 数组长度
//参数3 poll函数的超时时间(同select)
//返回值小于0,出错,等于0,poll函数等待超时
//大于0,表示poll由于监听的文件描述符就绪返回。

struct pollfd{
    int fd;//文件描述符
    short events;//输入参数 监听的事件集合(常用事件 POLLIN POLLOUT)
    short revents;//输出参数 返回的事件集合
}; 


poll的优点(同select比较)

不同与select使⽤三个位图来表⽰三个fdset的⽅式,poll使⽤⼀个pollfd的指针实现

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

poll的缺点

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

//使用epoll监控标准输入

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

int main(){

    struct pollfd fds;
    fds.fd = 0;//0标准输入
    fds.events = POLLIN;

    while(1){
        int ret = poll(&fds,1,-1);//-1 永久阻塞
        if(ret<0){
            perror("poll");
            return 1;
        }
        char buf[1024] = {0};
        ssize_t read_size = read(0,buf,sizeof(buf)-1);
        if(read_size < 0){
            perror("read");
            return 1;
        }
        if(read_size == 0){
            printf("read done\n");
            return 0;
        }
        buf[read_size] = '\0';

        printf("rep = %s\n",buf);
    }

    return 0;
}

epoll


epoll的相关函数


创建一个epoll的句柄(验证其为文件描述符的方式:连接服务器时,第一个client为5,不是4),既然是文件描述符,用完要关闭。

int epoll_create(int size);
//size为任意值,无意义

epoll的事件注册函数

int epoll_ctl (int epfd,int op,int fd,struct epoll_event *event);
//参数1 epoll_create() 的返回值(句柄)
//参数2 表示处理事件的方式(3种)
//参数3 需要监听的文件描述符
//参数4结构体指针(操作方式)

参数2的三种方法

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

    struct epoll_event
    poll与epoll

epoll_event结构体里边有两个参数,一个events(相当于位图)其常用操作有EPOLLIN,EPOLLOUT。第二个参数为一个联合,其能更好的操作一个未知甚至更复杂的结构(含有一个void*的指针)。

events常见操作:

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


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

//参数1 epoll句柄
//参数2 结构体数组首地址(同epoll_ctl的 *event)
//参数3 数组的大小
//参数4 超时时间

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


epoll的使用场景

对于多连接, 且多连接中只有⼀部分连接⽐较活跃时, ⽐较适合使⽤epoll


epoll的工作原理(底层红黑树存储,就绪存放于链表)

  • 当某⼀进程调⽤epoll_create⽅法时,Linux内核会创建⼀个eventpoll结构体,这个结构体中有两个成员与epoll的使⽤⽅式密切相关
  • 每⼀个epoll对象都有⼀个独⽴的eventpoll结构体,⽤于存放通过epoll_ctl⽅法向epoll对象中添加进来的事件.
  • 这些事件都会挂载在红⿊树中,如此,重复添加的事件就可以通过红⿊树⽽⾼效的识别出来(红⿊树的插⼊时间效率是lgn,其中n为树的⾼度).
  • ⽽所有添加到epoll中的事件都会与设备(网卡)驱动程序建⽴回调关系,也就是说,当响应的事件发⽣时会调⽤这个回调⽅法.
  • 这个回调⽅法在内核中叫eppollcallback,它会将发⽣的事件添加到rdlist双链表中.
    在epoll中,对于每⼀个事件,都会建⽴⼀个epitem结构体

  • 当调⽤epoll_wait检查是否有事件发⽣时,只需要检查eventpoll对象中的rdlist双链表中是否有epitem元素即可.

  • 如果rdlist不为空,则把发⽣的事件复制到⽤户态,同时将事件数量返回给⽤户. 这个操作的时间复杂度是O(1)

epoll的工作方式

  • 假如有一个文件描述符就绪,携带1K的数据,epoll_wait返回,假设开始读数据,一次只能读0.5K的数据,那么缓冲区还留下0.5K,那么此时epoll_wait立刻再次返回,告诉服务器该文件描述符就绪,让其读完剩余的0.5K数据,这样的操作为水平触发。
  • 边缘触发则是如上例如果1K的字节只读了0.5K,epoll_wait不会立刻返回,而是等到下次这个文件就绪后,返回,此时,服务器才有可能读取上次的缓存。故ET模式强制我们将缓冲区的数据一次性全部读取。所以只能是非阻塞读写。

水平触发(LT)

  • 支持阻塞和非阻塞读写

边缘触发(ET)(效率高)

  • 只支持非阻塞读写(ET(边缘触发)数据就绪只会通知⼀次,也就是说,如果要使⽤ET模式,当数据就绪时,需要⼀直read,直到出错或完成为⽌.但倘若当前fd为阻塞(默认),那么在当读完缓冲区的数据时,如果对端并没有关闭写端,那么该read函数会⼀直阻塞。)(想象僵持状态)

epoll的优点(同select比较)

  • ⽂件描述符数目⽆上限: 通过epoll_ctl()来注册⼀个⽂件描述符, 内核中使⽤红⿊树的数据结构来管理所有需要监控的⽂件描述符.
  • 基于事件的就绪通知⽅式: ⼀旦被监听的某个⽂件描述符就绪, 内核会采⽤类似于callback的回调机制, 迅速**这个⽂件描述符. 这样随着⽂件描述符数量的增加, 也不会影响判定就绪的性能;
  • 维护就绪队列: 当⽂件描述符就绪, 就会被放到内核中的⼀个就绪队列中. 这样调⽤epoll_wait获取就绪⽂件描述符的时候, 只要取队列中的元素即可, 操作的时间复杂度是O(1);
  • 从接口使用的角度上讲:不必每次都重新设置要监控的文件描述符,使接口使用方便,也能够避免用户态和内核态之间,来回的拷贝文件描述符结构。

LT

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/epoll.h>

typedef struct sockaddr sockaddr;
typedef struct sockaddr_in sockaddr_in;
typedef struct epoll_event epoll_event;


void ProcessListenScok(int epoll_fd, int listen_sock){
    sockaddr_in peer;
    socklen_t len;
    int new_sock = accept(listen_sock,(sockaddr*)&peer,&len);
    if(new_sock <0 ){
        perror("accept");
        return;
    }
    //把 new_sock 加入到epoll之中

    epoll_event event;
    event.events = EPOLLIN;
    event.data.fd = new_sock;
    int ret = epoll_ctl(epoll_fd,EPOLL_CTL_ADD,new_sock,&event);
    if(ret < 0){
        perror("epoll_ctl ADD");
        return;
    }
    printf("[client %d] connected\n",new_sock);
    return;
}

int ServerInit(const char* ip,short port){
    int listen_sock = socket(AF_INET,SOCK_STREAM,0);
    if(listen_sock < 0){
        perror("socket");
        return -1;
    }
    sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = inet_addr(ip);
    addr.sin_port = htons(port);
    int ret = bind(listen_sock,(sockaddr*)&addr,sizeof(addr));
    if(ret < 0 ){
        perror("bind");
        return -1;
    }
    ret = listen(listen_sock,10);
    if(ret < 0){
        perror("listen");
        return -1;
    }
    return listen_sock;
}


void ProcessNewSock(int epoll_fd,int new_sock){
    char buf[1024] = {0};
    ssize_t read_size = read(new_sock,buf,sizeof(buf)-1);
    if(read_size < 0){
        perror("read");
        return;
    }
    //读到返回值为0,对端关闭了文件描述符。
    //本端也应该关闭文件描述符,并且把文件描述符从epoll之中删除掉
    if(read_size == 0){
        close(new_sock);
        epoll_ctl(epoll_fd,EPOLL_CTL_DEL,new_sock,NULL);
        printf("[client %d] disconnected\n",new_sock);
        return;
    }
    buf[read_size] = '\0';
    printf("[client %d]say:%s\n",new_sock,buf);
    write(new_sock,buf,strlen(buf));
    return;
}



int main(int argc,char* argv[]){
    if(argc != 3){
        printf("Usage: ./server_epoll  [ip] [port]\n");
        return 1;
    }
    int listen_sock = ServerInit(argv[1],atoi(argv[2]));
    if( listen_sock < 0){
        perror("ServerInit");
        return 1;
    }

    //创建并初始化epoll;
    int epoll_fd = epoll_create(9);
    if(epoll_fd < 0){
        perror("epoll_create");
        return 1;
    }
    //把listen_sock放置到epoll
    epoll_event event;
    event.events = EPOLLIN;
    event.data.fd = listen_sock;
    int ret = epoll_ctl(epoll_fd,EPOLL_CTL_ADD,listen_sock,&event);
    if(ret < 0){
        perror("epoll_ctl");
        return -1;
    }
    printf("ServerInit OK!\n");

    while(1){
        epoll_event output_event[100];
        int nfds = epoll_wait(epoll_fd,output_event,100,-1);
        if(nfds < 0){
            perror("epoll_wait");
            continue;
        }
        //output_event[]里边保存着准备好的文件描述符
        int i = 0;
        for(;i < nfds;++i ){
            if(listen_sock == output_event[i].data.fd){//读
                ProcessListenScok(epoll_fd,listen_sock);
            }
            else{
                ProcessNewSock(epoll_fd,output_event[i].data.fd);//增加
            }
        }//for
    }//while


    return 0;
}

ET

相关标签: epoll

上一篇: epoll笔记

下一篇: nacos安装