EPOLL入门留念

  • 使用方法:

好像这个排版和Typora的有点区别,还在调。。。抱歉抱歉

  1. 文件描述符的创建

    
    #include 
    
    int epoll_create ( int size );
  2. 注册监听事件

    int epoll_ctl ( int epfd, int op, int fd, struct epoll_event *event );

    其中,epfd是上面创建文件描述符的返回值,op是要指定的操作类型,fd是要监听的文件描述符,event是struct epoll_event类型的事件

    操作类型:

    ​ EPOLL_CTL_ADD:往事件表中注册fd上的事件

    ​ EPOLL_CTL_MOD:修改fd上的注册事件

    ​ EPOLL_CTL_DEL:删除fd上的注册事件

    event事件的定义

    struct epoll_event
    {
       __unit32_t events;    // epoll事件
       epoll_data_t data;     // 用户数据 
    };

    事件主要有

    ​ EPOLLIN 监听是否可以读进来

    ​ EPOLLOUT 监听是否可以写出去

    ​ EPOLLET ET和LT是两种模式,缺省是LT,后面讲

  3. 收集

    int epoll_wait ( int epfd, struct epoll_event* events, int maxevents, int timeout );
    函数成功时返回就绪的文件描述符个数,失败时返回-1并设置errno

    其中,epfd是创建的epoll的文件描述符,events是将所有就绪的事件从内核事件表中复制到这里,maxevents是指定监听的事件数,timeout指等待多长时间,-1表示无限等待。

想简单看下用法的同学可以看到这就结束了,下面将简单介绍是什么以及为什么,有兴趣的也可以看看最后面的例子


  • 首先 什么是epoll

    epoll是用来实现高并发的一种手段。

  • 为什么要用epoll

    epoll与select和poll相比之下有很大优势。先说说select的缺点:

    1. 单进程能够监视的文件描述符有最大限制,通常是1024,在Linux内核头文件中有定义。

    2. select采取的是轮询的方式来探查是否有文件描述符准备好,当并发量非常大时,这种做法会非常耗时间。

    3. select要复制大量的数据结构,导致产生巨大的开销。

  • epoll快的原因

    mmap、红黑树、链表

    1. mmp

      epoll是通过内核与用户空间mmap同一块内存实现的。mmap将用户空间的一块地址和内核空间的一块地址同时映射到相同的一块物理内存地址(不管是用户空间还是内核空间都是虚拟地址,最终要通过地址映射映射到物理地址),使得这块物理内存对内核和对用户均可见,减少用户态和内核态之间的数据交换。内核可以直接看到epoll监听的句柄,效率高。

    2. 红黑树

      红黑树将存储epoll所监听的套接字。上面mmap出来的内存如何保存epoll所监听的套接字,必然也得有一套数据结构,epoll在实现上采用红黑树去存储所有套接字,当添加或者删除一个套接字时(epoll_ctl),都在红黑树上去处理,红黑树本身插入和删除性能比较好,时间复杂度O(logN)。

    3. 链表

      通过epoll_ctl函数添加进来的事件都会被放在红黑树的某个节点内,所以,重复添加是没有用的。当把事件添加进来的时候时候会完成关键的一步,那就是该事件都会与相应的设备(网卡)驱动程序建立回调关系,当相应的事件发生后,就会调用这个回调函数,该回调函数在内核中被称为:ep_poll_callback,这个回调函数其实就所把这个事件添加到rdllist这个双向链表中。一旦有事件发生,epoll就会将该事件添加到双向链表中。那么当我们调用epoll_wait时,epoll_wait只需要检查rdlist双向链表中是否有存在注册的事件,效率非常可观。这里也需要将发生了的事件复制到用户态内存中即可。

  • epoll的流程

    我简单说下epoll的流程吧

    1. epoll_wait函数调用ep_poll,当rdlist为空的时候挂起当前进程,直到rdlist非空时唤醒。

    2. 当文件fd改变的时候,调用相应fd上的回调函数ep_poll_callback

    3. 这个回调函数将fd对应的epitem加入rdlist,这将导致ep_wait进程被唤醒
    4. ep_transfer将rdlist中的epitem拷贝到txlist中,并清空rdlist
    5. ep_send_events将扫描txlist中的每个epitem,调用与之相关联的poll方法,取得events,然后将events和fd一起发送到用户空间,如果是LT模式就重新加入rdlist,如果是ET模式就不再加入,除非fd再次发生变化而且是我们关心的事件

​ 当使用ep_create创建一个描述符的时候Linux内核会创建一个eventpoll结构体,这里面的内容与epoll的使用息息相关

struct eventpoll{
    ....
    /*红黑树的根节点,这颗树中存储着所有添加到epoll中的需要监控的事件*/
    struct rb_root  rbr;
    /*双链表中则存放着将要通过epoll_wait返回给用户的满足条件的事件*/
    struct list_head rdlist;
    ....
};

对于epoll中,每个事件都对应着一个epitem结构体

struct epitem{
    struct rb_node  rbn;//红黑树节点
    struct list_head    rdllink;//双向链表节点
    struct epoll_filefd  ffd;  //事件句柄信息
    struct eventpoll *ep;    //指向其所属的eventpoll对象
    struct epoll_event event; //期待发生的事件类型
}

当调用epoll_wait检查是否有事件发生时,只需要检查eventpoll对象中的rdlist双链表中是否有epitem元素即可。如果rdlist不为空,则把发生的事件复制到用户态,同时将事件数量返回给用户。 十分的简单。

  • LT与ET模式
    这边我简单说一下自己的理解吧。ET模式称为边缘触发,意思是只有当检测的fd的读写状态发生变化时才返回。也就是说,如果来了2K的数据,你只读了1K,那么下一次也不会返回,仍然有1K的数据留在缓冲区中没读出来。LT称为水平触发,只要可读或者可写就返回,也就是说上面的1K数据也是有返回的。缺省状态是LT。

推荐这个博客 http://blog.chinaunix.net/uid-28541347-id-4273856.html


  • 例子

看到网上的例子都很复杂,因此特意写了一个简单的供大家参考epoll用法

服务器端

#include 
#include 
#include 
#include 
#include 
#include 

#define PORT 666
#define SA struct sockaddr
#define LISTENQ 1024
#define EPOLL_SIZE 128
#define MAXLINE 10240

//服务器端
int main(int argc, char const *argv[]) {
    struct sockaddr_in servaddr;
    struct epoll_event ev, events[EPOLL_SIZE];
    char buf[MAXLINE];
    int epfd, n, epwaitn;
    int connfd;
    int listenfd = socket(AF_INET, SOCK_STREAM, 0);

    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(PORT);
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);

    bind(listenfd, (SA*)&servaddr, sizeof(servaddr));
    listen(listenfd, LISTENQ);

    //--------------------------------------
    //到此为止都是简单的网络配置,这里不细说

    //这里设置监听,要监听listenfd这个文件描述符
    ev.data.fd = listenfd;
    //监听类型是是否可以读入
    ev.events = EPOLLIN;

    //创建epoll
    epfd = epoll_create(EPOLL_SIZE);
    //把刚刚设置的监听注册到新建的epoll当中去
    epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev);

    for(;;){
        //调用epoll_wait查看有几个描述符就绪
        epwaitn = epoll_wait(epfd, events, EPOLL_SIZE, -1);

        //遍历这些就绪的描述符
        for(int i=0; i//添加新监听接口进来
            if(events[i].data.fd == listenfd){
                //添加新监听接口进来
                connfd = accept(listenfd, NULL, NULL);
                struct epoll_event event;
                event.data.fd = connfd;
                event.events = EPOLLIN;
                epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &event);
            }
            else if(events[i].events & EPOLLIN){
                //有描述符准备好可以进行读取操作了
                int sockfd = events[i].data.fd;
                if( (n = read(sockfd, buf, MAXLINE)) < 0){
                    printf("read have a error\n");
                }else if(n == 0){
                    printf("n == 0\n");
                    close(sockfd);
                    break;
                }
                else{
                    //更改检测状态,由检测可读状态变为检测可写状态,准备输出
                    struct epoll_event event;
                    event.data.fd = events[i].data.fd;
                    event.events = EPOLLOUT | EPOLLET;
                    epoll_ctl(epfd, EPOLL_CTL_MOD, events[i].data.fd, &event);
                }
            }
            else if(events[i].events & EPOLLOUT){
                //原理同上
                if(write(events[i].data.fd, buf, n) < 0)
                    printf("write error\n");

                struct epoll_event event;
                event.data.fd = events[i].data.fd;
                event.events = EPOLLIN;
                epoll_ctl(epfd, EPOLL_CTL_MOD, events[i].data.fd, &event);
            }
        }
    }

    return 0;
}

客户端:

此代码仅仅为了测试epoll的使用,所以只写了个简单的

#include 
#include 
#include 
#include 
#include 

#define SERV_PORT 666
#define SA struct sockaddr
#define MAXLINE 1024

int main(int argc, char const *argv[]) {
  int listenfd;
  struct sockaddr_in servaddr;
  int n;
  char buf[MAXLINE], recv[MAXLINE];

  listenfd = socket(AF_INET, SOCK_STREAM, 0);
  bzero(&servaddr, sizeof(servaddr));
  servaddr.sin_family = AF_INET;
  servaddr.sin_port = htons(SERV_PORT);
  inet_pton(AF_INET, argv[1], &servaddr.sin_addr);

  connect(listenfd, (SA*)&servaddr, sizeof(servaddr));

  for(;;){
    while( (n = read(fileno(stdin), buf, MAXLINE)) > 0){
        write(listenfd, buf, n);
    if(n == 0)
      break;

    if( (n = read(listenfd, &recv, MAXLINE)) > 0)
      write(fileno(stdout), &recv, n);
    }
  }

  return 0;
}
  • 测试截图

    EPOLL入门留念_第1张图片

    ​ 呃。。。忽略最后不小心按到的^X

  • 鸣谢

    https://www.cnblogs.com/lojunren/p/3856290.html

    https://blog.csdn.net/asap_diablo/article/details/76922090

    https://blog.csdn.net/shenya1314/article/details/73691088

你可能感兴趣的:(Unix网络编程(第三版))