美文网首页
服务器IO多路复用之epoll机制

服务器IO多路复用之epoll机制

作者: 二进制人类 | 来源:发表于2022-09-20 19:42 被阅读0次

    概述

    epoll是在Linux2.6内核中提出,是对于select机制和poll机制的加强版本。相对于select和poll来说,epoll更加的灵活,没有文件描述符限制。epoll实例是使用一个文件描述符来管理一个进程中的多个文件描述符,将该进程中所有需要关心的文件描述符及其事件添加到内核中epoll实例对应的事件表中。在检测资源准备就绪的事件和文件描述符返回给用户空间,在用户空间使用数组来接收资源已经准备就绪的事件和文件描述符集合。

    特征

    1. 利用文件描述符来管理需要关心的文件描述符。对文件描述符没有限制。
    2. 添加到epoll实例中的文件描述符一直存在于epoll实例中,只有在用户删除的时候,从epoll实例中删除。可以一次添加多次使用。
    3. 有资源准备就绪,返回是已经准备就绪的所有文件描述符及其事件,直接对其进行IO操作。不需要逐一去查找准备就绪的文件描述符,效率会更高。

    处理模式

    在epoll实例对于事件的检测或者响应分为电平触发和边沿触发

    Level-triggered 电平触发

    当epoll_wait函数在检测的时候,当事件发生并将事件通知给应用程序,应用程序可以不用立即处理事件,在下一次调用epoll_wait的时候,会再次将该事件通知给应用程序。

    Edge-triggered 边沿触发

    当epoll_wait函数在检测的时候,当事件发生并将事件通知给应用程序,应用程序必须立即处理该事件,如果不处理该事件,在下一次调用epoll_wait的时候,不会再次将该事件通知给应用程序。

    PS:注意事项:

    ET边沿触发模式,可以在很大程度上去检测epoll事件的触发次数,所以效率比LT电平触发模式要高。

    ET模式下必须使用非阻塞接口,避免在事件触发后没有去处理事件而导致其它IO的阻塞。

    相关接口

    创建

    #include <sys/epoll.h>
    /**
     * [epoll_create 创建红黑树头节点]
     * @param  size [size在Linux 2.6.8 版本中已经被忽略,但是必须大于0.]
     * @return      [成功返回epoll实例的文件描述符,失败返回-1且修改errno的值。]
     */
    int epoll_create(int size);    /* 应用程序在调用该函数的时候内核空间会创建红黑树的头结点 */
    

    操作

    上树:将文件描述符及其事件添加到epoll实例中;

    下树:将文件描述符及其事件从epoll实例中删除

    #include <sys/epoll.h>
    /**
     * [epoll_ctl 操作]
     * @param  epfd  [epoll实例的文件描述符]
     * @param  op    [ 
                      EPOLL_CTL_ADD    将文件描述符及其事件添加到epoll实例中;
                      EPOLL_CTL_MOD    修改epoll实例中fd对应的事件;
                      EPOLL_CTL_DEL    将文件描述符及其事件从epoll实例中删除;
                      ]
     * @param  fd    [需要设置的文件描述符]
     * @param  event [
            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:需要关心的事件(EPOLLIN、EPOLLOUT) /
                epoll_data_t data;      
            };      
                     ]
     * @return       [成功返回0,失败返回-1且修改errno的值]
      PS:用户在调用epoll_ctl函数的时候,内核空间可以实现对红黑树中去添加、删除、修改节点。
     */
    int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); 
    

    阻塞检测IO事件

    #include <sys/epoll.h>
    /**
     * [epoll_wait 检测epoll实例中是否有资源准备就绪]
     * @param  epfd      [epoll实例的文件描述符]
     * @param  events    [结构体指针数组首元素的地址,用来存储资源准备就绪的文件描述符和事件信息。]
     * @param  maxevents [事件数,必须大于0]
     * @param  timeout   [设置阻塞的方式,超时时间
                         -1:阻塞模式:遇到有资源准备就绪返回,否则没有资源准备就绪会一直阻塞等待。
                          0:非阻塞模式,没有资源准备就绪返回错误信息,有资源准备就绪,直接返回
                         >0:按照设置的时间阻塞,以ms为单位。
                         ]
     * @return           [有资源准备就绪返回就绪IO的个数;
                          阻塞超时,返回0.
                          失败返回-1且修改errno的值
                          ]
     */
    int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
    

    关闭
    由于通过epoll_create创建的epoll实例返回的是文件描述符,所以在不使用epoll实例的时候,可以使用close函数来销毁epoll实例。内核就会将epoll实例的红黑树给销毁掉

    实例

    #include <stdio.h>
    #include <string.h>
    #include <stdlib.h>
    #include <unistd.h>
    
    #include <sys/types.h>    
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <netinet/ip.h> 
    #include <sys/epoll.h>
    
    int main()
    {
        int listenfd;
        int connfd;
        int ret;
        int optval;
        int count;
        struct sockaddr_in srvaddr;
        struct sockaddr_in cltaddr;
        socklen_t addrlen = sizeof(cltaddr);
        int fd;
        int epfd;
        char buf[128];
        int i;
        struct epoll_event event;
        struct epoll_event events[8];
    
        /* 1. 创建TCP流式套接字文件 */
        listenfd = socket(AF_INET, SOCK_STREAM, 0);
        if (listenfd == -1)
        {
            perror("socket");
            return -1;
        }
    
        /* 设置套接字允许IP地址和端口被重新设置 */
        optval = 1;/* 允许IP地址和端口被重新设置 */
        ret = setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval));
        if (ret == -1)
        {
            perror("setsockopt");
            return -1;
        }
    
        /* 2. 设置服务器主机的IP地址和端口号 */
        srvaddr.sin_family = AF_INET;
        srvaddr.sin_port = htons(8888);
        srvaddr.sin_addr.s_addr = htonl(INADDR_ANY);
        ret = bind(listenfd, (const struct sockaddr *)&srvaddr, sizeof(srvaddr));
        if (ret == -1)
        {
            perror("bind");
            return -1;
        }
    
        /* 3. 启动服务器监听客户端的连接请求 */
        ret = listen(listenfd, 1024);
        if (ret == -1)
        {
            perror("listen");
            return -1;
        }
        printf("listenfd = %d server init success\n", listenfd);
    
        /* 创建一个epoll实例 */
        epfd = epoll_create(512);
        if (epfd == -1)
        {
            perror("epoll_create");
            return -1;
        }
    
        /* 上树:将文件描述符listenfd及其读事件添加到epoll实例中 */
        event.events = EPOLLIN;
        event.data.fd = listenfd;
        ret = epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &event);
        if (ret == -1)
        {
            perror("epoll_ctl->EPOLL_CTL_ADD");
            return -1;
        }
       
        while(1)
        {
            /* 检测是否有资源准备就绪,准备就绪返回就绪的IO资源数 */
            count = epoll_wait(epfd, events, 512, 5000);
            if (count == -1)
            {
                perror("epoll_wait");
                return -1;
            }
            else if (count == 0)
            {
                fprintf(stderr, "epoll_wait timeout ...\n");
                continue;
            }
            for (i = 0; i < count; i++)
            {
                fd = events[i].data.fd;/* 得到准备就绪集合中的文件描述符 */
                if(events[i].events == EPOLLIN)
                {
                    /* 读事件 */
                    if (fd == listenfd)  /* 监听套接字资源准备就绪:说明有新的客户端发起连接请求 */
                    {
                        /* 4. 服务器等待监听客户端的连接请求并建立连接 */
                        connfd = accept(listenfd, (struct sockaddr *)&cltaddr, &addrlen);
                        if (connfd == -1)
                        {
                            perror("accept");
                            return -1;
                        }
                        printf("connfd = %d client connect success\n", connfd);
    
                        /* 上树:有新的客户端连接成功,将新的通信套接字文件描述符及其事件添加到epoll实例中 */
                        event.events = EPOLLIN;
                        event.data.fd = connfd;
                        ret = epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &event);
                        if (ret == -1)
                        {
                            perror("epoll_ctl->EPOLL_CTL_ADD");
                            return -1;
                        }
                    }
                    else    /* 通信套接字资源准备就绪:说明有已经连接的客户端发送数据请求 */
                    {
                        /* 接收客户端的数据请求 */
                        memset(buf, 0, sizeof(buf));
                        ret = read(fd, buf, sizeof(buf));
                        if (ret == -1)
                        {
                            perror("read");
                            return -1;
                        }
                        else if (ret == 0)
                        {
                            /*说明对端(客户端)退出:完成下树,将退出的文件描述符fd及其事件从epoll实例中删除 */
                            event.data.fd = fd;
                            ret = epoll_ctl(epfd, EPOLL_CTL_DEL, fd, &event);
                            if (ret == -1)
                            {
                                perror("epoll_ctl->EPOLL_CTL_DEL");
                                return -1;
                            }
                            close(fd);
                            break;
                        }
                        else
                        {
                            /* 调用write数据的回写 */
                            ret = write(fd, buf, sizeof(buf));
                            if (ret == -1)
                            {
                                perror("write");
                                return -1;
                            }
                        }
                    }
                }
                else if (events[i].events == EPOLLOUT)
                {
                    /* 写事件 */
                    printf("fd ==     = %d\n", fd);
                }
                else
                {
                    /* 其它事件 */
                    printf("fd ===== %d\n", fd);
                }
            }
        }
        /*服务器退出:完成下树,将退出的文件描述符listenfd及其事件从epoll实例中删除 */
        event.data.fd = listenfd;
        ret = epoll_ctl(epfd, EPOLL_CTL_DEL, listenfd, &event);
        if (ret == -1)
        {
            perror("epoll_ctl->EPOLL_CTL_DEL");
            return -1;
        }
    
        close(listenfd);
        close(epfd);
        return 0;
    }
    

    相关文章

      网友评论

          本文标题:服务器IO多路复用之epoll机制

          本文链接:https://www.haomeiwen.com/subject/kpkiortx.html