IO复用(epoll)

  在前面的文章中讲了实现IO复用的两种方式:selectpoll。今天主要讲一个更为高效的函数epoll。

epoll

  epoll能显著提高在大量链接中,只有少量活跃连接时的cpu利用率。因为,首先epoll可以复用监听的文件描述符集合,而不用每次在等待事件之前重新准备被监听的文件描述符集合。其次是因为epoll获取就绪事件时,不用遍历整个监听事件的集合,而是只需要遍历那些被内核IO一步唤醒的放入ready队列的文件描述符集合。
Epoll的主要API

#include <sys/epoll.h>

int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

①、int epoll_create(int size);
  用来创建一个epoll句柄,底层实现也就是生成一个红黑树的树根。它的参数只有一个size,设置监听文件描述符的个数,是个建议值,epoll后期监听的文件描述符上限和size无关。需要说明的是:当创建好epoll句柄后,它就是会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。
  
②、int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
  控制某个epoll监听文件描述符上的事件

第一个参数是epoll_create()创建的epoll句柄,
第二个参数是op操作,有三种:
  EPOLL_CTL_ADD,添加一个新的fd到到红黑树上,
  EPOLL_CTL_MOD,修改对应fd的监听事件,
  EPOLL_CTL_DEL,删除一个fd,也就是将其从红黑树上摘下来。
第三个参数是监听的文件描述符fd,  
第四个参数是epoll_event结构体指针,这个结构体里面有两个成员:

struct epoll_event {
      __uint32_t events;  /* Epoll events */
      epoll_data_t data;  /* User data variable */
  };

  第一个是event,监听的文件描述符的事件,一般使用的事件都有一下几个:

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

  第二个data也是一个epoll_data_t联合体,定义如下:
  

typedef union epoll_data {
            void *ptr;
            int fd;
            uint32_t u32;
            uint64_t u64;
        } epoll_data_t;

  在这个结构体中最常用的就是两个:void *ptr 和 int fd。因为这是一个联合体。同时只能使用其中的一个成员。一般情况下,我们直接使用fd这个成员,传入监听的文件描述符,和epoll_ctl函数的第三个参数保持一致。当我们想要进一步提高epoll的性能,可以使用void *ptr这个泛型指针。注册回调函数,当监听的事件满足时,直接调用该回调函数去执行相应的逻辑。

③、int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
  Epoll_wait函数是等待监听的事件就绪,类似于select函数和epoll函数。
第一个参数是epoll句柄,
第二个参数是epoll_event结构体类型的数组,
第三个参数是这个数组的大小,
第四个参数是设置超时,timeout。

ET模式和LT模式

  epoll除了提供select/poll那种IO事件的电平触发(Level Triggered)外,还提供了边沿触发(Edge Triggered),这就使得用户空间程序有可能缓存IO状态,减少epoll_wait的调用,提高应用程序效率。

LT模式

  LT模式,也就是水平触发模式,是epoll的默认工作方式,相当于比较快一点的poll。在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的,即在这种模式下,只要缓冲区有数据就会触发epoll_wait()函数。。可以设置为阻塞版本,也可以设置为非阻塞版本。

ET模式

  ET模式,边沿触发,是一种高效的工作模式,在这种模式下,当文件描述符变为就绪状态后,内核通过epoll通知,便不会在通知,及时缓冲区里面还有数据也不会再通知,在这种模式下只能使用非阻塞版本,是为了避免当一个文件句柄阻塞读或写操作时,把处理多个文件描述符的任务饿死。
  只有当read或wirte函数返回EAGIAN错误码时,才需要挂起等待,但并不是说每次都需要循环读,直到读到EAGIN才结束,当我们读到的字节数小于缓冲区大小是时,就可以认为读事件处理完成。使用epoll ET模式,可以减少epoll_wait()函数的调用次数,提高效率。

示例代码

  以一个epoll的服务器程序结束本文。

#include<stdio.h>
#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
#include<string.h>
#include<sys/types.h>
#include<netinet/in.h>
#include<sys/socket.h>
#include<sys/wait.h>
#include<arpa/inet.h>
#include<unistd.h>
#include<ctype.h>
#include<sys/epoll.h>
#include<fcntl.h>


#define   MYPORT  8888
#define  BACKLOG  10
#define MAXDATASIZE 1024
#define FILEMAX 3000 
#define size 20 //监听的事件数

int main()
{
    int i,j,maxi;
    int listenfd,connfd,sockfd; //定义套接字描述符
    int nready; //接受epool_wait返回值
    int numbytes; //接受recv返回值

    char buf[MAXDATASIZE]; //发送缓冲区
    struct epoll_event evt; //注册监听事件
    struct epoll_event ep[size]; //满足事件


    //定义IPV4套接口地址结构
    struct sockaddr_in seraddr;        //service 地址

    struct sockaddr_in cliaddr;     //client 地址
    int sin_size;

    //初始化IPV4套接口地址结构

    seraddr.sin_family =AF_INET; //指定该地址家族
    seraddr.sin_port =htons(MYPORT);  //端口
    seraddr.sin_addr.s_addr = INADDR_ANY;  //IPV4的地址
    bzero(&(seraddr.sin_zero),8);

    //socket()函数
    if((listenfd = socket(AF_INET,SOCK_STREAM,0))==-1)
    {
        perror("socket");
        exit(1);
    }

    //地址重复利用
    int on = 1;
    if(setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on)) < 0)
    {
        perror("setsockopt");
        exit(1);
    }

    //bind()函数
    if(bind(listenfd,(struct sockaddr *)&seraddr,sizeof(struct sockaddr))==-1)
    {
        perror("bind");
        exit(1);
    }

    //listen()函数
    if(listen(listenfd,BACKLOG)==-1)
    {
        perror("listen");
        exit(1);
    }

    int epfd = epoll_create(size); //创建句柄
    if(epfd == -1)
    {
        perror("epoll_create errror!\n");
        exit(1);
    }

    evt.events = EPOLLIN ;
    evt.data.fd = listenfd;

    //注册监听事件listenfd到epfd
    int ret = epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&evt);
    if(ret == -1)
    {
        perror("epoll_ctl error!\n");
        exit(1);
    }

    while(1)
    {
        nready = epoll_wait(epfd,ep,size,-1);  //监听事件是否就绪
        if(nready < 0)
        {
            perror("epoll_wait error!\n");
            exit(1);
        }

        for(i = 0;i < nready;i++)
        {
            if(!(ep[i].events & EPOLLIN))
            {
                continue;
            }
            else if(ep[i].data.fd == listenfd) //listenfd就绪,客户端发起连接
            {
                sin_size = sizeof(cliaddr);
                if((connfd=accept(listenfd,(struct sockaddr *)&cliaddr,&sin_size))==-1)
                {
                    perror("accept");
                    exit(1);
                }
                printf("client IP: %s\t PORT : %d\n",inet_ntoa(cliaddr.sin_addr),ntohs(cliaddr.sin_port));

                //修改connfd为非阻塞读
                evt.events = EPOLLIN | EPOLLET; //ET模式

                int flag = fcntl(connfd, F_GETFL);
                flag |= O_NONBLOCK;
                fcntl(connfd, F_SETFL, flag);

                evt.data.fd = connfd;
                int ret = epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&evt);
                if(ret == -1)
                {
                    perror("epoll_ctl error!\n");
                    exit(1);
                }
            }
            else
            {
                sockfd = ep[i].data.fd;
                memset(buf,0,sizeof(buf));

               // sockfd设置为非阻塞模式,数据还没有发给接收端时,调用recv就会返回-1,并且errno会被设为EAGAIN.
                numbytes = recv(sockfd,buf,1024,0)
                if(numbytes == -1 && EAGAIN != errno)
                {
                    perror("recv error!\n");
                    exit(1);
                }
                if(numbytes == 0)//客户端断开连接
                {
                    printf("client[%d],close!\n",i);
                    int ret = epoll_ctl(epfd,EPOLL_CTL_DEL,sockfd,NULL);
                    if(ret == -1)
                    {
                        perror("epoll_ctl error!\n");
                        exit(1);
                    }

                    close(sockfd);
                }
                if(numbytes > 0 )
                {
                    send(sockfd,buf,numbytes,0);
                    numbytes = recv(sockfd,buf,1024,0);
                }
            }
        }
    }

    close(listenfd);
    close(epfd);


    return 0;
}

猜你喜欢

转载自blog.csdn.net/z_ryan/article/details/80890118
今日推荐