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. epoll的基本事件

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

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

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

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

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

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

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

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 <stdio.h>
#include <stdlib.h>
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/epoll.h>
#include <string.h>
#include <iostream>
#include <unistd.h>
#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;
                        }
                    }
                }
            }
       }     
    }
    
}

3.使用tcp调试工具运行

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

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

3.代码弊端

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

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

二、使用epoll的几种模型

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

在这里插入图片描述

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

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

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

./sbin/nginx -c conf/nginx.conf

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

memcached: 多线程模式

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

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

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

2. 做法

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

2. 代码

/*
File:       epoll_fork.cpp
Function:   使用epoll+fork子进程的方式编写高性能的tcp网络服务器
Writer:     syq
Time:       2022-08-12


*/



#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>          
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/epoll.h>
#include <string.h>
#include <iostream>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/wait.h>
#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;
}

2. 效果

在这里插入图片描述

在这里插入图片描述

三、讲到“惊群效应”

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

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

三、总结

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

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ShaYQ

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值