【Linux】高级IO(二)

news2024/11/14 17:49:38

文章目录

  • 高级IO(二)
    • I/O多路转接之poll
      • poll服务器
    • I/O多路转接之epoll
      • epoll相关函数
      • epoll工作原理
      • epoll回调机制
      • epoll服务器
      • epoll的优点

高级IO(二)

I/O多路转接之poll

poll也是系统提供的一个多路转接接口

  • poll系统调用也可以让我们的程序同时监视多个文件描述符上的事件是否就绪

poll 函数

 int poll(struct pollfd *fds, nfds_t nfds, int timeout);
  • fds:一个poll函数监视的结构列表,每一个元素包含三个部分:文件描述符、监视的事件集合、就绪的事件集合

  • nfds : fds数组的长度

  • timeout : 表示poll函数的超时时间,单位是毫秒

  • 参数调用成功,返回事件就绪的文件描述符个数,timeout时间耗尽返回0,函数调用失败返回-1

poll服务器

Socket类

#pragma once

#include <iostream>
#include <unistd.h>
#include <memory.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/types.h>

class Socket{
public:
  static int SocketCreate();
  static void SocketBind(int sock, int port);
  static void SocketListen(int sock, int backlog);
};

int Socket::SocketCreate(){
  int sockfd = socket(AF_INET, SOCK_STREAM, 0);
  if (sockfd < 0) {
    std::cerr << "socket error" << std::endl;
    exit(2);
  }

  // 设置端口复用
  int opt = 1;
  setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
  std::cout << "SocketCreate success" << std::endl;
  return sockfd;
}

void Socket::SocketBind(int sock, int port) {
  struct sockaddr_in local;
  memset(&local, 0, sizeof(local));
  local.sin_family = AF_INET;
  local.sin_port = htons(port);
  local.sin_addr.s_addr = INADDR_ANY;

  socklen_t len = sizeof(local);
  if (bind(sock, (struct sockaddr*)&local, len) < 0) {
    std::cerr << "bind error" << std::endl;
    exit(3);
  }
  std::cout << "SocketBind success" << std::endl;
}


void Socket::SocketListen(int sock, int backlog) {
  if (listen(sock, backlog) < 0) {
    std::cerr << "listen error" << std::endl;
    exit(4);
  }
  std::cout << "SocketListen success" << std::endl;
}

PollServer类

#pragma once 

#include "socket.hpp"
#include <poll.h>

#define BACK_LOG 5
#define DFT_PORT 8889
#define POLL_CAP 1024
#define DFT_FD   -1

class PollServer{
public:
  ~PollServer() { if (listen_sock > 0) close(listen_sock); }
  static PollServer* GetInstance(int port = DFT_PORT);
  void InitPollServer(); 
  void Run();
private:
  PollServer(int _port) : listen_sock(-1), port(_port){}
  void ClearPollfds(struct pollfd fds[], int num, int default_fd);
  bool SetPollfds(struct pollfd fds[], int num, int fd);
  void HandlerEvent(struct pollfd fds[], int num);
  void UnSetPoolfds(struct pollfd fds[], int index);
private:
  int listen_sock;
  int port;
  static PollServer* instance;
};
PollServer* PollServer::instance = nullptr;

PollServer* PollServer::GetInstance(int port) {
  if (instance == nullptr) 
    instance = new PollServer(port);
  return instance;
}

void PollServer::InitPollServer() {
  listen_sock = Socket::SocketCreate();
  Socket::SocketBind(listen_sock, port);
  Socket::SocketListen(listen_sock, BACK_LOG);
}

void PollServer::ClearPollfds(struct pollfd fds[], int num, int default_fd) {
  for (int i = 0; i < num; i++) {
    fds[i].fd = default_fd;
    fds[i].events = 0;
    fds[i].revents = 0;
  }
}

bool PollServer::SetPollfds(struct pollfd fds[], int num, int fd) {
  for (int i = 0; i < num; i++) {
    if (fds[i].fd == DFT_FD) {
      fds[i].fd = fd;
      fds[i].events |= POLLIN; // 添加读事件
      return true;
    }
  }
  return false;
}



void PollServer::Run() {
  struct pollfd fds[POLL_CAP];
  ClearPollfds(fds, POLL_CAP, DFT_FD);
  SetPollfds(fds, POLL_CAP, listen_sock);
  for (; ;) {
    switch(poll(fds, POLL_CAP, -1)) {
      case 0:
        std::cout << "timeout..." << std::endl; break;
      case -1:
        std::cerr << "poll error" << std::endl;
      default:
        HandlerEvent(fds, POLL_CAP);
        break;
    }
  }
}

void PollServer::UnSetPoolfds(struct pollfd fds[], int index) {
  fds[index].fd = DFT_FD;
  fds[index].events = 0;
  fds[index].revents = 0;
}


void PollServer::HandlerEvent(struct pollfd fds[], int num) {
  for (int i = 0; i < num; i++) {
    if (fds[i].fd == DFT_FD) continue;
    if (fds[i].fd == listen_sock && fds[i].revents & POLLIN) { // 读连接
      struct sockaddr_in peer;
      socklen_t len = sizeof(peer);
      memset(&peer, 0, sizeof(peer));
      int sock = accept(listen_sock, (struct sockaddr*)&peer, &len);
      if (sock < 0) {
        std::cerr << "aceept error" << std::endl;
        continue;
      }
      std::string peer_ip = inet_ntoa(peer.sin_addr);
      short peer_port = ntohs(peer.sin_port);
      std::cout << "get a new link -> [" << peer_ip << ":" << peer_port << "]" << std::endl;
      if (!SetPollfds(fds, POLL_CAP, sock)){
        close(sock);
        std::cout << "poll server is full, close fd :" << sock << std::endl;
      }
    } else if (fds[i].revents & POLLIN) {
#define BUFFER_SIZE 1024
      char buffer [BUFFER_SIZE];
      ssize_t size = read(fds[i].fd, buffer, sizeof(buffer));
      if (size > 0) {
        buffer[size - 1] = 0;
        std::cout << "echo #" <<  buffer << std::endl;
      } else if (size == 0) {
        std::cout << "client quit..." << std::endl;
        close(fds[i].fd);
        UnSetPoolfds(fds, i);
      } else {
        std::cerr << "read error" << std::endl;
        close(fds[i].fd);
        UnSetPoolfds(fds, i);
      }
    }
  }
}

服务器测试

#include "poll_server.hpp"
#include <string>


void Usage(char* proc) {
  std::cout << "Usage: " << proc << " port" << std::endl;
}

int main(int argc, char* argv[]) {
  if (argc != 2) {
    Usage(argv[0]);
    exit(1);
  }

  int port = atoi(argv[1]);
  PollServer* ps = PollServer::GetInstance(port);
  ps->InitPollServer();
  ps->Run();
  return 0;

}

I/O多路转接之epoll

epoll也是系统提供的一个多路转接接口

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

epoll相关函数

epoll_create 函数

epoll_create 用于创建一个epoll模型

int epoll_create(int size);
int epoll_create1(int flags);
  • Linux2.6.8版本之后,size函数可以被忽略,但必须设置成大于0的值

  • epoll模型创建成功返回对应的文件描述符,失败返回-1同时设置错误码

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

epoll_ctl函数

epoll_ctl函数用于指定epoll模型中注册事件,该函数的原型如下

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
  • epfd : 指定epoll模型

  • op : 选项,由三个宏表示

  • fd : 需要监视的文件描述符

  • event : 需要监视该文件上的拿一些事件

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

option 三个选项

  • EPOLL_CTL_ADD:注册新的文件描述符到指定的epoll模型中
  • EPOLL_CTL_MOD:修改已经注册的文件描述符中的监听事件
  • EPOLL_CTL_DEL:从epoll模型中删除指定的文件描述符

event 参数对应的struct 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 events */
    epoll_data_t data;        /* User data variable */
};

struct epoll_event有两个成员,第一个成员表示需要监视的事件,第二个成员data是一个联合体结构,一般使用该结构体中的fd,表示需要监听的文件描述符

	   EPOLLIN
              The associated file is available for read(2) operations.
			  # 表示对应的文件描述符可以读(包括对端socket正常关闭)
       EPOLLOUT
              The associated file is available for write(2) operations.
			  # 表示对应的文件描述符可以写
       EPOLLRDHUP (since Linux 2.6.17)
              Stream  socket  peer closed connection, or shut down writing half of connection.
              (This flag is especially useful for writing simple code to detect peer  shutdown
              when using Edge Triggered monitoring.)

       EPOLLPRI
              There is urgent data available for read(2) operations.
			  # 表示对应文件描述符有进击数据可读(外带数据到来)
       EPOLLERR
              Error  condition happened on the associated file descriptor.  epoll_wait(2) will
              always wait for this event; it is not necessary to set it in events.
			  # 表示对应的文件描述符发送错误
       EPOLLHUP
              Hang up happened on the associated file descriptor.  epoll_wait(2)  will  always
              wait for this event; it is not necessary to set it in events.
			  # 表示对应的文件描述符被挂断了,即对端文件描述符关闭了
       EPOLLET
              Sets  the  Edge  Triggered  behavior  for  the  associated file descriptor.  The
              default behavior for epoll is Level Triggered.  See epoll(7) for  more  detailed
              information about Edge and Level Triggered event distribution architectures.
			  # 将epoll的工作方式设置成边缘触发(Edge Triggered)模式
       EPOLLONESHOT (since Linux 2.6.2)
              Sets  the one-shot behavior for the associated file descriptor.  This means that
              after an event is pulled out with epoll_wait(2) the associated  file  descriptor
              is  internally disabled and no other events will be reported by the epoll inter‐
              face.  The user must call epoll_ctl()  with  EPOLL_CTL_MOD  to  rearm  the  file
              descriptor with a new event mask.
			  # 只监听一次事件,当监听完这次事件后,就将该文件描述符移出模型

这些数值都是由宏定义的,它们的二进制序列中有且只有一个比特位是1,且为1的比特位是各不相同的,可以才epoll.h文件中查看

epoll_wait 函数

epoll_wait函数用于收集监视的事件中已经就绪的事件,该函数的函数原型如下

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
int epoll_pwait(int epfd, struct epoll_event *events, int maxevents, int timeout, const sigset_t *sigmask);
  • epfd : 指定的epoll模型

  • events: 内核会将已经就绪的事件拷贝到events数组当中(events不能是空指针,内核只负责将就绪的事件拷贝到events数组当中(内核只负责将就绪的事件拷贝到数组中,不会帮我们在用户态开辟内存,所以必须要提前开好空间,并且events绝对不能传空指针

  • maxevents : events数组元素的个数,该值不能大于创建epoll模型size值

  • timeout: 表示epoll_wait函数的可超时事件,单位是毫秒

  • 如果函数调用成功,返回事件就绪的文件描述符个数,如果时间耗尽返回0,如果调用失败返回-1,并设置错误码

timeout 的取值

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

epoll_wait调用失败,错误码可能被设置为

  • EBADF: 传入epoll模型的对应文件描述符无效
  • EFAULT: events指向的数组空间无法通过写入权限访问
  • EINTR: 此调用被信号中断
  • EINVAL: epfd不是一个epoll模型对应的文件描述符,或传入maxevents值小于零

epoll工作原理

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

rbr 和 rdlist实际就是一颗红黑树和一个就绪队列

struct eventpoll{
	// 红黑树的根结点,用于存储所有添加到epoll模型需要监视的文件描述符和事件
	struct rb_root rbr;
	// 就绪队列中存放的是将要通过epoll_wait返回给用户的满足条件的事件
	struct list_head rdlist;
    // 等待队列,多个执行流想同时访问一个epoll模型在此等待
    struct list wait_queue;
}
  • epoll模型中的红黑树本质就是告诉内核,需要监视哪些文件描述符上的哪些事件,调用epoll_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事件已经准备就绪

调用epoll_ctl向红黑树中插入结点时,如果设置了EPOLLONESHOT选项,当监听完这次事件后,就会将这个结点从二叉树中删除。如果没有设置这个选项,则该结点就会一直存在,除非用户调用epoll_ctl将该节点从二叉树中删除

epoll回调机制

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

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

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

  • 事件不断就绪,会不断调用回调方法向就绪队列中插入对应结点,上层也会不断通过epoll_wait函数从就绪队列中获取结点,这是典型的生产消费模型
  • 有序就绪队列会被多个执行流同时访问,所以需要使用互斥锁对齐进行保护,epoll当中就有mtx来保护临界资源,epoll本身时线程安全的
  • 所以eventpoll中还应该有一个等待队列,当多个执行流向同时访问一个epoll模型时,就需要再等待队列下进行等待

epoll服务器

Socket 类

这个类已经实现过很多次了,这里就不多赘述了

#pragma once

#include <iostream>
#include <unistd.h>
#include <memory.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/types.h>

class Socket{
public:
  static int SocketCreate();
  static void SocketBind(int sock, int port);
  static void SocketListen(int sock, int backlog);
};

int Socket::SocketCreate(){
  int sockfd = socket(AF_INET, SOCK_STREAM, 0);
  if (sockfd < 0) {
    std::cerr << "socket error" << std::endl;
    exit(2);
  }

  // 设置端口复用
  int opt = 1;
  setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
  std::cout << "SocketCreate success" << std::endl;
  return sockfd;
}

void Socket::SocketBind(int sock, int port) {
  struct sockaddr_in local;
  memset(&local, 0, sizeof(local));
  local.sin_family = AF_INET;
  local.sin_port = htons(port);
  local.sin_addr.s_addr = INADDR_ANY;

  socklen_t len = sizeof(local);
  if (bind(sock, (struct sockaddr*)&local, len) < 0) {
    std::cerr << "bind error" << std::endl;
    exit(3);
  }
  std::cout << "SocketBind success" << std::endl;
}


void Socket::SocketListen(int sock, int backlog) {
  if (listen(sock, backlog) < 0) {
    std::cerr << "listen error" << std::endl;
    exit(4);
  }
  std::cout << "SocketListen success" << std::endl;
}


EpollServer 类

EpollServer类当中除了需要包含服务器绑定的端口号以及监听套接字,还需要增加一个用于描述epoll模型对应的文件描述符的成员变量

#include "socket.hpp"
#include <sys/epoll.h>

#define EPOLL_CAP 1024 
#define DEFAULT_PORT 8889
#define BACK_LOG 5


class EpollServer{
public:
  static EpollServer* GetInstance(int _port = DEFAULT_PORT);				// 获取单例
  ~EpollServer();															// 析构函数
  void EpollServerInit();													// 服务器初始化					
  void Run();
private:
  EpollServer(int _port) : port(_port), listen_sock(-1), epoll_fd(-1){}     // 构造函数
  void AddEvent(int sock, uint32_t event);
  void DelEvent(int sock);
  void HandlerEvent(struct epoll_event revs[], int num);
private:
  int port;
  int listen_sock;
  int epoll_fd;
  static EpollServer* instance;   								             // 用于创建单例模式
};

EpollServer* EpollServer::instance = nullptr;

EpollServer* EpollServer::GetInstance(int _port) {
  if (instance == nullptr) {
    instance = new EpollServer(_port);
  }
  return instance;
}

EpollServer::~EpollServer(){
  if (listen_sock > 0) close(listen_sock);
  if (epoll_fd > 0) close(epoll_fd);
}

void EpollServer::EpollServerInit() {
  listen_sock = Socket::SocketCreate();
  Socket::SocketBind(listen_sock, port);
  Socket::SocketListen(listen_sock, BACK_LOG);
  epoll_fd = epoll_create(EPOLL_CAP);
  if (epoll_fd < 0) {
    std::cerr << "epoll_create error" << std::endl;
    exit(5);
  }
}

运行服务器

void EpollServer::AddEvent(int sock, uint32_t event) {
  struct epoll_event ev;
  ev.events = event;
  ev.data.fd = sock;
  epoll_ctl(epoll_fd, EPOLL_CTL_ADD, sock, &ev);
}


void EpollServer::Run(){
  AddEvent(listen_sock, EPOLLIN);
  for (;;){
    struct epoll_event revs[EPOLL_CAP];						// 用于接收已经就绪的事件
    int num = epoll_wait(epoll_fd, revs, EPOLL_CAP, -1);
    if (num < 0) {
      std::cerr << "epoll_wait error" << std::endl;
    } else if (num == 0) {
      std::cout << "time out..." << std::endl;
    } else {
      HandlerEvent(revs, num);								// 事件处理
    }
  }
}
  • 默认情况下,只要有底层就绪事件没有处理,epoll就会一直通知用户,也就是调用epoll_wait会一直成功返回,并将就绪的事件拷贝到传入的数组中。
  • 事件处理并非是将就绪队列中的数据拷贝到用户层,比如套接字的读事件就绪,需要调用accept获取到底层连接才算是处理完事件

事件处理

  • 调用epoll_wait得到的返回值来判断操作系统向revs数组中拷贝了多少个struct epoll_event 结构,进而一个个获取事件进行处理
void EpollServer::AddEvent(int sock, uint32_t event) {
  struct epoll_event ev;
  ev.events = event;
  ev.data.fd = sock;
  epoll_ctl(epoll_fd, EPOLL_CTL_ADD, sock, &ev);
}

void EpollServer::DelEvent(int sock) {
  epoll_ctl(epoll_fd, EPOLL_CTL_DEL, sock, nullptr);
}

void EpollServer::HandlerEvent(struct epoll_event revs[], int num) {
  for (int i = 0; i < num; i++) {
    if (revs[i].data.fd == listen_sock && revs[i].events & EPOLLIN) {
      struct sockaddr_in peer;
      socklen_t len = sizeof(peer);
      int sock = accept(listen_sock, (struct sockaddr*)&peer, &len);
      if (sock >= 0) {
        std::string client_ip = inet_ntoa(peer.sin_addr);
        short client_port = ntohs(peer.sin_port);
        std::cout << "get a new link [" << client_ip << ":" << client_port << std::endl;
        AddEvent(sock, EPOLLIN);
      } else {
        std::cerr << "accept error" << std::endl;
        continue;
      }
    } else if (revs[i].events & EPOLLIN) {
#define BUFFER_SIZE 1024
      char buffer[BUFFER_SIZE];
      ssize_t size = read(revs[i].data.fd, buffer, sizeof(buffer) - 1);
      if (size > 0) {
        buffer[size - 1] = 0;
        std::cout << "echo # " << buffer << std::endl;
      } else if (size == 0) {
        std::cout << "client quit..." << std::endl; 
        DelEvent(revs[i].data.fd);
        close(revs[i].data.fd);
      }
      else {
        std::cerr << "recv error" << std::endl;
        close(revs[i].data.fd);
        DelEvent(revs[i].data.fd);
      }
    }
  }
}

epoll 服务器测试

#include "epoll_server.hpp"
#include <string>

static void Usage(std::string proc) {
  std::cout << "Usage: " << proc << " port " << std::endl;
}

int main(int argc, char* argv[]) {
  if (argc != 2) {
    Usage(argv[0]);
    exit(1);
  }

  int port = atoi(argv[1]);
  EpollServer* es = EpollServer::GetInstance(port);
  es->EpollServerInit();
  es->Run();
  return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-z5Fz6VTx-1688913152641)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20230709221625132.png)]

这里我们编写的时单进程epoll服务器,但是其可以位多个客户端提供服务。我们可以使用ls /proc/PID/fd 命令没查看epoll服务器的文件描述符的使用情况

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QSgVPcwm-1688913152642)(C:\Users\Lenovo\AppData\Roaming\Typora\typora-user-images\image-20230709222050710.png)]

[clx@VM-20-6-centos poll_server]$ ll /proc/22697/fd
total 0
lrwx------ 1 clx clx 64 Jul  9 22:15 0 -> /dev/pts/5
lrwx------ 1 clx clx 64 Jul  9 22:20 1 -> /dev/pts/5
lrwx------ 1 clx clx 64 Jul  9 22:15 2 -> /dev/pts/5
lrwx------ 1 clx clx 64 Jul  9 22:20 3 -> socket:[1478855763]
lrwx------ 1 clx clx 64 Jul  9 22:20 4 -> anon_inode:[eventpoll]
lrwx------ 1 clx clx 64 Jul  9 22:20 5 -> socket:[1478878270]
[clx@VM-20-6-centos poll_server]$ 

epoll的优点

  • 接口使用方便,使用起来非常方便高效
  • 数据轻量拷贝:只在新增监视事件需要调用epoll_ctl将数据拷贝到内核。而select 和 poll每次都要重新将所有需要监视的事件从用户拷贝到内核。此外调用epoll_wait获取就绪事件时,只会拷贝就绪的事件,不会进行不必要的拷贝擦欧总
  • 事件的回调机制:避免操作系统主动进行轮询检测事件就绪,而是采用回调函数机制,将文件描述符放入到就绪队列中。调用epoll_wait时候直接访问就绪队列就知道哪些文件描述符已经就绪,检测是否有文件描述符的时间复杂度是O(1),只要判断就绪队列是否为空即可
  • 没有数量限制,如果内存允许,就可以一直向红黑树中增加结点

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

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

相关文章

ruoyi若依 组织架构设计--[ 部门管理 ]

ruoyi若依 组织架构设计--[ 部门管理 ] 部门管理部门查询部门新增部门修改部门删除 部门管理 部门查询 需要注意的是&#xff0c;部门管理也有数据权限&#xff0c;比如A用户分配的数据权限(通过角色分配)是深圳总公司&#xff0c;那么A用户登录后看到的部门也是深圳总公司&am…

2023年前端面试题汇总-数据结构(二叉树)

对于树这个结构,最常见的就是二叉树。我们除了需要了解二叉树的基本操作之外,还需要了解一些特殊的二叉树,比如二叉搜索树、平衡二叉树等,另外还要熟悉二叉树的遍历方式,比如前序遍历、中序遍历、后序遍历、层序遍历。另外还要知道二叉树的常用遍历的方式:深度优先遍历和…

非线性优化知识

这里列下最小二乘的四种解法的优缺点&#xff0e; #mermaid-svg-CLbQz6o8j7JMq9MM {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-CLbQz6o8j7JMq9MM .error-icon{fill:#552222;}#mermaid-svg-CLbQz6o8j7JMq9MM .err…

前端开发中的单例模式

在前端开发中&#xff0c;单例模式是一种常见的设计模式&#xff0c;用于确保一个类只有一个实例&#xff0c;并提供一个全局访问点来获取该实例。 在JavaScript中&#xff0c;可以使用以下几种方式来实现单例模式&#xff1a; 字面量方式&#xff1a; const singleton {// …

JVM 调优测试Jmeter 压测

Jmeter 内存不足了&#xff0c;修个5个线程吧 测试结果&#xff1a; Jmeter配置参数 5个线程&#xff0c;每个线程1秒跑1000次 测试串行吞吐量 -XX:PrintGCDetails -Xmx128M -Xms128M -XX:HeapDumpOnOutOfMemoryError -XX:UseSerialGC -XX:PermSize32M GC回收4次 吞吐量138…

SQL Server 2008每天自动备份数据库

在SQL Server 2008数据库中。为了防止数据的丢失我们就需要按时的来备份数据库了。要是每天都要备份的话&#xff0c;人工备份会很麻烦的&#xff0c;自动备份的话就不需要那么麻烦了&#xff0c;只要设置好了&#xff0c;数据库就会自动在你设置的时间段里备份。那么自动备份要…

ihrm项目结构详解

大体介绍 云服务的三种模式 Iaas&#xff1a;基础设施即服务 Pass&#xff1a;平台即服务 Saas&#xff1a;软件即服务 系统设计 主键id生成策略 lombok data setter getter noArgs&#xff08;无参构造&#xff09; 模块搭建 1 企业得增删改查 2 全局异常处理器 3 跨域…

选择排序算法介绍

算法介绍 选择排序&#xff08;Selection Sort&#xff09;是一种简单直观的排序算法。它的基本思想是每次从待排序的元素中选取最小&#xff08;或最大&#xff09;的元素&#xff0c;放到已排序部分的末尾&#xff0c;直到全部元素排序完毕。 以下是选择排序的详细步骤&…

【实战】 六、用户体验优化 - 加载中和错误状态处理(下) —— React17+React Hook+TS4 最佳实践,仿 Jira 企业级项目(十)

文章目录 一、项目起航&#xff1a;项目初始化与配置二、React 与 Hook 应用&#xff1a;实现项目列表三、TS 应用&#xff1a;JS神助攻 - 强类型四、JWT、用户认证与异步请求五、CSS 其实很简单 - 用 CSS-in-JS 添加样式六、用户体验优化 - 加载中和错误状态处理1~234.用useAs…

java实现一个简单的webSocket聊天demo

java实现一个简单的webSocket聊天demo 一、依赖二、配置准备三、demo代码编写四、启动测试五、编写业务 一、依赖 添加pom文件依赖 <!-- websocket--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter…

IDEA环境配置汇总

1、配置项目编码 2、配置运行看板Services IDEA开启并配置Services窗口 这里已经配置好了&#xff0c;如果没有&#xff0c;就点击&#xff0c;点击Run Configuration Type&#xff0c;选择所需要的&#xff0c;点击即可。 配置spring与docker看板(按照上面的方法来配置&am…

【Python】查询SQL并用柱状图展示

需求&#xff1a; 查询2022年各月订单量&#xff0c;并用柱状图展示 SQL&#xff1a; select date_format(create_time,%Y-%m) as mon ,count(distinct order_id) as ord_cnt from prod.order_info where date_format(create_time,%Y-%m) between 2022-01 and 2022-12 group…

Mac OS装Windows系统开启虚拟化

目录 引言前提macOS开启虚拟化mac下的Windows开启虚拟化双系统开启虚拟化修改启动管理程序开启虚拟化 注意事项 引言 在开发工作中&#xff0c;很多软件需要用到virtual box&#xff0c;但是使用virtual box需要开启虚拟化&#xff0c;而有些苹果笔记本虚拟化是关闭的&#xf…

【GitHub】一条命令快速部署 Kubernetes(k8s) 集群的工具-sealos

Sealos 是一个GitHub上优秀的开源项目&#xff0c;目前项目点赞数已达&#xff1a;10.2k&#xff0c;核心特性&#xff1a; 管理集群生命周期下载和使用完全兼容 OCI 标准的分布式应用定制化分布式应用Sealos Cloud 项目开源协议&#xff1a;Apache-2.0 项目主开发语言&…

NSS [SWPUCTF 2022 新生赛]funny_web

NSS [SWPUCTF 2022 新生赛]funny_web 账号NSS 密码2122693401 私货不去细细研究了&#xff0c;直接看题。 num不等于12345&#xff0c;但是intval&#xff08;num&#xff09;等于12345 ①intval():可以获取变量的整数值intval()中有一个特性&#xff0c;其中若传入1e4&…

tensorboard与torchinfo的使用

目录 1. tensorboard1.1 本地使用1.2 远程服务器使用 2. torchinfoRef 1. tensorboard 1.1 本地使用 只需要掌握一个 torch.utils.tensorboard.writer.SummaryWriter 接口即可。 在初始化 SummaryWriter 的时候&#xff0c;通常需要指定log的存放路径。这个路径默认是 runs/…

Python脚本小工具之文件与内容搜索

目录 一、前言 二、代码 三、结果 一、前言 ​日常工作中&#xff0c;经常需要在指定路径下查找指定类型的文件&#xff0c;或者是指定内容的查找&#xff0c;在window环境中&#xff0c;即可以使用一些工具如notepad或everything&#xff0c;也可以使用python脚本。但在l…

【C++进阶】bitset位图介绍以及模拟实现

文章目录 位图介绍一、位图的引入二、位图的概念 位图模拟实现一、构造函数二、set&#xff0c;reset&#xff0c;test函数三、代码测试四、完整代码 位图介绍 一、位图的引入 先来看下边一道面试题&#xff1a; 给40亿个不重复的无符号整数&#xff0c;没排过序。给一个无符…

SAR ADC version2 ——ADC背景介绍

目录&#xff1a; ADC常用指标分类 静态性能&#xff1a;微分非线性&#xff1a;DNL 积分非线性&#xff1a;INL 仿真测试DNL&#xff1a;&#xff08;码密度法&#xff09;&#xff08;code density&…

OpenCV 入门教程:像素访问和修改

OpenCV 入门教程&#xff1a;像素访问和修改 导语一、像素访问1.1 获取图像的大小1.2 访问图像的像素值1.3 修改图像的像素值 二、示例应用2.1 图像反转2.2 阈值化操作 三、总结 导语 在图像处理和计算机视觉领域&#xff0c;像素级操作是非常重要和常见的任务之一。通过像素访…