多路转接(中)——poll和epoll

目录

一、poll

1.select的缺点

2.认识poll系统调用

3.poll的优点

二、poll服务器

三、epoll

1.poll的缺点

2.认识epoll的三个接口

3.epoll的原理

四、epoll服务器


一、poll

1.select的缺点

select虽然可以增加IO的效率,但是它有两个问题:

(1)等待fd有上限

fd_set类型的大小为128字节,每个字节有8个比特位,所以fd_set类型能包含1024个比特位。它最多能监视1024个文件描述符,在高访问量的服务器中是严重不足的。

(2)每次调用都需要重新设置fd_set。

每次调用select都需要重新设置fd_set,而且读写异常三种事件每个都需要维护一个变量,而且程序员也要维护相应的数组,增加了我们维护的成本。

所以为了解决select的问题,我们引入了另一种多路转接的方案poll。

2.认识poll系统调用

(1)poll函数声明

int poll(struct pollfd* fds, nfds_t nfds, int timeout);

头文件:poll.h

功能:poll负责IO中多个描述符等的那部分,该函数会在描述符的读写异常事件就绪时,提醒进程进行处理。

参数:后面讲。

返回值:返回值大于0表示有相应个文件描述符就绪,返回值等于0表示没有文件描述符就绪,超时返回,返回值小于0表示poll调用失败。

(2)struct pollfd* fds

它是一个struct pollfd类型的类指针,我们传参需要传入一个该类型的数组。

struct pollfd结构体定义如下:

struct pollfd
{
    int fd;
    short events;
    short revents;
};

struct pollfd结构体中存在三个成员变量。

第一个是fd,表示需要操作系统等待的文件描述符。第二个是short events,表示该描述符需要poll等待该fd的事件类型,也就是读事件,写事件,异常事件。第三个是short revents,可以告知用户该描述符的哪个事件就绪了。

此时我们需要关心文件描述符的哪个事件,就直接将事件设置到struct pollfd结构体中的short events。struct pollfd结构体将用户和操作系统设置的字段分开了,所以就不存在相互干扰的问题,也更不需要位图结构了。

当指定文件描述符的事件就绪时,操作系统会设置其结构体的short revents,用户直接读取这个字段便知道是哪个事件就绪,并进行相应处理了。

event和revent有以下参数可选择:

其中最常用的是POLLIN和POLLOUT。

POLLIN:是否可以从操作系统中读数据

POLLOUT:是否可以向操作系统写数据(写的本质是操作系统的缓冲区有空间让你写数据,如果没有,写事件不会就绪)

而且这些events和revents可以使用按位或接收多个宏,比方说,你要关心一个fd的读和写事件,你就可以写events = EPOLLIN | EPOLLOUT,这样就实现了多个事件的传递。

(3)nfds_t nfds

nfds表示fds数组的长度。

(4)int timeout

timeout表示poll函数的超时时间,单位是毫秒(ms),如果是0表示没有timeout时间,为非阻塞状态,如果为-1表示永久阻塞状态。

3.poll的优点

pollfd结构包含了要监视的events和发生的revents,不再使用select“参数值”传递,使用比select更方便。

poll在设计上没有监视的最大数量限制,但是太多也会导致运行过慢。

二、poll服务器

poll服务器与我们之前实现的select服务器的整体架构一致。

err.hpp、log.hpp、sock.hpp三个头文件可以直接使用,server.cc做少量修改,再根据之前的selectserver.hpp做一定修改实现一个pollserver.hpp,就可以运行了。

pollserver.hpp

#pragma once
#include<iostream>
#include<poll.h>
#include<string>
#include<functional>
#include"sock.hpp"

namespace poll_func
{
    static const int default_port = 8080;//默认端口号为8080
    static const int num = 2048;//设置元素数为2048,当然poll可监视的文件描述符无上限,也可以设计令其动态增长
    static const int default_fd = -1;//将所有需要管理的文件描述符放入一个数组,-1是数组中的无效元素

    using func_t = std::function<std::string (const std::string&)>;

    class PollServer
    {
    public:
        PollServer(func_t func, int port = default_port)
            :_listensock(-1)
            ,_port(port)
            ,_fds(nullptr)
            ,_func(func)
        {}

        ~PollServer()
        {
            if(_listensock > 0)
                close(_listensock);//关闭监听文件描述符
            if(_fds)
                delete []_fds;//释放存储文件描述符的数组
        }

        //对数组内的某个结构体进行初始化
        void reset_item(int i)
        {
            _fds[i].fd = default_fd;
            _fds[i].events = 0;
            _fds[i].revents = 0;
        }

        void initserver()
        {
            //创建listen套接字,绑定端口号,设为监听状态
            _listensock = Sock::Socket();
            Sock::Bind(_listensock, _port);
            Sock::Listen(_listensock);

            //构建一个结构体数组
            _fds = new struct pollfd[num];
            //初始化所有数据
            for(int i = 0; i<num; ++i)
            {
                reset_item(i);
            }
            //第一个是监听描述符,对其初始化
            _fds[0].fd = _listensock;
            _fds[0].events = POLLIN;
        }

        void start()
        {
            int timeout = -1;
            while(1)
            {
                //调用poll
                int n = poll(_fds, num, timeout);//timeout为-1表示阻塞调用,大于0的值表示这段时间内阻塞
                switch(n)
                {
                    case 0://没有描述符就绪
                        logmessage(NORMAL, "time out.");
                        break;
                    case -1://select出错了
                        logmessage(ERROR, "select error, error code:%d %s", errno, strerror(errno));
                        break;
                    default://有描述符就绪(获取链接就属于读就绪)
                        logmessage(NORMAL, "server get new tasks.");
                        handler_read();//处理数据
                        break;
                }
            }
        }

        void Accepter()
        {
            //走到这里说明等的过程select已经完成了
            std::string clientip;
            uint16_t clientport = 0;
            //poll只负责等,接收链接还是需要accept,但是这次调用不会阻塞了
            int sock = Sock::Accept(_listensock, &clientip, &clientport);
            if (sock < 0)//接收出错不执行
                return;
            logmessage(NORMAL, "accept success [%s:%d]", clientip.c_str(), clientport);

            //链接已经被建立,新的描述符通信产生
            //这个描述符我们也要再次插入数组
            int i = 0;
            for(i = 0; i<num; ++i)
            {
                if(_fds[i].fd == default_fd) 
                    break;
            }
            if(i == num)//数组满了
            {
                logmessage(WARNING, "server if full, please wait");
                close(sock);//关闭该链接
            }
            else
            {
                //将数据插入数组
                _fds[i].fd = sock;
                _fds[i].events = POLLIN;
                _fds[i].revents = 0;
            }
            print_list();//打印数组的内容
        }

        void Receiver(int pos)
        {
            //接收客户端发来的数据
            char buffer[1024];
            ssize_t n = recv(_fds[pos].fd, buffer, sizeof(buffer)-1, 0);
            if (n > 0)
            {
                buffer[n] = 0;//在末尾加上/0
                logmessage(NORMAL, "client# %s", buffer);
            }
            else if (n == 0)
            {
                close(_fds[pos].fd);
                reset_item(pos);
                logmessage(NORMAL, "client quit");
                return;
            }
            else
            {
                close(_fds[pos].fd);
                reset_item(pos);
                logmessage(ERROR, "client quit: %s", strerror(errno));
                return;
            }

            //使用回调函数处理数据
            std::string response = _func(buffer);

            //发回响应
            write(_fds[pos].fd, response.c_str(), response.size());
        }

        void handler_read()
        {
            //我们将读取数据的处理分为两种:
            //第一种是获取到了新链接
            //第二种是有数据需要被读取
            for(int i = 0; i<num; ++i)
            {
                //筛选出有效的文件描述符
                if(_fds[i].fd != default_fd)
                {
                    //筛选出events为POLLIN的结构体
                    if(_fds[i].events & POLLIN)
                    {
                        if (_fds[i].fd == _listensock && (_fds[i].revents & POLLIN))//监听套接字存在了
                            Accepter();
                        else if(_fds[i].revents & POLLIN)
                            Receiver(i);
                    }
                }
            }
        }

        void print_list()
        {
            std::cout << "fd list:" << std::endl;
            for(int i = 0; i<num; ++i)
            {
                if(_fds[i].fd != default_fd)
                    std::cout << _fds[i].fd << " ";
            }
            std::cout << std::endl;
        }

    private:
        int _listensock;
        int _port;
        struct pollfd* _fds;
        func_t _func;
    };
}

server.cc

#include"pollserver.hpp"
#include"err.hpp"
#include<memory>

using namespace std;
using namespace poll_func;

static void Usage(std::string proc)
{
    std::cerr << "Usage:\n\t" << proc << " port" << "\n\n";
}

string transaction(const string& str)
{
    return str;
}

int main(int argc, char *argv[])
{
    unique_ptr<PollServer> p(new PollServer(transaction));
    p->initserver();
    p->start();
    return 0;
}

三、epoll

1.poll的缺点

poll虽然允许我们监视没有上限的描述符,但它也有自己的缺点。

(1)和select函数一样,poll返回后,也需要以遍历的方式轮询获取就绪的描述符。

(2)每次调用poll都需要把大量的struct pollfd结构从用户态拷贝到内核中。

(3)同时连接的大量客户端在同一时刻可能只有很少的处于就绪状态,因此随着监视描述符数量的增长,poll的效率也会线性下降。

为了解决poll的问题,我们又引入了epoll的三个接口。

2.认识epoll的三个接口

(1)epoll_create

int epoll_create(int size);

头文件:sys/epoll.h

功能:创建epoll模型。

参数:int size该值必须大于0,自Linux2.6.8(我们目前使用Linux的版本都高于该版本)以后,该参数事实上是被忽略的,不起实际作用。

返回值:成功返回epoll模型的文件描述符,失败返回-1并设置错误码。

(2)epoll_ctl

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

头文件:sys/epoll.h

功能:修改epoll句柄的属性。

参数:int epfd该值是epoll模型的描述符,int op表示修改句柄的选项,int fd要操作的文件描述符,struct epoll_event* event是用于epoll中的描述符属性结构体指针。

返回值:成功返回epoll模型的文件描述符,失败返回-1并设置错误码。

struct epoll_event结构体及其内部的struct epoll_data联合体在内核中的定义如下:

typedef union epoll_data{
    void *ptr;
    int fd;
    uint32_t u32;
    uint64_t u64;
} epoll_data_t;

struct epoll_event{
    uint32_t events;
    epoll_data_t data;
}

struct epoll_event结构体有两个成员变量。第一个成员是uint32_t events,用来设置需要等待的事件,它也有相关的宏,而且也支持按位或。

宏定义如下:

含义

EPOLLIN

表示对应的文件描述符可以读 (包括对端SOCKET正常关闭)

EPOLLOUT

表示对应的文件描述符可以写

EPOLLPRI

表示对应的文件描述符有紧急的数据可读 (这里应该表示有带外数据到来)

EPOLLERR

表示对应的文件描述符发生错误

EPOLLHUP

表示对应的文件描述符被挂断

EPOLLET

将EPOLL设为边缘触发(Edge Triggered)模式, 这是相对于水平触发(Level Triggered)来说的

EPOLLONESHOT

只监听一次事件, 当监听完这次事件之后, 如果还需要继续监听这个socket的话, 需要再次把这个socket加入到EPOLL队列里

第二个参数是一个联合体epoll_data_t data,可以看到有四个成员共用这个联合体。

(3)epoll_wait

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

头文件:sys/epoll.h

功能:从epoll模型中获取被等待的文件描述符的状态。

参数:int epfd该值是epoll模型的描述符,struct epoll_event* events是一个数组,epoll_wait会将就绪的文件描述符的结构体放入这个数组中,供用户层读取,int maxevents是events数组的大小,这个值不能大于epoll_create的size,int timeout和poll中是一样的,不解释了。

返回值:大于0表示就绪的文件描述符个数,等于0表示超时返回,小于0表示调用失败。

3.epoll的原理

(1)描述符就绪的本质

我们以数据接收的读事件为例。

网络通信的接收端会将数据从网卡(物理层)逐层向上交付,最后到达应用层。那么接收端的操作系统是怎么知道数据到了网卡里的?

计算机遵循冯诺依曼体系,我们再增加中断向量表一起讲解。

当网卡接收到数据后,作为外设的网卡会产生一个控制信号并发给CPU的控制器。这样的外设给CPU发送一个信号表示数据到来,这叫做中断事件发生。

冯诺依曼体系中,外设的数据信号不能直接传递到CPU中,必须经过存储器(红线);而外设的控制信号可以直接传递给CPU的控制器(黑线)。

CPU收到信号后,会根据中断信号的编号,去操作系统维护的中断向量表中找到对应的中断服务函数并且执行。中断服务函数会调用网卡接收数据的驱动程序,读取数据并且向上层交付(绿线)。

在这里要重点关注中断服务函数,从网卡中接收数据是从它开始的。

(2)epoll模型的执行

epoll模型理论图包含计算机体系结构中的驱动,操作系统,系统调用三部分。

在调用epoll_create创建模型后,会返回一个文件描述符fd,这个fd同样放在服务器进程PCB所维护的进程描述符表中,通过fd这个句柄就可以找到对应的epoll模型。

epoll模型也是一个复杂的大结构体。由于在Linux中一切皆文件,所以创建模型后返回的也是一个文件描述符。

下图操作系统红框中黑框内的部分就是epoll模型,它维护了一个红黑树和一个就绪队列。

当我们需要增加需要等待的文件描述符时,可以调用epoll_ctl,将fd以及要关心的事件构建成一个struct epoll_event变量插入红黑树。红黑树的key值是文件描述符,value值能找到对应struct epoll_event变量。

如果是删除或者修改数据,同样是在修改红黑树,而红黑树查找效率非常高,所以相比poll的遍历,其操作会很高效。

当操作系统发现红黑树中有节点的事件就绪后,就会将该节点放入到就绪队列中,就绪队列本质是一个双向循环链表。

节点从红黑树放入就绪队列的过程并没有发生拷贝,我们操作系统中的某个数据结构中的节点往往也是其他数据结构的节点。比方说,我们增加一些指针变量等数据,红黑树的一个节点,也能成为链表中的一个节点。

当网卡中有数据到来时,通过中断函数最终会调用网卡驱动程序。驱动程序中有一个操作系统提供的回调函数void* private_data。

private_data回调函数会将红黑树节点中的next和prev指针的指向关系做修改,让该节点链入就绪队列中去,也就是图中的struct epoll_event结构,凡是处于就绪队列中的节点必然有事件已经就绪。

用户层在调用epoll_wait后,就能获取内核中就绪队列的内容,所以用户层获取到的struct epoll_event数组中,是当前就绪全部事件。

从内核到用户层虽然也需要遍历,但是此时是遍历拷贝,不再需要遍历检测。所以时间复杂度由O(N)变成了O(1),效率大大提升。

四、epoll服务器

epoll服务器与我们之前实现的select、poll服务器的整体架构一致。

err.hpp、log.hpp、sock.hpp三个头文件依旧直接使用,server.cc做少量修改,再根据之前的pollserver.hpp做一定修改实现一个epollserver.hpp,就可以运行了。

epollserver.hpp

#pragma once
#include<iostream>
#include<string>
#include<string.h>
#include<sys/epoll.h>
#include<functional>
#include"err.hpp"
#include"log.hpp"
#include"sock.hpp"

namespace epoll_func
{
    static const int default_port = 8080;//默认端口号为8080
    static const int size = 128;//epoll模型可容纳结构体的数目
    static const int default_fd = -1;//将所有需要管理的文件描述符放入一个数组,-1是数组中的无效元素
    static const int default_num = 64;//

    using func_t = std::function<std::string (const std::string&)>;

    class EpollServer
    {
    public:
        EpollServer(func_t func, int port = default_port, int num = default_num, int fd = default_fd)
            :_port(port)
            ,_listensock(fd)
            ,_epfd(fd)
            ,_revs(nullptr)
            ,_num(num)
            ,_func(func)
        {}

        ~EpollServer()
        {
            if(_listensock > 0)
                close(_listensock);//关闭监听文件描述符
            if(_epfd > 0)
                close(_epfd);//关闭epoll模型的文件描述符
            if(_revs)
                delete []_revs;//释放存储文件描述符的数组
        }

        void initserver()
        {
            //创建listen套接字,绑定端口号,设为监听状态
            _listensock = Sock::Socket();
            Sock::Bind(_listensock, _port);
            Sock::Listen(_listensock);

            //创建epoll模型
            _epfd = epoll_create(size);
            if(_epfd < 0)
            {
                //创建epoll模型失败
                logmessage(FATAL, "epoll create error: %s", strerror(errno));
                exit(EPOLL_CREATE_ERROR);
            }

            //建立listen套接字的epoll_event结构体
            struct epoll_event ev;
            ev.events = EPOLLIN;
            ev.data.fd = _listensock;
            //将该数据加入epoll模型
            epoll_ctl(_epfd, EPOLL_CTL_ADD, _listensock, &ev);

            //申请就绪事件的空间
            _revs = new struct epoll_event[_num];

            //初始化完成,打印提示
            logmessage(NORMAL, "init server success");
        }

        void start()
        {
            //timeout为-1表示阻塞调用
            int timeout = -1;
            while(1)
            {
                int n = epoll_wait(_epfd, _revs, _num, timeout);
                //epoll_wait将所有就绪的节点放入_revs数组中,n为元素个数
                switch(n)
                {
                    case 0://没有描述符就绪
                        logmessage(NORMAL, "time out.");
                        break;
                    case -1://select出错了
                        logmessage(ERROR, "epoll_wait error, error code:%d %s", errno, strerror(errno));
                        break;
                    default://有描述符就绪(获取链接就属于读就绪)
                        logmessage(NORMAL, "server get new tasks.");
                        handler_event(n);//处理数据
                        break;
                }
            }
        }

        void handler_event(int n)
        {
            logmessage(DEBUG, "handler_event in");
            for(int i = 0; i<n; ++i)
            {
                //将events和fd从结构体中拿出来
                uint32_t events = _revs[i].events;
                int sock = _revs[i].data.fd;

                //链接事件的处理
                if(sock == _listensock && (events & EPOLLIN))
                {
                    //listen套接字的读事件就绪
                    std::string clientip;
                    uint16_t clientport;
                    int fd = Sock::Accept(_listensock, &clientip, &clientport);//接收链接,获取各个输出型参数
                    if(fd < 0)
                    {
                        logmessage(ERROR, "Accept error");
                        continue;//再次接收
                    }

                    //链接建立完成,但我们还需要关心新建立的链接的读事件
                    //所以还需要在epoll模型中添加新套接字
                    //建立listen套接字的epoll_event结构体
                    struct epoll_event ev;
                    ev.events = EPOLLIN;
                    ev.data.fd = fd;
                    //将该数据加入epoll模型
                    epoll_ctl(_epfd, EPOLL_CTL_ADD, fd, &ev);
                }
                else if(events & EPOLLIN)
                {
                    //普通的读事件就绪
                    char buffer[1024];
                    //读取数据
                    ssize_t n = read(sock, buffer, sizeof(buffer)-1);
                    if(n > 0)//读到了数据
                    {
                        buffer[n] = '\0';//将/n替换为/0
                        logmessage(NORMAL, "client# %s", buffer);
                        //使用回调函数处理数据
                        std::string resp = _func(std::string(buffer));
                        //发回数据
                        send(sock, resp.c_str(), resp.size(), 0);
                    }
                    else if(n == 0)//读到了结尾
                    {
                        //将sock从epoll模型中移除
                        epoll_ctl(_epfd, EPOLL_CTL_DEL, sock, nullptr);
                        //关闭文件描述符
                        close(sock);
                        logmessage(NORMAL, "client quit");
                        //注意移除和关闭描述符不能交换顺序
                        //因为先关闭描述符,该套接字对应的结构体就会从epoll模型中移除,再次移除会出错
                    }
                    else//出错了
                    {
                        //将sock从epoll模型中移除
                        epoll_ctl(_epfd, EPOLL_CTL_DEL, sock, nullptr);
                        //关闭文件描述符
                        close(sock);
                        //打印错误码
                        logmessage(ERROR, "recv error, code: %d, errstring: %s", errno, strerror(errno));
                    }
                }
                else//还可以增加处理写时间的代码,我这里只处理读事件,就不写这些代码了
                {}
            }
            logmessage(DEBUG, "handler_event out");
        }

    private:
        uint16_t _port;
        int _listensock;
        int _epfd;
        struct epoll_event *_revs;
        int _num;
        func_t _func;
    };
}

server.cc

#include"epollserver.hpp"
#include<memory>

using namespace std;
using namespace epoll_func;

string echo(const string& message)
{
    return message;    
}
void Usage(string proc)
{
    std::cerr << "Usage:\n\t" << proc << " port" << "\n\n";
}

int main(int argc, char* argv[])
{
    if(argc != 2)
    {
        Usage(argv[0]);
        exit(USAGE_ERROR);
    }
    uint16_t port = atoi(argv[1]);
    unique_ptr<EpollServer> p(new EpollServer(echo, port));
    p->initserver();
    p->start();
    return 0;
}

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

高级算法复习

时间代价 主定理 递归树 排序 贪心算法 贪心选择性&#xff08;Greedy-choice property&#xff09;&#xff1a; 通过做出局部最优&#xff08;贪婪&#xff09;选择&#xff0c;可以得出全局最优解——这是贪心算法可行的第一个基本要素&#xff0c;也是贪心算法与动态规划…

使用 Redis 实现生成分布式全局唯一ID(使用SpringBoot环境实现)

目录 一、前言二、如何通过Redis设计一个分布式全局唯一ID生成工具2.1、使用 Redis 计数器实现2.2、使用 Redis Hash结构实现 三、通过代码实现分布式全局唯一ID工具3.1、编写获取工具3.2、测试获取工具 四、总结 一、前言 在很多项目中生成类似订单编号、用户编号等有唯一性数…

创建云端服务器

1.申请云端服务器 每个账户有三个月的免费试用 我的服务器选择是centos7 &#xff0c;别选成win了。 2.创建实例 创建实例的步骤&#xff0c;阿里云有文档 介绍 大致就是 左边点实例 -》 顶部选你申请服务器时的地区-》下面就出现一条实例-》点更多 -》要重置实例密码 -》同一…

Docker安装ewomail

ewomail相关链接 官网官方安装文档gitee 开始安装 快速安装 wget -c https://down.ewomail.com/install-03.sh && sh install-03.sh 域名docker安装 创建docker容器 docker run -idt \-p 25:25 \-p 110:110 \-p 143:143 \-p 465:465 \-p 587:587 \-p 993:993 \-…

【带头学C++】----- 三、指针章 ---- 3.10 函数指针(补充基础知识)

1.函数指针 1.1 函数的返回值类型为指针类型 将函数内部的合法地址通过返回值 返回给函数外部使用 注意:函数不要返回普通局部变量的地址 分析&#xff1a; 在这段代码中&#xff0c;函数getAddr()返回一个指向局部变量data地址&#xff08;作用域是函数内部&#xff09;的指…

DevOps简介

DevOps简介 1、DevOps的起源2、什么是DevOps3、DevOps的发展现状4、DevOps与虚拟化、容器 1、DevOps的起源 上个世纪40年代&#xff0c;世界上第一台计算机诞生。计算机离不开程序&#xff08;Program&#xff09;驱动&#xff0c;而负责编写程序的人&#xff0c;被称为程序员&…

Django ModelSerializer 实现自定义验证详解

随着 Web 开发的日益复杂化&#xff0c;对数据验证的需求也日益增加。Django REST framework 提供了一套强大的、灵活的验证系统&#xff0c;帮助开发者轻松处理各种复杂情况。本文将重点探讨 Django ModelSerializer 中如何实现自定义验证。 1. 简介 Django ModelSerializer…

深度学习 opencv python 实现中国交通标志识别 计算机竞赛_1

文章目录 0 前言1 yolov5实现中国交通标志检测2.算法原理2.1 算法简介2.2网络架构2.3 关键代码 3 数据集处理3.1 VOC格式介绍3.2 将中国交通标志检测数据集CCTSDB数据转换成VOC数据格式3.3 手动标注数据集 4 模型训练5 实现效果5.1 视频效果 6 最后 0 前言 &#x1f525; 优质…

2023面试笔记四

1、gc导致的cpu冲高 排查是否为gc导致&#xff0c;看如下两点&#xff1a; gc频率和耗时 内存占用率 &#xff08;1&#xff09;gc频率和耗时有两种手段看&#xff1a; 第一种&#xff1a;根据gc日志的打印时间&#xff0c;可确定每次gc间隔的时间和耗时&#xff1a; 使用…

一个界面现代美观,色彩年轻化的Vue3+SpringBoot3前后端分离中后台管理脚手架

&#x1f4da; 在线文档 | ✨ 提交需求 | &#x1f680; 演示地址&#xff08;账号/密码&#xff1a;admin/admin123&#xff09; 简介 ContiNew Admin &#xff08;Continue New Admin&#xff09;中后台管理框架/脚手架&#xff0c;持续以最新流行技术栈构建&#xff0c;拥…

[git] cherry pick 将某个分支的某次提交应用到当前分支

功能&#xff1a;将某个分支的某次提交应用到当前分支 应用场景&#xff1a; 在合并分支时&#xff0c;是将源分支的所有内容都合并到目标分支上&#xff0c;有的时候我们可能只需要合并源分支的某次或某几次的提交&#xff0c;这个时候我们就需要使用到git的cherry-pick操作…

单链表(4)

看尾插函数 尾插函数跟头插函数唯一的不同就是找尾巴 尾插函数&#xff1a; 首先是动态申请一个新结点 把val放到新结点里面当新结点的data 然后在单链表里面找尾巴 比如说指针p找到尾巴了&#xff0c;现在将指针p指向新的结点&#xff0c;尾插就好了 这里的p类似于头插函…

Presto资源管理之Resource Groups And Selector

文章目录 前言资源组配置选择器规则 Selector Rules全局配置 Global Properties选择器属性配置案例配置 prestoDb 前言 资源组对资源使用进行限制&#xff0c;并可以对在其中运行的查询执行队列策略&#xff0c;或将资源分配给子组。查询属于单个资源组&#xff0c;并且从该组…

ESP32建立TCP连接

ESP32建立TCP连接 1.搭建ESP-IDF开发环境 搭建开发环境直接从官网下载即可。 https://docs.espressif.com/projects/esp-idf/zh_CN/v5.1.1/esp32s3/index.html https://dl.espressif.com/dl/esp-idf/?idf4.4 使用官方的下载器下载好&#xff0c;就可以自动安装&#xff0…

Java poi给docx中的关键字标记颜色

Java poi给docx中的关键字标记颜色 <dependency><groupId>org.apache.poi</groupId><artifactId>poi</artifactId><version>4.1.2</version></dependency><dependency><groupId>org.apache.poi</groupId>&l…

用于图像处理的高斯滤波器 (LoG) 拉普拉斯

一、说明 欢迎来到拉普拉斯和高斯滤波器的拉普拉斯的故事。LoG是先进行高斯处理&#xff0c;继而进行拉普拉斯算子的图像处理算法。用拉普拉斯具有过零功能&#xff0c;实现边缘岭脊提取。 二、LoG算法简述 在这篇博客中&#xff0c;让我们看看拉普拉斯滤波器和高斯滤波器的拉普…

Java 并发编程面试题——重入锁 ReentrantLock

目录 1.ReentrantLock 是什么&#xff1f;2.✨什么是重入锁&#xff1f;ReentrantLock 是如何实现可重入特征的&#xff1f;3.公平锁和非公平锁有什么区别&#xff1f;ReentrantLock 分别是如何实现的&#xff1f;4.✨ReentrantLock 的实现原理是什么&#xff1f;5.为什么 Reen…

Sui与SUMM3R推出NFT增长计划,毕业即提供5万美元资助

为了寻找并支持专注于NFT的高质量初创企业&#xff0c;我们自豪地宣布在Sui上推出NFT增长计划SUMM3R。这是一个为种子期和初创期公司提供的综合计划&#xff0c;该加速器将利用Sui独特的NFT技术&#xff0c;为它们提供无与伦比的知识、连接和资源&#xff0c;以建立可持续的业务…

k8s 部署mqtt —— 筑梦之路

mqtt是干嘛的&#xff0c;网上有很多资料&#xff0c;这里就不再赘述。 --- apiVersion: apps/v1 kind: Deployment metadata:labels:app: mqttname: mqttnamespace: default spec:replicas: 1selector:matchLabels:app: mqttstrategy:rollingUpdate:maxSurge: 25%maxUnavaila…

Leetcode_2:两数相加

题目描述&#xff1a; 给你两个 非空 的链表&#xff0c;表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的&#xff0c;并且每个节点只能存储 一位 数字。 请你将两个数相加&#xff0c;并以相同形式返回一个表示和的链表。 你可以假设除了数字 0 之外&#xff…