• 2022-08-12 Linux下epoll模型-高性能网络IO


    Linux下高性能网络IO-epoll

    做网络IO不得不讲的就是epoll,与大家分享一下,即使总结又是提高。
    讲得不好,欢迎大家留言指正。



    前言

    epoll的描述,网络上门很多,不再过多的陈述。
    摘自网络:
    在这里插入图片描述


    一、epoll的基础知识

    1. 基本API

    epoll调用,基本的API有:epoll_create,epoll_ctl,epoll_wait; epoll_create:
    用来创建一个epoll epoll_wait: 监控哪些可读可写,一次性返回的是所有的可读可写的fd,把内核中就绪队列的一次性拷贝出来。
    epoll_ctl: 用来向epoll中,增加(add)、修改(mod)、删除(del)文件描述符;

    1. 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 event */
             epoll_data_t data;      /* User data variable */
         };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    1. epoll的基本事件

    EPOLLIN:表示对应的文件描述符可以读;
    EPOLLOUT:表示对应的文件描述符可以写;
    EPOLLPRI:表示对应的文件描述符有紧急的数可读;
    EPOLLERR:表示对应的文件描述符发生错误;
    EPOLLHUP:表示对应的文件描述符被挂断;
    EPOLLET: ET的epoll工作模式;

    1. epoll中的水平触发和边缘触发(摘自网络)

    水平触发(level-trggered)
    只要文件描述符关联的读内核缓冲区非空,有数据可以读取,就一直发出可读信号进行通知,
    当文件描述符关联的内核写缓冲区不满,有空间可以写入,就一直发出可写信号进行通知
    LT模式支持阻塞和非阻塞两种方式。epoll默认的模式是LT。

    边缘触发(edge-triggered)
    当文件描述符关联的读内核缓冲区由空转化为非空的时候,则发出可读信号进行通知,
    当文件描述符关联的内核写缓冲区由满转化为不满的时候,则发出可写信号进行通知
    两者的区别在哪里呢?水平触发是只要读缓冲区有数据,就会一直触发可读信号,而边缘触发仅仅在空变为非空的时候通知一次,

    个人总结:水平触发没有处理结束可以反复触发;边缘触发则是一次性的;

    二、单线程代码演示tcp服务器

    实现一个单线程服务器,从客户端接收到任何数据,都进行确认,发送回复“OK”;
    
    • 1

    1.大致流程图

    在这里插入图片描述

    2.代码展示

    代码如下(示例):

    /*
    File:       epoll.cpp
    Function:   epoll的编程方法使用,使用epoll编写网络tcp服务器
    Writer:     syq
    Time:       2022-08-11
    
    
    */
    
    
    /*
        1. 优点
            epoll没有文件描述符的限制;工作效率不会随文件描述符数量增大而效率低;内核级优化;
            只遍历触发的,而select则遍历所有的fd
        2. 水平触发(没有处理就反复发送),边缘触发(只触发一次)
        3. 函数
            epool_create\epool_ctl\epoll_wait
        4. 事件
            EPOLLLET、EPOLLIN、EPOLLOUT、EPOLLPRI、EPOLLERR、EPOLLHUP
        5. 操作
            add\mod\del
    
    */
    #include 
    #include 
    #include           /* See NOTES */
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #define MAX_NUM 512
    #define MAX_EVENTS 1024
    
    /*
    * 初始化一个server socket
    * 参数: nPort使用的端口
    * 返回: 返回已经初始化完成的socketfd
    */
    int Init_ServerSocket(int nPort)
    {
        int sockfd = socket(AF_INET,SOCK_STREAM,0); 
        if(sockfd == -1){
            perror("socket error!");
            exit(1);
        }
        struct sockaddr_in server_addr;
        server_addr.sin_family = AF_INET;
        server_addr.sin_addr.s_addr = INADDR_ANY;
        server_addr.sin_port = htons(nPort);
        bzero(&(server_addr.sin_zero),8);
    
        if(bind(sockfd,(struct sockaddr *)&server_addr,sizeof(server_addr)) < 0) {
            perror("Bind error:");
            exit(1);
        }
        if(listen(sockfd,MAX_NUM) < 0){
            perror("Listen error:");
            exit(1);
        }
        return sockfd;
    }
    
    
    
    /*
    * 演示epoll的单线程使用,缺点一次只能响应一个连接,并且处理数据
    */
    
    int main(int argc,char* argv[])
    {
        char bufin[1024] = {0};
        //1. 得到一个监听socket
        int m_nAcceptfd = Init_ServerSocket(8888);
        //2. 创建一个epoll
        int m_nEpollfd = epoll_create(256);
        struct epoll_event ev,EVENTS[MAX_EVENTS];
        //3. 默认设置为水平触发,可以返回读取
        ev.events = EPOLLIN;
        ev.data.fd = m_nAcceptfd;
        epoll_ctl(m_nEpollfd,EPOLL_CTL_ADD,m_nAcceptfd,&ev);
        //4. 进入循环,开始循环等待epoll的事件触发
        while(1){
           int nAlreadyIn = epoll_wait(m_nEpollfd,EVENTS,MAX_EVENTS,-1);
           //5. 采用遍历
           for(int i = 0; i < nAlreadyIn; i ++){
                //6. 判断触发的是accepted
                if(EVENTS[i].data.fd == m_nAcceptfd){
                    //7. 
                    if(1){
                        //8. 调用accept,获取到设备描述符
                        int socketfd = accept(m_nAcceptfd,NULL,NULL);
                        std::cout<< "get connected :"<<bufin<<std::endl;
                        //9. 进入循环读取
                        while(1){
                            memset(bufin,0,1024);
                            int nRead = recv(socketfd,bufin,1024,0);
                            if(nRead >= 0){
                                //10. 打印数据
                                std::cout<< "recv:"<<bufin<<std::endl;
                                send(socketfd,"ok",2,0);
                            }else{
                                std::cout<< "error:"<<std::endl; 
                                close(socketfd); 
                                break;
                            }
                        }
                    }
                }
           }     
        }
        
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114

    3.使用tcp调试工具运行

    ![在这里插入图片描述](https://img-blog.csdnimg.cn/ce45c38a430947e88d6e24d0384ce292.png

    可以看到能够正常的实现tcp服务器接收到数据后,回发“OK字段”。

    3.代码弊端

    这样编写的tcp服务器,即使用了epoll,也面临以下问题:

    1. 不能多客户端处理:至始至终只能有一个客户端进行连接,不能由多个客户端连接;
    2. recv函数和send函数在同一个处理触发事件中,当send数据量大的时候,会造成缓冲区阻塞,影响整体效率。

    二、使用epoll的几种模型

    通过学习和总结,列出了以下几种使用epoll开发的模型:
    
    • 1

    在这里插入图片描述

    三、常见的使用epoll的服务器

    redis: 使用单线程方式,线程内将send\recv操作不放在一个流程中进行,利用“流程异步”;

    nginx:多进程方式;
    在这里插入图片描述

    ./sbin/nginx -c conf/nginx.conf
    
    • 1

    可以看到,启动了一个master进程,4个worker process; 并且worker process都是master process的子进程;
    在这里插入图片描述

    memcached: 多线程模式

    三、利用epoll+fork实现高性能网络服务器

    1. 使用fork多进程的优缺点

    缺点:fork出来的进程被长期占用,分配子进程花费的时间长;
    优点:进程隔离,不会相互影响,稳定性更高;
    
    • 1
    • 2

    2. 做法

    1. 一次性创建N个子进程,每个子进程创建epoll,并且进入wait流程;而父进程则调用wait函数等待,否则子进程会编程孤儿进程;

    2. 代码

    /*
    File:       epoll_fork.cpp
    Function:   使用epoll+fork子进程的方式编写高性能的tcp网络服务器
    Writer:     syq
    Time:       2022-08-12
    
    
    */
    
    
    
    #include 
    #include 
    #include           
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #define MAX_NUM 512
    #define MAX_EVENTS 1024
    #define PROCESS_NUM 5
    
    /*
    * 初始化一个server socket
    * 参数: nPort使用的端口
    * 返回: 返回已经初始化完成的socketfd
    */
    int Init_ServerSocket(int nPort)
    {
        int sockfd = socket(AF_INET,SOCK_STREAM,0); 
        if(sockfd == -1){
            perror("socket error!");
            exit(1);
        }
        struct sockaddr_in server_addr;
        server_addr.sin_family = AF_INET;
        server_addr.sin_addr.s_addr = INADDR_ANY;
        server_addr.sin_port = htons(nPort);
        bzero(&(server_addr.sin_zero),8);
    
        if(bind(sockfd,(struct sockaddr *)&server_addr,sizeof(server_addr)) < 0) {
            perror("Bind error:");
            exit(1);
        }
        if(listen(sockfd,MAX_NUM) < 0){
            perror("Listen error:");
            exit(1);
        }
        return sockfd;
    }
    
    
    
    /*
    * 利用fork创建子进程
    */
    
    int main(int argc,char* argv[])
    {
        int status = -1;
        int flags = 1;
        int backlog = 10;
        pid_t pid = -1;
        char bufin[1024] = {0};
        //1. 得到一个监听socket
        int m_nAcceptfd = Init_ServerSocket(28888);
    
        //2. 进行fork
         
        for(int a=0; a < PROCESS_NUM; a++){
            if(pid !=0){
                pid = fork(); 
            }
        }
    
        //3.子进程执行
        if(pid == 0){
            //4. 创建一个epoll
            int m_nEpollfd = epoll_create(256);
            struct epoll_event ev,EVENTS[MAX_EVENTS];
            //5. 默认设置为水平触发,可以返回读取
            ev.events = EPOLLIN;
            ev.data.fd = m_nAcceptfd;
            epoll_ctl(m_nEpollfd,EPOLL_CTL_ADD,m_nAcceptfd,&ev);
            //6. 进入循环,开始循环等待epoll的事件触发
            while(1){
                int nAlreadyIn = epoll_wait(m_nEpollfd,EVENTS,MAX_EVENTS,-1);
                //5. 采用遍历
                for(int i = 0; i < nAlreadyIn; i ++){
                        //6. 判断触发的是accepted
                        if(EVENTS[i].data.fd == m_nAcceptfd){
                                //7. 调用accept,获取到设备描述符
                                int socketfd = accept(m_nAcceptfd,NULL,NULL);
                                std::cout<< "get connected :"<<socketfd<<std::endl;
                                //8.将新创建的socket设置为 NONBLOCK 模式
                                flags = fcntl(socketfd, F_GETFL, 0);
                                fcntl(socketfd, F_SETFL, flags|O_NONBLOCK);
    
                                //9. 将它放进epoll,并且设置为边缘触发
                                ev.events = EPOLLIN | EPOLLET;
                                ev.data.fd = socketfd;
                                epoll_ctl(m_nEpollfd,EPOLL_CTL_ADD,socketfd,&ev);
    
                            }else{
                                //10. 继续执行代码
                                if(EVENTS[i].events & EPOLLIN){
                                    //11.读数据
                                    memset(bufin,0,1024);
                                    int nRead = recv(EVENTS[i].data.fd,bufin,1024,0);
                                
                                    if(nRead <= 0){
                                        //13. 做错误数据分类
                                        switch (errno){
                                            case EAGAIN: //说明暂时已经没有数据了,要等通知
                                                break;
                                            case EINTR: //被终断了,再来一次
                                                printf("recv EINTR... \n");
                                                nRead = recv(EVENTS[i].data.fd, bufin, 1024, 0);
                                                break;
                                            default:
                                                printf("the client is closed, fd:%d\n", EVENTS[i].data.fd);
                                                epoll_ctl(m_nEpollfd, EPOLL_CTL_DEL, EVENTS[i].data.fd, &ev); 
                                                close(EVENTS[i].data.fd);
                                                ;
                                        }
                                        break;
                                    }
                                    if(nRead > 0){
                                        //12. 打印数据
                                        std::cout<< "recv:"<<bufin<<std::endl;
                                        send(EVENTS[i].data.fd,"ok",2,0);
                                    }
                                    
                                }                 
                            }           
                        }
                }     
            }else{
                std::cout<<"wait"<<std::endl;
                waitpid(-1,&status,0);
            }
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148

    2. 效果

    在这里插入图片描述

    在这里插入图片描述

    三、讲到“惊群效应”

    惊群现象就是多进程(多线程)在同时阻塞等待同一个事件的时候(休眠状态),如果等待的这个事件发生,那么他就会唤醒等待的所有进程(或者线程),但是最终却只可能有一个进程(线程)获得这个时间的“控制权”,对该事件进行处理,而其他进程(线程)获取“控制权”失败,只能重新进入休眠状态,这种现象和性能浪费就叫做惊群

    参考链接:https://www.zhihu.com/question/22756773

    三、总结

    epoll与select的比较; epoll没有文件描述符的限制;工作效率不会随文件描述符数量增大而效率低;内核级优化;
    epoll只遍历触发的,而select则遍历所有的fd;

    下一阶段,我们将继续研究redis,nginx等源码。

  • 相关阅读:
    寒假作业2月13号
    [c++基础]-vector类
    C++控制不同进制输出(二进制,八进制,十进制,十六进制)各种进制之间的转换
    python笔记Ⅳ--序列(列表、切片)
    在 MATLAB 中显示 3D 图像
    WPF调用webapi并展示数据(二):类库实体类的构建
    科技型中小企业认定条件
    CKA认证,开启您的云原生之旅!
    Spring JDBC(配置数据源,操作数据库)
    Bayes判别:统计学中的经典分类方法
  • 原文地址:https://blog.csdn.net/shayueqing/article/details/126303381