Linux下的I/O复用技术 — epoll如何使用(epoll_create、epoll_ctl、epoll_wait) 以及 LT/ET 使用过程解析

epoll如何使用?

  • epoll如何使用?
    • 0、为什么选择epoll
    • 1、epoll的使用过程
      • 1.0、相关基础概念(便于理解事件触发过程)
      • 1.1、epoll_create:文件描述符的创建
      • 1.2、epoll_ctl:注册监控事件
      • 1.3、epoll_wait:事件等待,返回就绪事件
    • 2、LT/ET 使用过程
      • 2.1 LT水平触发(Level Triggered)
      • 2.2 ET边沿触发(Edge Triggered )
    • 3、使用案例
    • 4、常问面试问题
  • 参考

epoll如何使用?

0、为什么选择epoll

请参考上一篇博客:

epoll为什么更高效 ? epoll涉及的数据结构?

1、epoll的使用过程

Linux下的I/O复用技术 — epoll如何使用(epoll_create、epoll_ctl、epoll_wait) 以及 LT/ET 使用过程解析_第1张图片

1.0、相关基础概念(便于理解事件触发过程)

1、事件

  • 可读事件,当文件描述符关联的内核读缓冲区可读,则触发可读事件。
    (可读:内核缓冲区非空,有数据可以读取)

  • 可写事件,当文件描述符关联的内核写缓冲区可写,则触发可写事件。
    (可写:内核缓冲区不满,有空闲空间可以写入)

2、通知机制
通知机制,就是当事件发生的时候,则主动通知。通知机制的反面,就是轮询机制。

3、socket 和 文件描述符之间的关系

套接字也是文件。具体数据传输流程如下:

  • server端监听到有连接时,应用程序会请求内核创建Socket;

  • Socket创建好后会返回一个文件描述符给应用程序;

  • 当有数据包过来网卡时,内核会通过数据包的源端口,源ip,目的端口等在内核维护的一个ipcb双向链表中找到对应的Socket,并将数据包赋值到该Socket的缓冲区

  • 应用程序请求读取Socket中的数据时,内核就会将数据拷贝到应用程序的内存空间,从而完成读取Socket数据

注意:
操作系统针对不同的传输方式(TCP,UDP)会在内核中各自维护一个Socket双向链表,当数据包到达网卡时,会根据数据包的源端口,源ip,目的端口从对应的链表中找到其对应的Socket,并会将数据拷贝到Socket的缓冲区,等待应用程序读取。

想了解文件描述符(fd)是什么请参考此链接:
文件描述符(file descriptor)是什么?socket 和 文件描述符之间的关系?

4、服务器文件传输过程:server(服务器) ——》 client(客户)
Linux下的I/O复用技术 — epoll如何使用(epoll_create、epoll_ctl、epoll_wait) 以及 LT/ET 使用过程解析_第2张图片
当然:服务器接收客户的请求,红色箭头方向相反。

5、内核接收网络数据全过程

  • 1、计算机收到了对端传送的数据(步骤①);

  • 2、数据经由网卡传送到内存(步骤②);

  • 3、然后网卡通过中断信号通知cpu有数据到达,cpu执行中断程序(步骤③)。此处的中断程序主要有两项功能;

    • 先将网络数据写入到对应socket的接收缓冲区里面(步骤④);
    • 再唤醒进程A(步骤⑤),重新将进程A放入工作队列中。

Linux下的I/O复用技术 — epoll如何使用(epoll_create、epoll_ctl、epoll_wait) 以及 LT/ET 使用过程解析_第3张图片

注:
蓝色区域里面的等待队列:就是用户空间进程调用recv函数(读取数据)请求读取内核缓冲区内的数据,由于缓冲区数据没有准备好,所以处于等待状态(又称为阻塞状态)。

你有这样的疑问吗?操作系统如何知道网络数据对应于哪个socket?

因为一个socket对应着一个端口号,而网络数据包中包含了ip和端口的信息,内核可以通过端口号找到对应的socket。当然,为了提高处理速度,操作系统会维护端口号到socket的索引结构,以快速读取。

如何同时监视多个socket的数据?

哈哈哈,下面就该引出epoll了。epoll是一种I/O事件通知机制,是linux 内核实现IO多路复用的一个实现,在一个操作里同时监听多个输入输出源,在其中一个或多个输入输出源可用的时候返回,然后对其的进行读写操作。

1.1、epoll_create:文件描述符的创建

epoll_create的作用

  • epoll需要使用一个额外的文件描述符,来唯一标识内核中的这个事件表。 这个文件描述符使用如下epoll_create函数来创建;
  • epoll把用户关心的文件描述符上的事件放在内核里的一个事件表中,从而无须像select和poll那样每次调用都要重复传入文件描述符集或事件集。
  • 调用epoll_create时,内核除了帮我们在epoll文件系统里建了个file结点(epoll_create创建的文件描述符),在内核cache里建了个 红黑树用于存储以后epoll_ctl传来的socket外,还会再建立一个list链表,用于存储准备就绪的事件.(概括就是:调用epoll_create方法时,内核会跟着创建一个eventpoll对象)
    Linux下的I/O复用技术 — epoll如何使用(epoll_create、epoll_ctl、epoll_wait) 以及 LT/ET 使用过程解析_第4张图片
  • eventpoll对象也是文件系统中的一员,和socket一样,它也会有等待队列。
  • 创建一个代表该epolleventpoll对象是必须的,因为内核要维护“就绪列表”等数据,“就绪列表”可以作为eventpoll的成员。
struct eventpoll
{
    spin_lock_t lock;            //对本数据结构的访问
    struct mutex mtx;            //防止使用时被删除
    wait_queue_head_t wq;        //sys_epoll_wait() 使用的等待队列
    wait_queue_head_t poll_wait; //file->poll()使用的等待队列
    struct list_head rdllist;    //事件满足条件的链表
    struct rb_root rbr;          //用于管理所有fd的红黑树
    struct epitem *ovflist;      //将事件到达的fd进行链接起来发送至用户空间
}

总结:
epoll_create创建额外的文件描述符,来唯一标识内核中的这个内核事件表(eventpoll对象)

epoll_create函数原型

该函数返回的文件描述符将用作其他所有epoll系统调用的第一个参数,以指定要访问的内核事件表。

#include
int epoll_create(int size);

返回值:

  • 返回文件描述符epollfd

参数说明:

  • size参数现在并不起作用,只是给内核一个提示,告诉内核应该如何为内部数据结构划分初始大小。

需要注意的是:

这个文件描述符也会占用一个fd值,在linux下如果查看/proc/进程id/fd/,能够看到这个fd,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽

1.2、epoll_ctl:注册监控事件

epoll_ctl的作用

创建epoll对象后,可以用epoll_ctl添加或删除所要监听的socket。以添加socket为例,如下图,如果通过epoll_ctl添加sock1、sock2和sock3的监视,内核会将eventpoll添加到这三个socket的等待队列中。
Linux下的I/O复用技术 — epoll如何使用(epoll_create、epoll_ctl、epoll_wait) 以及 LT/ET 使用过程解析_第5张图片
注意:

当socket收到数据后,中断程序会操作eventpoll对象,而不是直接操作进程。

epoll_ctl函数原型

操作epoll的内核事件表

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

返回值:

  • 成功返回0,不成功返回-1并设置errno。

参数说明:

  • 1、epfd :epoll_create()的返回值;
  • 2、op : 指定操作类型。操作类型有如下3种:
    • EPOLL_CTL_ADD:注册新的fd到epfd中;
    • EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
    • EPOLL_CTL_DEL:从epfd中删除一个fd;
  • 3、fd:是需要监听的fd
  • 4、event:告诉内核需要监听什么事,指定事件,它是epoll_event结构指针类型。
    • epoll_event的定义如下:
struct epoll_event {
  __uint32_t events;  /* Epoll 事件 */
  epoll_data_t data;  /* 用户数据 */
};

其中events成员描述事件类型,具体如下:

事件 描述
EPOLLIN 可读取非高优先级数据(重要,必用)
EPOLLPRI 可读取高优先级数据
EPOLLOUT 普通数据可写 (重要,必用)
EPOLLHUP 本端描述符产生一个挂断事件,默认监测事件
EPOLLET 采用边沿触发事件通知(重要)
EPOLLONESHOT 在完成事件通知后禁用检查
EPOLLRDHUP 套接字对端关闭
EPOLLERR 有错误发生

data成员用于存储用户数据,其类型epoll_data_t的定义如下:

typedef union epoll_data
{
    void* ptr;              //指定与fd相关的用户数据
    int fd;                 //指定事件所从属的目标文件描述符
    uint32_t u32;
    uint64_t u64;
} epoll_data_t;

epoll_data_t是一个共用体,其4个成员中使用最多的是fd,它指定事件所从属的目标文件描述符。ptr成员可以用来指定与fd相关的用户数据。但由于epoll_data_t是一个共用体,我们不能同时使用其ptr成员和fd成员,因此,如果要将文件描述符和用户数据关联起来,以实现快速的数据访问,只能放弃使用epoll_data_t的fd成员,而在ptr指向的用户数据中包含fd。

1.3、epoll_wait:事件等待,返回就绪事件

epoll_wait的作用
当socket收到数据后,中断程序会给eventpoll的“就绪列表”添加socket引用。如下图展示的是sock2和sock3收到数据后,中断程序让rdlist引用这两个socket,而不是像select轮询所有的sock。
Linux下的I/O复用技术 — epoll如何使用(epoll_create、epoll_ctl、epoll_wait) 以及 LT/ET 使用过程解析_第6张图片
eventpoll对象相当于是socket和进程之间的中介,socket的数据接收并不直接影响进程,而是通过改变eventpoll的就绪列表来改变进程状态。

当程序执行到epoll_wait时,如果rdlist已经引用了socket,那么epoll_wait直接返回,如果rdlist为空,阻塞进程,如下:

  • 1、在某时刻进程A运行到了epoll_wait语句。如下图所示,内核会将进程A放入eventpoll的等待队列中,阻塞进程。

Linux下的I/O复用技术 — epoll如何使用(epoll_create、epoll_ctl、epoll_wait) 以及 LT/ET 使用过程解析_第7张图片

  • 2、当socket接收到数据,中断程序一方面修改rdlist,另一方面唤醒eventpoll等待队列中的进程,进程A再次进入运行状态(如下图)。也因为rdlist的存在,进程A可以知道哪些socket发生了变化。
    Linux下的I/O复用技术 — epoll如何使用(epoll_create、epoll_ctl、epoll_wait) 以及 LT/ET 使用过程解析_第8张图片

epoll_wait函数原型
它在一段超时时间内等待一组文件描述符上的事件,阻塞等待注册的事件发生,返回事件的数目,其原型如下:

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

返回值:

  • 成功时返回就绪文件描述符个数,失败时返回-1并设置errno。

参数说明:

  • 1、epfd :epoll_create()的返回值;
  • 2、events: 用来记录被触发的events(结构参考epoll_ctl),其大小受制于maxevents
  • 3、maxevents: 设定最多监听多少个事件,必须大于0,一般设定为65535
  • 4、timeout:在函数调用中阻塞时间上限,单位是ms
    • timeout = -1:表示调用将一直阻塞,直到有文件描述符进入ready状态或者捕获到信号才返回;
    • timeout = 0:用于非阻塞检测是否有描述符处于ready状态,不管结果怎么样,调用都立即返回;
    • timeout > 0:表示调用将最多持续timeout时间,如果期间有检测对象变为ready状态或者捕获到信号则返回,否则直到超时。

timeout参数设计技巧问题?

  • 1、设置为-1,程序阻塞在此,后续任务没法执行。
  • 2、设置为0,程序能继续跑,但即使没事件时,程序也在空转,十分占用cpu时间片,我测试时每个进程都是60+%的cpu占用时间。
  • 综上,我们给出比较好的设置方法:将其设置为1,但还没完,因为即使这样设置,处理其它任务时,在每次循环都会在这浪费1ms的阻塞时间,多次循环后性能损失就比较明显了。
    • 为了避免该现象,我们通常向epoll再添加一个fd,我们有其它任务要执行时直接向该fd随便写入一个字节,将epoll唤醒从而跳过阻塞时间。没任务时epoll超过阻塞时间1ms也会自动挂起,不会占用cpu,两全其美。
    • int eventfd(unsigned int initval, int flags),linux中是一个较新的进程通信方式,可以通过它写入字节。

2、LT/ET 使用过程

2.1 LT水平触发(Level Triggered)

  • Level Triggered (LT) 水平触发
    • socket接收缓冲区不为空 ,说明有数据可读, 读事件一直触发
    • socket发送缓冲区不满 ,说明可以继续写入数据 ,写事件一直触发
    • 符合思维习惯,epoll_wait返回的事件就是socket的状态

LT的处理过程:

  • 1、accept一个连接,添加到epoll中监听EPOLLIN事件 .(注意这里没有关注EPOLLOUT事件
  • 2、当EPOLLIN事件到达时,read fd中的数据并处理 .
  • 3、当需要写出数据时,把数据write到fd中;如果数据较大,无法一次性写出,那么在epoll中监听EPOLLOUT事件 .
  • 4、当EPOLLOUT事件到达时,继续把数据write到fd中;如果数据写出完毕,那么在epoll中关闭EPOLLOUT事件
//LT模式的工作流程
void lt( epoll_event* events, int number, int epollfd, int listenfd )
{
    char buf[ BUFFER_SIZE ];
    for ( int i = 0; i < number; i++ )
    {
        int sockfd = events[i].data.fd;
        if ( sockfd == listenfd )
        {
            struct sockaddr_in client_address;
            socklen_t client_addrlength = sizeof( client_address );
            int connfd = accept( listenfd, ( struct sockaddr* )&client_address, &client_addrlength );
            addfd( epollfd, connfd, false );
        }
        else if ( events[i].events & EPOLLIN )
        {
            //只要socket读缓存中还有未读出的数据,这段代码就被触发
            printf( "event trigger once\n" );
            memset( buf, '\0', BUFFER_SIZE );
            int ret = recv( sockfd, buf, BUFFER_SIZE-1, 0 );
            if( ret <= 0 )
            {
                close( sockfd );
                continue;
            }
            printf( "get %d bytes of content: %s\n", ret, buf );
        }
        else
        {
            printf( "something else happened \n" );
        }
    }
}

2.2 ET边沿触发(Edge Triggered )

  • Edge Triggered (ET) 边沿触发
    • socket的接收缓冲区状态变化时触发读事件,即空的接收缓冲区刚接收到数据时触发读事件(从无到有
    • socket的发送缓冲区状态变化时触发写事件,即满的缓冲区刚空出空间时触发读事件(从有到无
    • 仅在状态变化时触发事件

ET的处理过程:

  • 1、accept一个一个连接,添加到epoll中监听EPOLLIN|EPOLLOUT事件()
  • 2、当EPOLLIN事件到达时,read fd中的数据并处理,read需要一直读,直到返回EAGAIN为止
  • 3、当需要写出数据时,把数据write到fd中,直到数据全部写完,或者write返回EAGAIN
  • 4、当EPOLLOUT事件到达时,继续把数据write到fd中,直到数据全部写完,或者write返回EAGAIN

从ET的处理过程中可以看到,ET的要求是需要一直读写,直到返回EAGAIN,否则就会遗漏事件。而LT的处理过程中,直到返回EAGAIN不是硬性要求,但通常的处理过程都会读写直到返回EAGAIN,但LT比ET多了一个开关EPOLLOUT事件的步骤

当我们使用ET模式的epoll时,我们应该按照以下规则设计:

  • 1、在接收到一个I/O事件通知后,立即处理该事件。程序在某个时刻应该在相应的文件描述符上尽可能多地执行I/O。
  • 2、在ET模式下,在使用epoll_ctl注册文件描述符的事件时,应该把描述符设置为非阻塞的(非常重要)
    • 因为程序采用循环(ET里面采用while循环,看清楚呦,LE是if判断)来对文件描述符执行尽可能多的I/O,而文件描述符又被设置为可阻塞的,那么最终当没有更多的I/O可执行时,I/O系统调用就会阻塞。基于这个原因,每个被检查的文件描述符通常应该置为非阻塞模式,在得到I/O事件通知后重复执行I/O操作,直到相应的系统调用(比如read(),write())以错误码EAGAIN或EWOULDBLOCK的形式失败。
//ET模式的工作流程
void et( epoll_event* events, int number, int epollfd, int listenfd )
{
    char buf[ BUFFER_SIZE ];
    for ( int i = 0; i < number; i++ )
    {
        int sockfd = events[i].data.fd;
        if ( sockfd == listenfd )
        {
            struct sockaddr_in client_address;
            socklen_t client_addrlength = sizeof( client_address );
            int connfd = accept( listenfd, ( struct sockaddr* )&client_address, &client_addrlength );
            addfd( epollfd, connfd, true );
        }
        else if ( events[i].events & EPOLLIN )
        {
            //这段代码不会被重复触发,所以我们循环读取数据,以确保把socket读缓存中的所有数据读出
            printf( "event trigger once\n" );
            while( 1 )
            {
                memset( buf, '\0', BUFFER_SIZE );
                int ret = recv( sockfd, buf, BUFFER_SIZE-1, 0 );
                if( ret < 0 )
                {
                    //对于非阻塞IO,下面条件成立表示数据已经全部读取完毕。
                    //此后,epoll就能再次触发sockfd上的EPOLLIN事件,已驱动下一次读操作
                    if( ( errno == EAGAIN ) || ( errno == EWOULDBLOCK ) )
                    {
                        printf( "read later\n" );
                        break;
                    }
                    close( sockfd );
                    break;
                }
                else if( ret == 0 )
                {
                    close( sockfd );
                }
                else
                {
                    printf( "get %d bytes of content: %s\n", ret, buf );
                }
            }
        }
        else
        {
            printf( "something else happened \n" );
        }
    }
}

3、使用案例

Server.cpp

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

#define SOCKET_PORT 9999
#define EPOLL_SIZE 10

int main(int argc, char *argv[])
{
    int server_fd;
    int client_fd;
    int epfd;
    
    struct sockaddr_in addr;
    memset(&addr,0,sizeof(addr));
    addr.sin_family = AF_INET;
    
    if (argc == 2){
        addr.sin_addr.s_addr = inet_addr(argv[1]);
        addr.sin_port = htons(SOCKET_PORT);
    }else if(argc==3){
        addr.sin_addr.s_addr = inet_addr(argv[1]);
        addr.sin_port = htons(atoi(argv[2]));
    }else{
        addr.sin_addr.s_addr = htonl(INADDR_ANY);
        addr.sin_port = htons(SOCKET_PORT); 
    }
    
    server_fd = socket(PF_INET,SOCK_STREAM,0);
    if (server_fd < 0){
        std::cout<<"Create Socket Errors, "<<server_fd<<std::endl;
        exit(server_fd);
    }
    
    if (bind(server_fd, (struct sockaddr *)&addr, sizeof(struct sockaddr)) < 0) 
    {
        perror("bind error");
        return -1;
    }
    
    if (listen(server_fd, 10)<0)
    {
        perror("listen error");
        return -1;
    }else{
        printf("Stared the Socket: %s:%d\n",
               inet_ntoa(addr.sin_addr),
               ntohs(addr.sin_port));
    }
    
    epfd = epoll_create(EPOLL_SIZE);
    if (epfd < 0)
    {
        perror("Epoll Create");
        exit(-1);
    }
    
    struct epoll_event ev;
    struct epoll_event events[EPOLL_SIZE];
    ev.data.fd = server_fd;
    ev.events = EPOLLIN | EPOLLET;
    epoll_ctl(epfd, EPOLL_CTL_ADD, server_fd, &ev);
    fcntl(server_fd, F_SETFL, fcntl(server_fd, F_GETFD, 0)| O_NONBLOCK);
    
    while (1)
    {
        int epoll_events_count = epoll_wait(epfd, events, EPOLL_SIZE, -1);
        if (epoll_events_count < 0)
        {
            perror("epoll failed");
            break;
        }
        printf("epoll_events_count = %d\n", epoll_events_count);
        for (int i=0;i < epoll_events_count;i++)
        {
            if (events[i].data.fd==server_fd&&(events[i].events & EPOLLIN))
            {
                struct sockaddr_in remote_addr;
                
                socklen_t socklen = sizeof(struct sockaddr_in);
                client_fd = accept(server_fd, (struct sockaddr *)&remote_addr, &socklen);
                if (client_fd < 0)
                {
                    perror("accept error");
                }
                printf("client connection from: %s : % d(IP : port), "
                       "clientfd = %d \n",
                       inet_ntoa(remote_addr.sin_addr),
                       ntohs(remote_addr.sin_port),
                       client_fd);
                
                ev.data.fd = client_fd;
                epoll_ctl(epfd, EPOLL_CTL_ADD, client_fd, &ev);
                
                fcntl(client_fd, F_SETFL,
                      fcntl(client_fd, F_GETFD, 0)| O_NONBLOCK);
                send(client_fd,"Welcome to My server",21,0);
            }else{ 
                char buffer[1024];
                int len;
                if ((len=recv(events[i].data.fd,buffer,sizeof(buffer), 0))>0)
                {
                    send(events[i].data.fd,"Welcome to My server\n",21,0);
                    printf("%s fd %d \n",buffer,events[i].data.fd);
                    
                    //close(client_fd);
                }else{
                    printf("client offline with: "
                           "clientfd = %d \n",
                           events[i].data.fd);
                }
            }
        }
    }
    close(server_fd);
    close(epfd);
    return 0;                                                                   
}

Client.cpp

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

//#include   //menset
#include 
#include 

#define EPOLL_SIZE 10

int main(int argc, char *argv[])
{
    int size;
    int client_fd;
    
    struct sockaddr_in addr;
    memset(&addr,0,sizeof(addr));
    addr.sin_family = AF_INET;
    
    if (argc == 3){
        addr.sin_addr.s_addr = inet_addr(argv[1]);
        addr.sin_port = htons(atoi(argv[2]));
    }else{
        printf("The host ip and port is falied.\n");
        exit(-1);
    }
    
    client_fd = socket(PF_INET,SOCK_STREAM,0);
    
    if (client_fd < 0){
        std::cout<<"Create Socket Errors, "<<client_fd<<std::endl;
        exit(client_fd);
    }
    
    size = connect(client_fd, (struct sockaddr *)&addr,
                   sizeof(struct sockaddr));
    if (size < 0)
    {
        perror("connect error");
    }
    printf("Connect client: %s:%d \n",inet_ntoa(addr.sin_addr),
           ntohs(addr.sin_port));
    
    int epfd;
    epfd = epoll_create(EPOLL_SIZE);
    struct epoll_event ev;
    
    ev.data.fd = client_fd;
    ev.events = EPOLLIN | EPOLLET;
    epoll_ctl(epfd, EPOLL_CTL_ADD, client_fd, &ev);
    fcntl(client_fd, F_SETFL, fcntl(client_fd, F_GETFD, 0)| O_NONBLOCK);
    
    char buffer[1024];
    struct epoll_event events[EPOLL_SIZE];
    
    int len;
    while (1)
    {
        int epoll_events_count = epoll_wait(epfd, events, EPOLL_SIZE, -1);
        if (epoll_events_count<0)
        {
            perror("Epoll wait");
            exit(-1);
        }
        
        for (int i=0;i<epoll_events_count;i++)
        {
            printf("client fd %d, event fd %d \n",client_fd,
                   events[i].data.fd);
            len = recv(client_fd, buffer, sizeof(buffer), 0);
            if (len > 0)
            {
                printf("%s",buffer);
            }
            sleep(3);
            send(client_fd,"Welcome to My client",21,0);
        }
    }
    return 0;
}

部分函数说明

atoi 将字符串转化为int
int atoi (const char * str)

atol 将字符串转化为long
long int atol ( const char * str )

4、常问面试问题

参考上一篇博客:Epoll常见面试问题,LT和ET区别?将socket设为非阻塞?正确的读写方式?关于epoll的数据结构?epoll与select、poll的对比?

参考

1、https://www.cnblogs.com/kefeiGame/p/7246942.html
2、https://zhuanlan.zhihu.com/p/21374980
3、https://www.cnblogs.com/lojunren/p/3856290.html
4、https://bbs.gameres.com/thread_842984_1_1.html
5、https://blog.csdn.net/ChaseRaod/article/details/79361025
6、https://www.zhihu.com/question/23614342/answer/184513538

你可能感兴趣的:(网络编程)