epoll的使用

以下内容均为本人学习比价,若有错误,欢迎指出
在网络编程中,poll可能实际中用到的不是很多,所以作为了解性内容
上上篇为 IO多路转接之select:select
上一篇为 IO多路转接之poll:poll

多路转接之epol

epoll是在2.5.44内核中引进的,被公认为是最好的多路IO就绪通知方法

一、epoll的相关接口

创建一个文件句柄
 #include <sys/epoll.h>
 int epoll_create(int size);
  • 创建一个 epoll 对象,这里类似于创建管道,但是这里返回的是一个标识该软件资源的文件描述符。

  • 这里的参数 size 在linux2.6.8之后就忽略了,但是减一传参为256,128避免平台不同出错

  • 既然返回的是epoll句柄,用完之后记得关闭
epoll的事件注册函数
#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
  • epoll不同于select 是在监听事件是才告诉内核要监听哪些文件描述符,哪种类型的事件,而是在这里先调用注册函数注册要监听的文件描述符和事件类型。
  • 第一个参数为要epoll_create()返回的epoll句柄
  • 第二个参数为动作
    EPOLL_CTL_ADD: 注册新的 fd 到 opfd 中
    EPOLL_CTL_MOD: 修改已注册的 fd 的监听事件
    EPOLL_CTL_DEL: 从epfd中删除该fd

  • 第三参数为要监听的文件描述符

  • 第四个参数为一个结构体指针,这个结构体中的信息为告诉内核需要监听什么事件
    第四个参数的结构如下

struct epoll_event

 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 */
 };
  • events 为一个位图,每一位表示不同的事件,类似于 pollfd 里面的结构,用来表示监听事件集合
  • data 为联合体,用来保存用户自定制数据,传什么类型的数据,就对联合体里面的哪个数据进行赋值
  • 这里的data 在一般情况下用保存对应的文件描述符(后面会提到)
收集epoll监控事件中已经发送的事件
 #include <sys/epoll.h>
 int epoll_wait(int epfd, struct epoll_event *events,
                      int maxevents, int timeout);
  1. 第一个参数为我们创建的epoll模型
  2. 第二个参数为事件数组
  3. 第三个为数组大小
  4. 第四个参数为超时时间

二、epoll的工作原理

1.创建epoll模型
调用epoll_create()之后,内核会做3件事情
(1)在操作系统底层(硬件驱动,网卡)构建会调机制
(2)在操作系统层构建一颗红黑树(一种相对平衡的二叉搜索树),树的每个节点用来保存用户关心的事件(即用户关心的文件描述符和所关心的事件类型)
(3)在操作系统层构建一个就绪队列,保存众多事件中已经就绪的事件
2.用户控制事件
(1) 用户通过调用epoll_ctl()实现实现告诉操作系统,你现在要关心的文件描述符和关心的事件类型
(2)操作系统会将这一事件保存在红黑树中
3.内核激活事件
(1)操作系统得知网卡(文件)上面有数据就绪时(硬件机制),激活该事件,将其存入就绪队列中
(2)用户调用epoll_wait()返回时,返回的为就绪队列中就绪的事件
我们说epoll_wait()的实现是O(1)的时间复杂度,只需要关注就绪队列是否为空,不为空就将事件复制到用户态

实例代码

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

//基于epoll实现的http服务器
void Handle_events(int epfd,epoll_event events[],int maxevent,int listen_socket)
{
  if(events == NULL || maxevent <=0)
  {
    return;
  }

  //因为执行到这一步,说明一定有事件就绪
  //我们只需要关心是哪类事件就绪
  int i = 0;
  for(i = 0; i < maxevent; ++i)
  {
    char buf[1024 * 10] = {0};
    if(events[i].data.fd == listen_socket && (events[i].events & EPOLLIN))
    {
      //(a)说明listen_socket已经就绪  
      sockaddr_in peer;
      socklen_t peer_len = sizeof(peer);
      int acc_sock = accept(listen_socket,(sockaddr *)&peer,&peer_len); 
      if(acc_sock < 0)
      {
        perror("accept");
        continue;
      }
      else
      {
        //将已经建立连接的文件描述符加入到epfd中,并且设置为关心读事件
        //让epoll_wait()监听该文件的读事件就绪
        epoll_event event;
        event.data.fd = acc_sock;
        event.events = EPOLLIN;

        int ret = epoll_ctl(epfd,EPOLL_CTL_ADD,acc_sock,&event);
        if(ret < 0)
        {
          perror("epoll_ctl add");
          continue;
        }
      }
      continue;
    }

    else
    {
      //(b)先关心读事件
      if(events[i].events & EPOLLIN)
      {
        //这里只读取一次,但是这样其实是不安全的,因为一次并不能保证把缓冲区所有是数据都读走
        //有可能造成粘包问题
        ssize_t read_size =  read(events[i].data.fd,buf,sizeof(buf)-1);      
        if(read_size < 0)
        {
          perror("read");
          close(events[i].data.fd);
          epoll_ctl(epfd,EPOLL_CTL_DEL,events[i].data.fd,NULL);
          continue;
        }
        if(read_size == 0)
        {
          printf("client sidconnect!\n");
          close(events[i].data.fd);
          epoll_ctl(epfd,EPOLL_CTL_DEL,events[i].data.fd,NULL);
          continue;
        }
        buf[read_size] = '\0';
        printf("%s",buf);
        //将已经读完的文件描述符加入到epfd中,并且设置为关心写事件
        //让epoll_wait()监听该文件的写事件就绪
        epoll_event event;
        event.data.fd = events[i].data.fd;
        event.events = EPOLLOUT;
        epoll_ctl(epfd,EPOLL_CTL_ADD,events[i].data.fd,&event);
      }
      else
      {
        //(c)再关心写事件
        if(events[i].events & EPOLLOUT)
        {
          ///这里对应所有的响应都恢复一个html页面
          const char * recv = "HTTP/1.1 200 OK\r\n\r\n<html><h1>hello world</h1><html>"; 
          write(events[i].data.fd,recv,strlen(recv));
          //短连接,一次响应之后将连接断开
          close(events[i].data.fd);
          epoll_ctl(epfd,EPOLL_CTL_DEL,events[i].data.fd,NULL);
        }

      }

    }
  }
}


//启动服务器
int Server_Start(const char * ip,const short port)
{
  int sock = socket(AF_INET,SOCK_STREAM,0);

  if(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(sock,(sockaddr *)&addr,sizeof(addr));
  if(ret < 0)
  {
    perror("bind");
    return -1;
  }

  ret = listen(sock ,5);
  if(ret < 0)
  {
    perror("listen");
    return -1;
  }
  return sock;
}



//main()函数
int main(int argc,char * argv[])
{
  //判断命令行参数
  if(argc != 3)
  {
    printf("Usage :./server [ip] [port]\n");
    return 1;
  }

  //1.启动服务器
  int listen_socket = Server_Start(argv[1],atoi(argv[2]));
  if(listen_socket < 0)
  {
    printf("server start faild\n");
    return 2;
  }

  printf("server start ok\n");

  //2.开始事件循环

  //(a)构造epoll对象
  int epfd = epoll_create(256);
  if(epfd < 0)
  {
    perror("epoll_create");
    return 3;
  }

  //(b)注册事件
  epoll_event event;//定义events结构
  event.data.fd = listen_socket;//将用户要监听的文件描述符赋值
  event.events = EPOLLIN;//为该文件描述符只注册读事件
  int ret = epoll_ctl(epfd,EPOLL_CTL_ADD,listen_socket,&event);
  if(ret < 0)
  {
    perror("epoll_ctl add\n");
    return 4;
  }
  //(c)进行循环处理
  while(1)
  {
    //调用epoll_wait()开始监听
    epoll_event events[128];
    int size = epoll_wait(epfd,events,sizeof(events)/sizeof(events[0]),-1);//超时时间为1s监听

    switch(size)
    { 
      case -1:
        perror("epoll_wait");
        break;
      case 0:
        printf("time out\n");
        break;
      default://返回值大于0 的说明一定有事件就绪了
        Handle_events(epfd,events,sizeof(events)/sizeof(events[0]),listen_socket);
        break;
    }
      perror("epoll_wait");
      printf("time out\n");

  }//end while()

  close(listen_socket);
  close(epfd);

}//end main()

epoll特点总结

对比于我们之前学的select 和 poll
epoll的优点

  1. 文件描述符没有上限,这一点和poll类似,都是采用了数组来存储,而select 是受fd_set结构大小的限制
  2. 基于事件的就绪通知方式,操作系统通过硬件机制一旦监听的某个文件描述符就绪,会采用类似于callback回调机制,激活这个文件描述符,这样随着文件描述符数目的增多,也不会影响判定就绪的性能
  3. 内核维护就绪队列,当文件描述符就绪,操作系统会将这个事件放在就绪队列中,用于epoll_wait()返回,实现epoll_wait()O(1)的时间复杂度
  4. 内存映射机制,内核直接将就绪队列通过mmap的方式映射到用户态,避免了拷贝数据到内存(这个观点被很多人认可,但是我觉得存在疑点)

对上面的内存映射机制有以下疑点:

  1. 我们再使用这些接口的时候,并没有用到内存映射的相关接口,而是在调用epoll_wait()时,我们自己将开辟好的空间作为输出型参数,最后将内核数据拷贝到用户态
  2. 操作系统是不相信任何用户的,假如采用内存映射机制维护就绪队列的话,用户可以直接修改内核的数据,我们都知道,访问内核数据必须通过系统给我们提供的系统调用接口

完。

猜你喜欢

转载自blog.csdn.net/Misszhoudandan/article/details/81142530
今日推荐