【Linux】IO多路转接 - epoll

news2024/11/29 5:49:03

文章目录

  • I/O多路转接之epoll
    • epoll初识
    • epoll的相关系统调用函数
      • epoll_create
      • epoll_ctl
      • epoll_wait
    • epoll工作原理
    • epoll服务器-*
    • epoll的优缺点
    • epoll工作方式
    • 对比LT和ET

I/O多路转接之epoll

epoll初识

epoll也是系统提供的一个多路转接的接口,epoll才是使用和面试的重点,在效率和易用性方面都有提升,

  • 与select和poll的定位是一样的, epoll系统调用也可以让我们的程序同时监视多个文件描述符上的事件是否就绪,适用场景也相同
  • epoll在命名上比poll多了一个e,这个e可以理解成是extend,epoll就是为了同时处理大量文件描述符而改进的poll
  • epoll在2.5.44内核中被引进,它几乎具备了select和poll的所有优点,被公认为Linux2.6下性能最好的多路I/O就绪通知方法

epoll的相关系统调用函数

epoll有三个相关的系统调用,分别是epoll_create、epoll_ctl和epoll_wait,

epoll_create

epoll_create函数用于创建一个epoll模型

#include <sys/epoll.h>
int epoll_create(int size);

参数说明

size:自从Linux2.6.8之后,size参数是被忽略的,但为了向前兼容,大多写成128或256, 但size的值必须设置为大于0的值

返回值说明

返回的是epoll句柄, 也就是对应的文件描述符,对应创建一个epoll模型, 否则返回-1,同时错误码会被设置

注意: 当不再使用该epoll模型时,必须调用close函数关闭epoll模型对应的文件描述符,当所有引用epoll实例的文件描述符都已关闭时,内核将销毁该实例并释放相关资源

image-20221006103228006

如图所示,epoll_create 返回值本质是个文件描述符


epoll_ctl

不管是哪种多路转接方案,都要进行的工作步骤是:用户告诉内核和内核告诉用户,而epoll_ctl负责的就是用户告诉内核的任务,告诉内核你需要帮我关心文件描述符上的什么事件

epoll_ctl函数用于向指定的epoll模型中注册事件,

#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

参数说明

  • epfd:指定的epoll模型 -> 也就是epoll_create函数的返回值
  • op:表示具体的动作,用三个宏来表示
    • op的取值有以下三种:
      • EPOLL_CTL_ADD:注册新的文件描述符到指定的epoll模型中,
      • EPOLL_CTL_MOD:修改已经注册的文件描述符的监听事件,
      • EPOLL_CTL_DEL:从epoll模型中删除指定的文件描述符,
  • fd:需要监视的文件描述符, ( 用来指定关心的文件描述符)
  • event:需要监视该文件描述符上的哪些事件

关于struct epoll_event结构体

image-20221006102103255

struct epoll_event结构中有两个成员:第一个成员events表示的是需要监视的事件,第二个成员data是一个联合体结构,一般选择使用该结构当中的fd,表示需要OS帮我们关心的文件描述符

关于events的取值:

解释
EPOLLIN表示对应的文件描述符可以读(包括对端SOCKET正常关闭)
EPOLLOUT表示对应的文件描述符可以写
EPOLLPRI表示对应的文件描述符有紧急的数据可读(带外数据)
EPOLLERR表示对应的文件描述符发生错误
EPOLLHUP表示对应的文件描述符被挂断
EPOLLET将EPOLL设为边缘触发(Edge Triggered)模式
EPOLLONESHOT只监听一次事件,本次之后自动将该fd删去

同样的,这些都是以宏的方式进行定义的,其二进制序列当中有且只有一个比特位是1,且为1的比特位是各不相同的

image-20221006102400913

返回值说明

函数调用成功返回0,调用失败返回-1,同时错误码会被设置,


注意:和 select 和 poll 不同,epoll_ctl 函数向内核提供用户所关心的fd和事件时只用提供一次,后序OS都会记得, 如果要删除或修改就再调用时修改下op


epoll_wait

epoll_wait负责的就是内核告诉用户特定fd事件就绪的任务,

  • epoll_ctl函数用于收集监视的文件描述符中已经就绪的事件
#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

参数说明

  • epfd:指定的epoll模型 -> 即:epoll_create的返回值
  • events:内核会将已经就绪的事件拷贝到events数组当中(events不能是空指针,内核只负责将就绪事件拷贝到该数组中,不会帮我们在用户态中分配内存)
  • maxevents:events数组中的元素个数,该值不能大于创建epoll模型时传入的size值
  • timeout:表示epoll_wait函数的超时时间,单位是毫秒(ms)

关于参数timeout的取值

  • -1:epoll_wait调用后进行阻塞等待,直到被监视的某个文件描述符上的某个事件就绪
  • 0:epoll_wait调用后进行非阻塞等待,无论被监视的文件描述符上的事件是否就绪,epoll_wait检测后都会立即返回
  • 特定的时间值:epoll_wait调用后在指定的时间内进行阻塞等待,如果被监视的文件描述符上一直没有事件就绪,则在该时间后epoll_wait进行超时返回

返回值说明

  • 如果函数调用成功,则返回有事件就绪的文件描述符个数
  • 如果timeout时间耗尽,则返回0
  • 如果函数调用失败,则返回-1,同时错误码会被设置
    • epoll_wait调用失败时,错误码可能被设置为:
      • EBADF:传入的epoll模型对应的文件描述符无效
      • EFAULT:events指向的数组空间无法通过写入权限访问
      • EINTR:此调用被信号所中断
      • EINVAL:epfd不是一个epoll模型对应的文件描述符,或传入的maxevents值小于等于0

注意:和 select 和 poll 不同,epoll_wait 不需要遍历第三方数组或容器来检测哪些文件描述符上的事件已经就绪,epoll_wait 会将已经就绪的文件描述符上的事件其封装成 epoll_event 结构按顺序输出到缓冲区,具体个数就是返回值


epoll工作原理

之前poll和select的策略

数据从硬件拷贝到内核,操作系统在将其拷贝到进行相关的各种缓冲区中,此时数据的事件才算就绪,操作系统会顺便遍历检测下其他fd下的的数据是否就绪是否需要拷贝,然后CPU将进程从等待队列中唤醒,操作系统在通知上层哪些fd的那些事件已经就绪了,select和poll是这样的策略


epoll模型的: 红黑树和就绪队列

当某一进程调用epoll_create函数时,Linux内核会创建一个eventpoll结构体,也就是我们所说的epoll模型,eventpoll结构体当中的成员红黑树根节点rbr和就绪队列rdlist 与epoll的使用方式密切相关

struct eventpoll{
	...
	//红黑树的根节点,这棵树中存储着所有添加到epoll中的需要监视的事件
	struct rb_root rbr;
	//就绪队列中则存放着将要通过epoll_wait返回给用户的满足条件的事件
	struct list_head rdlist;
	...
}
  • epoll模型当中的红黑树本质就是告诉内核,需要监视哪些文件描述符上的哪些事件,调用epll_ctl函数实际就是在对这颗红黑树进行对应的增删改操作,
  • epoll模型当中的就绪队列本质就是告诉内核,哪些文件描述符上的哪些事件已经就绪了,调用epoll_wait函数实际就是在从就绪队列当中获取已经就绪的事件

在epoll中,对于每一个事件都会有一个对应的epitem结构体,红黑树和就绪队列当中的节点分别是基于epitem结构中的rbn成员和rdllink成员的,epitem结构当中的成员ffd记录的是指定的文件描述符值,event成员记录的就是该文件描述符对应的事件

struct epitem{
	struct rb_node rbn; //红黑树节点
	struct list_head rdllink; //双向链表节点
	struct epoll_filefd ffd; //事件句柄信息
	struct eventpoll *ep; //指向其所属的eventpoll对象
	struct epoll_event event; //期待发生的事件类型
}
  • 对于epitem结构当中rbn成员来说,ffd与event的含义是,需要监视ffd上的event事件是否就绪
  • 对于epitem结构当中的rdlink成员来说,ffd与event的含义是,ffd上的event事件已经就绪了

说明:

  • 红黑树是一种二叉搜索树,因此必须有键值key,而这里的文件描述符就天然的可以作为红黑树的key值,
  • 调用epoll_ctl向红黑树当中新增节点时,如果设置了EPOLLONESHOT选项,当监听完这次事件后,如果还需要继续监听该文件描述符则需要重新将其添加到epoll模型中,本质就是当设置了EPOLLONESHOT选项的事件就绪时,操作系统会自动将其从红黑树当中删除,
  • 而如果调用epoll_ctl向红黑树当中新增节点时没有设置EPOLLONESHOT,那么该节点插入红黑树后就会一直存在,除非用户调用epoll_ctl将该节点从红黑树当中删除,

回调机制

所有添加到红黑树当中的事件,都会与设备(网卡)驱动程序建立回调方法

  • 对于select和poll来说,操作系统在监视多个文件描述符上的事件是否就绪时,需要让操作系统主动对这多个文件描述符进行轮询检测,这一定会增加操作系统的负担
  • 而对于epoll来说,操作系统不需要主动进行事件的检测,当红黑树中监视的事件就绪时,会自动调用对应的回调方法,将就绪的事件添加到就绪队列当中
  • 当用户调用epoll_wait函数获取就绪事件时,只需要关注底层就绪队列是否为空,如果不为空则将就绪队列当中的就绪事件拷贝给用户即可

采用回调机制最大的好处,就是不再需要操作系统主动对就绪事件进行检测了,当事件就绪时会自动调用对应的回调函数进行处理


具体过程:

在操作系统将数据拷贝到缓冲区中后,执行回调机制:拿缓冲区内容和对应fd在就绪队列中新增一个节点,然后再唤醒进程,epoll_wait 就会检测这个就绪队列,再向上层通知就绪情况


说明一下:

  • 只有添加到红黑树当中的事件才会与底层建立回调方法,因此只有当红黑树当中对应的事件就绪时,才会执行对应的回调方法将其添加到就绪队列当中,
  • 当不断有监视的事件就绪时,会不断调用回调方法向就绪队列当中插入节点,而上层也会不断调用epoll_wait函数从就绪队列当中获取节点,这是典型的生产者消费者模型,
  • **由于就绪队列可能会被多个执行流同时访问,因此必须要使用互斥锁对其进行保护,**eventpoll结构当中的lock和mtx就是用于保护临界资源的,因此epoll本身是线程安全的,
  • eventpoll结构当中的wq(wait queue)就是等待队列,当多个执行流想要同时访问同一个epoll模型时,就需要在该等待队列下进行等待

简述分析:

调用epoll_create一定是一个进程,而进程会有一个关联的文件描述符数组,当我们创建一个 epoll 模型时,在模型中内核为我们维护一棵红黑树和就绪队列,红黑树节点中存储是用户让内核关注的fd和相关事件并携带其他的一些信息,

比如:

struct rb_tree_node {
	int fd;
    uint32_t events;
    struct rb_tree* left;
    struct rb_tree* right;
    enum color;
    //...
}

在用户向内核中注册fd和相关事件的时候,epoll 会触发底层相应的回调机制:维护一个就绪队列,队列节点中保存fd和就绪的事件,在操作系统将数据拷贝到缓冲区中后,执行回调机制:拿缓冲区内容和对应fd在就绪队列中新增一个节点,然后再唤醒进程,epoll_wait 就会检测这个就绪队列,再向上层通知就绪情况

image-20221006110828265

注意:红黑树、回调机制、就绪队列都是以文件的形式链接在进程相关文件结构中的,上层可直接用fd找到创建的 epoll 模型

image-20221006111006287

epoll三部曲

总结一下,epoll的使用过程就是三部曲:

  • 调用epoll_create创建一个epoll模型
    • 也就是存储组注册fd及其事件的红黑树、底层缓冲区内有数据后内核触发的回调机制以及保存就绪fd及其事件的就绪队列
  • 调用epoll_ctl,将要监控的文件描述符进行注册
    • 在红黑树中新增一个节点,对文件描述符及其事件进行增删改
    • 其实是对红黑树中的节点进行相应的增删改操作
    • 建立该新增描述符的回调策略,当底层有事件就绪的时候,这个回调方法会将发生的事件添加到就绪队列中
  • 调用epoll_wait,等待文件描述符就绪
    • 以O(1)的事件复杂度,检测是否有事件就绪
      • 检查是否有事件发生时:只需要检查队列是否有元素即可,如果队列不为空,则把就绪事件复制到用户态,同时将事件数量返回给用户

epoll服务器-*

这里我们实现一个简单的epoll服务器,该服务器也只是读取客户端发来的数据并进行打印,为了方便我们将套接字的接口封装在一个文件当中Sock.hpp

1)首先:我们需要指明epoll服务器的端口号,所以最好是使用命令行参数, 然后调用依次进行套接字的创建、绑定和监听,然后创建epoll模型

  • epoll服务器进行事件循环之前,需要先调用epoll_ctl将监听套接字添加到epoll模型当中,服务器刚开始运行时只需要监视监听套接字的读事件是否就绪(即:是否有链接到来)

2)进行事件循环,而epoll服务器要做的就是不断调用epoll_wait函数,从就绪队列当中获取就绪事件进行处理即可,假设epoll_wait函数的返回值为n

  • n > 0:说明已经有文件描述符的读事件就绪,并且epoll_wait函数的返回值代表的就是有事件就绪的文件描述符个数,接下来就应该对就绪事件进行处理
  • n == 0: 则说明timeout时间耗尽,此时直接准备进行下一次epoll_wait调用即可
  • n == -1 : 说明epoll_wait函数出错了,此时也让服务器准备进行下一次epoll_wait调用
    • 但实际应该进一步判断错误码,根据错误码来判断是否应该继续调用epoll_wait函数
#include"Sock.hpp"
#include<sys/epoll.h>
#include<iostream>
#include<string>

#define SIZE 128 
#define NUM 64 
static void Usage(std::string proc)
{
    std::cout << "Usage"<<proc<<"port" <<std::endl; 
}
//之后我们是这样启动程序的: ./epoll_server port
int main(int argc,char* argv[])
{
    if(argc!=2)
    {
        Usage(argv[0]);
        exit(1);
    }
    //建立套接字,完成绑定监听的操作
    uint16_t port = atoi(argv[1]);
    int listen_sock = Sock::Socket();
    Sock::Bind(listen_sock,port);
    Sock::Listen(listen_sock);

    //创建epoll模型
    int epfd = epoll_create(SIZE);

    //将监听套接字添加到epoll模型中,并关心其读事件
    struct epoll_event ev;
    ev.data.fd = listen_sock;
    ev.events = EPOLLIN;//我们关心的是读取事件是否就绪

    epoll_ctl(epfd,EPOLL_CTL_ADD,listen_sock,&ev);//向epoll模型当中注册关于listen_sock的事件

    //事件循环
    struct epoll_event revs[NUM];
    for(;;)
    {
        int timeout = -1;
        int n = epoll_wait(epfd,revs,NUM,timeout);
        switch(n)
        {
            case 0:
                std::cout << "time out ..." << std::endl;
                break;
            case -1:  
                std::cerr << "epoll error ..." << std::endl;
                break;
            default:   
                std::cout << "有事件就绪啦!" << std::endl;
                break;
        }
    }
    
    //最后不要忘记关闭监听套接字和epoll模型
    close(listen_sock);
    close(epfd);
    return 0;
}

注意:

1)默认情况下,只要底层有就绪事件,但是我们没有处理,参数epoll会一直通知用户,也就是调用epoll_wait会一直成功返回,并将就绪的事件拷贝到我们传入的数组当中

2)所谓的事件处理并不是调用epoll_wait将底层就绪队列中的就绪事件拷贝到用户层,比如当这里的读事件就绪后,我们应该调用accept获取底层建立好的连接,或调用recv读取客户端发来的数据,这才算是将事件处理了

3)如果我们仅仅是调用epoll_wait将底层就绪队列当中的事件拷贝到应用层,那么这些就绪事件实际并没有被处理掉,底层注册的回调函数会被再次调用,将就绪的事件重新添加到就绪队列当中,本质原因就是我们实际并没有对底层就绪的数据进行读取


事件处理

如果底层就绪队列当中有就绪事件,那么调用epoll_wait函数时就会将底层就绪队列中的事件拷贝到用户提供的revs数组当中,接下来epoll服务器就应该对就绪事件进行处理了,事件处理过程如下:

  • 根据epoll_wait时得到的返回值n: 来判断操作系统向我们传入的revs数组中拷贝了多少个struct epoll_event结构,进而对这些文件描述符上的事件进行处理
  • 对于每一个拷贝上来的struct epoll_event结构,如果该结构当中的events当中包含读事件,则说明该文件描述符对应的读事件就绪,但接下来还需要进一步判断该文件描述符是监听套接字还是与客户端建立的套接字
  • 如果是监听套接字的读事件就绪,则调用accept函数将底层建立好的连接获取上来,并调用epoll_ctl函数将获取到的套接字添加到epoll模型当中,表示下一次调用epoll_wait函数时需要监视该套接字的读事件
  • 如果是与客户端建立的连接对应的读事件就绪,则调用recv函数读取客户端发来的数据,并将读取到的数据在服务器端进行打印
    • 如果在调用recv函数时发现客户端将连接关闭或recv函数调用失败,则epoll服务器也直接关闭对应的连接
    • 并调用epoll_ctl函数将该连接对应的文件描述符从epoll模型中删除,表示下一次调用epoll_wait函数时无需再监视该套接字的读事件
      • 客户端进行了退出,服务器也需要关闭与这个客户端链接的文件描述符,因为曾经在内核当中注册了要关心这个文件描述符上的某些事件,所以还要在epoll模型当中把对这个文件描述符的关心的事项去掉,本质就是删除红黑树中对应的节点,并且去掉该文件描述符上底层建立好的回调机制,因为你曾经打开过链接,所以必须关闭,因为曾经添加过文件描述符,所以必须去掉它
#include"Sock.hpp"
#include<sys/epoll.h>
#include<iostream>
#include<string>

#define SIZE 128 
#define NUM 64 
static void Usage(std::string proc)
{
    std::cout << "Usage "<<proc<<" port " <<std::endl; 
}
//之后我们是这样启动程序的: ./epoll_server port
int main(int argc,char* argv[])
{
    if(argc!=2)
    {
        Usage(argv[0]);
        exit(1);
    }
    //建立套接字,完成绑定监听的操作
    uint16_t port = atoi(argv[1]);
    int listen_sock = Sock::Socket();
    Sock::Bind(listen_sock,port);
    Sock::Listen(listen_sock);

    //创建epoll模型
    int epfd = epoll_create(SIZE);

    //将监听套接字添加到epoll模型中,并关心其读事件
    struct epoll_event ev;
    ev.data.fd = listen_sock;
    ev.events = EPOLLIN;//我们关心的是读取事件是否就绪

    epoll_ctl(epfd,EPOLL_CTL_ADD,listen_sock,&ev);//向epoll模型当中注册关于listen_sock的事件

    //事件循环
    struct epoll_event revs[NUM]; //用于获取内核告诉用户已经就绪的事件
    for(;;)
    {
        int timeout = -1; //表示
        //这里传入的数组revs,仅仅是从内核中拿回来已经就绪的事件
        int n = epoll_wait(epfd,revs,NUM,timeout);
        switch(n)
        {
            case 0:
                std::cout << "time out ..." << std::endl;
                break;
            case -1:  
                std::cerr << "epoll error ..." << std::endl;
                break;
            default:   
                std::cout << "有事件就绪啦!" << std::endl;
                //因为已经把就绪事件按顺序整合在数组中,并且n就是已经就绪的事件个数
                for(int i = 0;i<n;i++)
                {
                    int sock = revs[i].data.fd;//获取已经就绪的文件描述符
                    std::cout << "文件描述符: " << sock << " 上面有事件就绪啦" << std::endl;
                    if(revs[i].events & EPOLLIN) //读取事件就绪
                    {
                        std::cout << "文件描述符: " << sock << " 读事件就绪" << std::endl;
                        //判断是监听套接字还是普通文件描述符上的读事件就绪
                        if(sock == listen_sock) //连接事件就绪
                        {
                            std::cout << "文件描述符: " << sock << " 链接数据就绪" << std::endl;
                            //处理链接事件
                            int fd = Sock::Accept(listen_sock);
                            if(fd>=0)
                            {
                                std::cout << "获取新链接成功啦: " << fd << std::endl;
                                //此时不能立即读取,因为有链接到来,并不代表该链接上有数据
                                //如果一旦读取,但是数据不就绪,此时就要被阻塞了进程就被挂起了
                                //做法:将当前新链接的文件描述符添加到epoll模型当中
                                struct epoll_event _ev;
                                _ev.events = EPOLLIN; //只关心读   如果想关心读和写: EPOLLIN | EPOLLOUT
                                _ev.data.fd = fd;
                                epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &_ev); //新的fd托管给了epoll!
                                std::cout << "已经将" << fd << " 托管给epoll啦" << std::endl;
                            }
                            else 
                            {
                                std::cout <<"获取新的链接失败"<<std::endl;
                                continue;
                            }
                        }
                        else //普通文件描述符上的读取事件就绪
                        {
                            std::cout << "文件描述符: " << sock << "正常数据就绪" << std::endl;
                            char buffer[1024] = {0};
                            //最后一个参数是0表示阻塞读取
                            //但是此时不会被阻塞,因为现在这个文件描述符上的读事件是就绪的
                            //少读取一个字符,因为我们把读到的内容当成字符串,最后位置放\0
                            ssize_t s = recv(sock,buffer,sizeof(buffer)-1,0);
                            if(s>0)
                            {
                                buffer[s] = '\0';
                                std::cout << "client [" << sock << "]# " << buffer << std::endl;
                            }
                            else if(s == 0) //对端关闭链接
                            {
                                std::cout << "client quit " << sock << std::endl;
                                close(sock);
                                //将当前文件描述符从epoll模型中删除
                                epoll_ctl(epfd, EPOLL_CTL_DEL, sock, nullptr);
                                std::cout << "sock: " << sock << "delete from epoll success" << std::endl;
                            }
                            else    //读取失败
                            {
                                std::cout << "recv error" << std::endl;
                                //将当前文件描述符从epoll模型中删除
                                close(sock); 
                                epoll_ctl(epfd, EPOLL_CTL_DEL, sock, nullptr);
                                std::cout << "sock: " << sock << "delete from epoll success" << std::endl;
                            }
                        }
                    }
                    else if(revs[i].events & EPOLLOUT) //写事件就绪
                    {}
                    else //...
                    {  }
                }
                break;
        }
    }

    //最后不要忘记关闭监听套接字和epoll模型
    close(listen_sock);
    close(epfd);
    return 0;
}


上述代码中:如果我们读完之后想处理写事件: 将我们的关心的事件更改成为``EPOLLOUT`

ssize_t s = recv(sock, buffer, sizeof(buffer) - 1, 0);
if (s > 0)
{
    buffer[s] = 0;
    std::cout << "client [" << sock << "]# " << buffer << std::endl;

    //假设读完之后想处理写事件: 将我们的关心时间更改成为EPOLLOUT
    //将当前新链接的文件描述符重新添加到epoll模型当中(相当于是修改)
    struct epoll_event _ev;
    _ev.events = EPOLLOUT;
    _ev.data.fd = sock;
    epoll_ctl(epfd, EPOLL_CTL_MOD, sock, &_ev);
}

服务器测试:

因为编写epoll服务器在调用epoll_wait函数时,我们将timeout的值设置成了-1,因此运行服务器后如果没有客户端发来连接请求,那么服务器就会在调用epoll_wait函数后进行阻塞等待

image-20221006171244205


当我们用telnet工具连接epoll服务器后,epoll服务器调用的epoll_wait函数在检测到监听套接字的读事件就绪后就会调用accept获取建立好的连接,然后把当前新连接的文件描述符放到epoll模型中

image-20221006171441653

下次循环时,当客户端发来的数据, epoll就能告知我们这个新连接的文件描述符读取事件就绪了, 也能够成功被epoll服务器收到并进行打印输出

image-20221006171506870


关于为什么新链接的文件描述符的值是5:

因为监听套接字的文件描述符值是3,然后epoll模型又占了一个位置,其值是4,所以现在最小的没有被使用的文件描述符值就是5


此外,我们这里编写的也是一个单进程的epoll服务器,但是它可以同时为多个客户端提供服务

image-20221006171721271

我们可以用ls /proc/PID/fd命令,查看当前epoll服务器的文件描述符的使用情况,其中文件描述符0、1、2是默认打开的,分别对应的是标准输入、标准输出和标准错误,3号文件描述符对应的是监听套接字,4号文件描述符对应的是服务器创建的epoll模型,5号和6号文件描述符对应的分别是正在访问服务器的两个客户端

image-20221006172841366


当服务器端检测到客户端退出后,也会关闭对应的连接,此时epoll服务器对应的5号和6号文件描述符就关闭了

image-20221006171841546

image-20221006172933956


epoll的优缺点

优点

1)接口使用方便:接口分离解耦,更方便高效,不需要重新设置fd及事件集合,做到输入输出参数分离

2)数据拷贝轻量:只在新增监视事件的时候调用epoll_ctl将数据从用户拷贝到内核,而select和poll每次都需要重新将需要监视的事件从用户拷贝到内核,此外,调用epoll_wait获取就绪事件时,只会拷贝就绪的事件,不会进行不必要的拷贝操作

3)事件回调机制:避免操作系统主动轮询检测事件就绪,而是采用回调函数的方式,将就绪的文件描述符结构加入到就绪队列中,调用epoll_wait时直接访问就绪队列就知道哪些文件描述符已经就绪,检测是否有文件描述符就绪的时间复杂度是O(1)

4)没有数量限制:监视的文件描述符数目无上限,只要内存允许,就可以一直向红黑树当中新增节点


注意: 有人说epoll中使用了内存映射机制,内核可以直接将底层就绪队列通过mmap的方式映射到用户态,此时用户就可以直接读取到内核中就绪队列当中的数据,避免了内存拷贝的额外性能开销,这种说法是否正确?

  • 这种说法是错误的,实际操作系统并没有做任何映射机制,因为操作系统是不相信任何人的,操作系统不会让用户进程直接访问到内核的数据的,用户只能通过系统调用来获取内核的数据
  • 因此用户要获取内核当中的数据,势必还是需要将内核的数据拷贝到用户空间

与select和poll的不同之处

1)需要明确得是:在使用select和poll时,都需要借助第三方数组来维护历史上的文件描述符以及需要监视的事件,这个第三方数组是由用户自己维护的,对该数组的增删改操作都需要用户自己来进行

而使用epoll时,不需要用户自己维护所谓的第三方数组,epoll底层的红黑树就充当了这个第三方数组的功能,并且该红黑树的增删改操作都是由内核维护的,用户只需要调用epoll_ctl让内核对该红黑树进行对应的操作即可

2)在使用多路转接接口时,数据流都有两个方向,一个是用户告知内核,一个是内核告知用户,select和poll将这两件事情都交给了同一个函数来完成,而epoll在接口层面上就将这两件事进行了分离,epoll通过调用epoll_ctl完成用户告知内核,通过调用epoll_wait完成内核告知用户


epoll工作方式

epoll的工作方式有两种,分别是LT方式(水平触发)和ET方式(边缘触发)这是epoll特有的模式概念,select和poll没有


水平触发(LT,Level Triggered)

特点:

  • 底层只要有事件就绪,只要不被取走就一直通知上层
  • 像数字电路当中的高电平触发一样,只要一直处于高电平,则会一直触发

image-20221006153324635

1)由于在LT工作模式下,只要底层有事件就绪就会一直通知用户,因此当epoll检测到底层读事件就绪时,可以不立即进行处理,或者只处理一部分,因为只要底层数据没有处理完,下一次epoll还会通知用户事件就绪

  • 所以:当epoll_wait返回后,可以不立刻进行处理或者只处理就绪事件的一部分,之后仍会通知事件就绪,然后再调用epoll_wait,直到缓冲区的所有数据都被处理,

2)LT模式支持阻塞读写和非阻塞读写 select,poll,epoll的状态默认就是LT模式


边缘触发(ET,Edge Triggered)

特点:

  • 只有底层就绪事件数量由无到有或由有到多发生变化的时候,epoll才会通知用户,并且只通知这一次
  • 就像数字电路当中的上升沿触发一样,只有当电平由低变高的那一瞬间才会触发

image-20221006153347129

因为epoll的状态默认就是LT模式,如果想要将epoll改为ET工作模式:

  • 则需要在添加事件时设置EPOLLET选项,

image-20221006153020158

1)由于在ET工作模式下,只有底层就绪事件无到有或由有到多发生变化的时候才会通知用户,因此当epoll检测到底层读事件就绪时,必须立即进行处理,而且必须全部处理完毕,因为有可能此后底层再也没有事件就绪,那么epoll就再也不会通知用户进行事件处理,此时没有处理完的数据就相当于丢失了

2)ET工作模式下epoll通知用户的次数一般比LT少,因此ET的性能一般比LT性能更高,Nginx就是默认采用ET模式使用epoll的

3)ET模式必须非阻塞的读写

ET工作模式下应该如何进行读写

在ET工作模式下,只有底层就绪事件无到有或由有到多发生变化的时候才会通知用户,这就倒逼用户当读事件就绪时必须一次性将数据全部读取完毕,当写事件就绪时必须一次性将发送缓冲区写满,否则可能再也没有机会进行读写了

因此读数据时必须循环调用recv函数进行读取,写数据时必须循环调用send函数进行写入

  • 当底层读事件就绪时,循环调用recv函数进行读取,直到某次调用recv读取时,实际读取到的字节数小于期望读取的字节数,则说明本次底层数据已经读取完毕了
  • 但有可能最后一次调用recv读取时,刚好实际读取的字节数和期望读取的字节数相等,但此时底层数据也恰好读取完毕了,但是我们并不知道数据已经读取完毕了,所以还会继续进行读取,此时我们再调用recv函数进行读取,那么recv就会因为底层没有数据而被阻塞住
  • 而这里的阻塞是非常严重的,就比如我们这里写的服务器都是单进程的服务器,如果recv被阻塞住,并且此后该数据再也不就绪,那么就相当于我们的服务器挂掉了,因此在ET工作模式下循环调用recv函数进行读取时,必须将对应的文件描述符设置为非阻塞状态
  • 调用send函数写数据时也是同样的道理,需要循环调用send函数进行数据的写入,并且必须将对应的文件描述符设置为非阻塞状态

强调: ET工作模式下,recv和send操作的文件描述符必须设置为非阻塞状态,这是必须的,不是可选的


对比LT和ET

  • 在ET模式下**,一个文件描述符就绪之后,用户不会反复收到通知,看起来比LT更高效**
    • 但如果在LT模式下能够做到每次都将就绪的文件描述符立即全部处理,不让操作系统反复通知用户的话,其实LT和ET的性能也是一样的
  • 此外,ET的编程难度比LT更高

水平触发LT像是一个尽职的快递员,只要你不去取就会一直在提醒你,边缘触发ET干活很随便,只为了完成任务通知一次,之后爱来不来爱取不取

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/528352.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

SpringBoot【开发实用篇】---- 整合第三方技术(缓存)

SpringBoot【开发实用篇】---- 整合第三方技术&#xff08;缓存&#xff09; SpringBoot内置缓存解决方案手机验证码案例SpringBoot整合Ehcache缓存SpringBoot整合Redis缓存SpringBoot整合Memcached缓存SpringBoot整合jetcache缓存纯远程方案纯本地方案本地远程方案远程方案的数…

tomcat控制台打印乱码解决

一、注册表修改 HKEY_CURRENT_USER ->console ->tomcate 新增 32位 CodePage 16进制 fde9 二、idea 中配置 Tomcat 后启动服务&#xff0c;输出打印日志乱码问题 解决办法&#xff1a; ①、打开安装idea文件路径&#xff0c;在bin目录下&#xff0c;找到下面两个文件 ②…

图像动态裁剪

1. 背景 以两级级联模型为例&#xff0c;第一级目标检测模型用于检测人员&#xff0c;第二级目标检测模型用于检测手机、对讲机等。然后实际数据采集过程中&#xff0c;手机、对讲机这些设备并不在人员的一级检测框内&#xff0c;使得二级模型训练的样本较少。 二级目标检测模…

详细讲解,接口自动化—Requests之Cookie鉴权关联接口实战

目录 前言&#xff1a; 一、 简介 二、 实战操作 1. 登录接口 2. 查询订单接口 3. 新增订单接口 4. 修改订单接口 5. 删除订单接口 三、 结束语 前言&#xff1a; 接口自动化测试是软件测试过程中的重要一环&#xff0c;现在越来越多的公司开始使用自动化测试来提高测…

Gigabyte Z490 Vision D i9-10900k电脑 Hackintosh 黑苹果efi引导文件

原文来源于黑果魏叔官网&#xff0c;转载需注明出处。&#xff08;下载请直接百度黑果魏叔&#xff09; 硬件型号驱动情况 主板Gigabyte Z490 Vision D 处理器Intel i9-10900k已驱动 内存64GB G.Skill Trident Z 3600Mhz CL18已驱动 硬盘西数 WDS250G3X0C-00SJG0 ( SN750) …

cad文件怎么转换成pdf格式?一键操作的4个方法

在很多时候&#xff0c;我们为了能够更好地查看CAD图纸&#xff0c;需要将其格式转换为PDF。所以说&#xff0c;CAD文件格式的转换是非常关键的。首先&#xff0c;将CAD转换为PDF格式能够有效提升文件的兼容性。CAD软件通常需要特定的软件才能打开和编辑&#xff0c;而PDF格式则…

Python Scrapy爬虫框架安装和创建

1、检查Win环境 python版本 python 2、whl方式安装 twisted twisted异步网络框架&#xff0c;可加快下载速度。优点是用少量的代码实现快速的抓取。 由于scrapy需要twisted的环境&#xff0c;我们直接去下载whl文件根据自己的Python版本选择 https://www.lfd.uci.edu/~gohlke/p…

由浅入深理解java集合(五)——集合 Map

HashMap 前面已经介绍完了Collection接口下的集合实现类&#xff0c;今天我们来介绍Map接口下的两个重要的集合实现类HashMap,TreeMap。 HashMap 是一个散列表&#xff0c;它存储的内容是键值对(key-value)映射。 既然要介绍HashMap&#xff0c;那么就顺带介绍HashTable,两者进…

【UE4】部署像素流

目录 一、单实例本地像素流送 步骤 1. 勾选插件 2. 打包工程并启动信令服务器 3. 创建快捷方式并启动游戏 二、单实例局域网像素流送 步骤 1. 编辑cirrus.js 2. 编辑快捷方式属性 3. 启动 一、单实例本地像素流送 步骤 1. 勾选插件 勾选使用“Pixel Streaming”插件&…

瑞吉外卖 - 新增员工功能(6)

某马瑞吉外卖单体架构项目完整开发文档&#xff0c;基于 Spring Boot 2.7.11 JDK 11。预计 5 月 20 日前更新完成&#xff0c;有需要的胖友记得一键三连&#xff0c;关注主页 “瑞吉外卖” 专栏获取最新文章。 相关资料&#xff1a;https://pan.baidu.com/s/1rO1Vytcp67mcw-PD…

智慧水务管控一体化平台,实现水务数字化管理

平台概述 柳林智慧水务管控一体化平台是以物联感知技术、大数据、智能控制、云计算、人工智能、数字孪生、AI算法、虚拟现实技术为核心&#xff0c;以监测仪表、通讯网络、数据库系统、数据中台、模型软件、前台展示、智慧运维等产品体系为支撑&#xff0c;以城市水资源、水生…

ArcSWAT报错:数据集未投影;Dataset must have a projected coordinate system

文章目录 1 报错内容2 定义投影3 重新执行ArcSWAT相关步骤 1 报错内容 Dataset must have a projected coordinate system. The current coordinate system is geographic . Please define a projected coordinate system for your DEM dataset using ArcToolbox before procee…

Java 线程池(Thread Pools)详解

目录 1、线程池介绍 2、线程池执行原理 3、线程池中的阻塞队列 4、Java 线程池中的拒绝策略 5、Java 提供的创建线程池的方式 6、线程池的使用示例 7、ForkJoinPool 和 ThreadPool 的区别 1、线程池介绍 线程池是一种重用线程的机制&#xff0c;用于提高线程的利用率和管…

Android开发:我们很迷茫,出路在哪里?

“都说今年是互联网行业寒风刺骨&#xff0c;尤其移动端开发市场更是饱和&#xff0c;在跌跌撞撞近一个月后&#xff0c;我终于在一家小公司找到了工作。入职后&#xff0c;领导让我接手一个二手Android项目&#xff0c;项目很庞大&#xff0c;前任开发人员已离职一个多月了&am…

实现 Kubernetes 安全态势管理

Kubernetes 已经成为容器编排的事实标准。它引入了强大的管理功能&#xff0c;但也带来了一些严峻的安全挑战——尤其是在多云环境中。其中包括缺乏对设置的可见性、镜像的滥用、通信故障和监控困难。 理解 K8s 的安全挑战 Kubernetes 挑战的核心是需要以高度协调的方式管理大…

日撸 Java 三百行day51

文章目录 说明Day51 KNN 分类器1.KNN2.代码1.aff内容解读2.代码理解 说明 闵老师的文章链接&#xff1a; 日撸 Java 三百行&#xff08;总述&#xff09;_minfanphd的博客-CSDN博客 自己也把手敲的代码放在了github上维护&#xff1a;https://github.com/fulisha-ok/sampledat…

静电防护:消除静电的秘诀!

随着现代科技的进步&#xff0c;人们对静电防护越来越重视。有的人认为消除静电是不可能做到的事情&#xff0c;但实际上并不是这样的&#xff01; 1&#xff1a;静电的产生 静电是一个非常普遍的现象&#xff0c;通常发生在5 kV电压下。静电可以产生于物体表面或环境中。如果…

电视盒子哪个牌子好?博主力荐2023目前性能最好的电视盒子

电视盒子能让电视机在不换新的前提下丰富资源、升级配置&#xff0c;是电视机的最佳拍档&#xff0c;但面对这么多的品牌让大家在选购时都会疑惑电视盒子哪个牌子好&#xff0c;博主老周盘点了目前性能最好的电视盒子&#xff0c;具体是哪些品牌呢&#xff1f;请看下文&#xf…

CMU-CERT内部威胁数据集 Insider Threat

CMU-CERT内部威胁数据集 Insider Threat CMU-CERT简介CMU-CERT版本CMU-CERT r1版本内容logon.csv内容decive.csv内容HTTP.csv内容LDAP and Administrative records勘误一些已知的缺陷 CMU-CERT网站 CMU-CERT简介 首先解释一下CMU-CERT是什么意思。 “CMU”是卡内基梅隆大学&a…

专业的Web自动化测试工具拥有哪些特点?

Web自动化测试是为了解决Web应用程序测试工程师在测试过程中的挑战和复杂性而实施的&#xff0c;可以通过自动化测试工具来实现。自动化测试工具是一种软件&#xff0c;其目的在于自动执行测试&#xff0c;提高测试效率和测试准确性&#xff0c;那专业的Web自动化测试工具拥有哪…