reactor网络模型的原理与实现

一、rector网络模型

        对高并发编程,网络连接上的消息处理,可以分为两个阶段:等待消息准备好、消息处理。当使用默认的阻塞套接字时,往往是把这两个阶段合而为一,这样操作套接字的代码所在的线程就得睡眠来等待消息准备好,这导致了高并发下线程会频繁的睡眠、唤醒,从而影响了 CPU 的使用效率

        高并发编程方法当然就是把两个阶段分开处理。即,等待消息准备好的代码段,与处理消息的代码段是分离的。当然,这也要求套接字必须是非阻塞的,否则,处理消息的代码段很容易导致条件不满足时,所在线程又进入了睡眠等待阶段。那么问题来了,等待消息准备好这个阶段怎么实现?它毕竟还是等待,这意味着线程还是要睡眠的!解决办法就是,线程主动查询,或者让 1 个线程为所有连接而等待!这就是 IO 多路复用了。多路复用就是处理等待消息准备好这件事的,但它可以同时处理多个连接!它也可能“等待”,所以它也会导致线程睡眠,然而这不要紧,因为它一对多、它可以监控所有连接。这样,当我们的线程被唤醒执行时,就一定是有一些连接准备好被我们的代码执行了。

        作为一个高性能服务器程序通常需要考虑处理三类事件: I/O 事件,定时事件及信号。两种高效的事件处理模型:Reactor 和 Proactor

        首先来回想一下普通函数调用的机制:程序调用某函数,函数执行,程序等待,函数将结果和控制权返回给程序,程序继续处理。Reactor 释义“反应堆”,是一种事件驱动机制。和普通函数调用的不同之处在于:应用程序不是主动的调用某个 API 完成处理,而是恰恰相反,Reactor 逆置了事件处理流程,应用程序需要提供相应的接口并注册到 Reactor 上,如果相应的时间发生,Reactor 将主动调用应用程序注册的接口,这些接口又称为“回调函数”

        Reactor 模式是处理并发 I/O 比较常见的一种模式,用于同步 I/O,中心思想是将所有要处理的 I/O 事件注册到一个中心 I/O 多路复用器上,同时主线程/进程阻塞在多路复用器上;一旦有 I/O 事件到来或是准备就绪(文件描述符或 socket 可读、写),多路复用器返回并将事先注册的相应 I/O 事件分发到对应的处理器中。

Reactor 模型有三个重要的组件:

  • 多路复用器:由操作系统提供,在 linux 上一般是 select, poll, epoll 等系统调用。
  • 事件分发器:将多路复用器中返回的就绪事件分到对应的处理函数中。
  • 事件处理器:负责处理特定事件的处理函数

具体流程如下:

  1. 注册读就绪事件和相应的事件处理器;
  2. 事件分离器等待事件;
  3. 事件到来,激活分离器,分离器调用事件对应的处理器;
  4. 事件处理器完成实际的读操作,处理读到的数据,注册新的事件,然后返还控制权

Reactor 模式是编写高性能网络服务器的必备技术之一,它具有如下的优点:

  • 响应快,不必为单个同步时间所阻塞,虽然 Reactor 本身依然是同步的;
  • 编程相对简单,可以最大程度的避免复杂的多线程及同步问题,并且避免了多线程/进程的切换开销; 可扩展性,可以方便的通过增加 Reactor 实例个数来充分利用 CPU 资源;
  • 可复用性,reactor 框架本身与具体事件处理逻辑无关,具有很高的复用性;Reactor 模型开发效率上比起直接使用 IO 复用要高,它通常是单线程的,设计目标是希望单线程使用一颗 CPU 的全部资源,但也有附带优点,即每个事件处理中很多时候可以不考虑共享资源的互斥访问。可是缺点也是明显的,现在的硬件发展,已经不再遵循摩尔定律,CPU 的频率受制于材料的限制不再有大的提升,而改为是从核数的增加上提升能力,当程序需要使用多核资源时,Reactor 模型就会悲剧, 为什么呢?

        如果程序业务很简单,例如只是简单的访问一些提供了并发访问的服务,就可以直接开启多个反应堆,每个反应堆对应一颗 CPU 核心,这些反应堆上跑的请求互不相关,这是完全可以利用多核的。例如 Nginx 这样的 http 静态服务器。

二、reactor为什么搭配非阻塞IO

(1)多线程环境下时,是可以将listenfd放到多个线程的多个epoll中去处理的,那如果有一个连接接入的时候,会给全部线程发送一个信号,也就是所有的线程的epoll都可能返回(惊群),但是该事件只会在一个线程中被处理,其他线程再去accept的时候,连接已经被其他线程处理过,也就不存在了,如果socket没有被设置成nonblocking,此accept将阻塞当前线程

(2)当为边缘触发模式下(一次只会触发一次读事件),读事件触发时,read需要循环把读缓存读空,不然会到下次数据到来时才会触发读事件。比如有500字节数据,单次read 100字节,需要循环5次全部读完,那第6次再去读的时候就会阻塞住当前线程

(3)当reactor使用的是select实现时,select存在一个bug:当某个socket接收缓冲区有新数据到达,然后select报告这个socket描述符可读,但随后,协议栈检查到这个新分节检验和错误时,会丢弃这个分节,这时再调用read则无数据可读,如果socket没有被设置成nonblocking,此read将阻塞当前线程

额外补充一点:在select中关闭socket时需要注意,关闭socket需要及时更新监测socket集合。当一个socket被关闭之后,我们需要将其对应的文件描述符从select的监测集合中删除。否则,会导致select函数在下一次调用时伪唤醒,也就是说,调用后立即返回,并且一直返回“有事件发生”,而实际上这个已关闭的文件描述符并没有事件可处理。

三、代码实现

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <sys/time.h>
#include <sys/sendfile.h>#define BUFFER_LENGTH        1024
#define MAX_EPOLL_EVENTS    1024
#define RESOURCE_LENGTH        1024#define SERVER_PORT            8888
#define PORT_COUNT            100#define TIME_SUB_MS(tv1, tv2)  ((tv1.tv_sec - tv2.tv_sec) * 1000 + (tv1.tv_usec - tv2.tv_usec) / 1000)#if 0struct epoll_event {__uint32_t events;  // epoll 监控的事件类型epoll_data_t data;  // 用户数据};typedef union epoll_data {void *ptr;int fd;__uint32_t u32;__uint64_t u64;} epoll_data_t;typedef int NCALLBACK(int ,int, void*);1、在epoll监听到可读可写事件后,回调recv_cb、send_cd时的参数传递过程如下:
nty_event_add: struct epoll_event.data.ptr = struct ntyevent.arg
nty_event_set: struct ntyevent.arg = struct ntyreactor 2、所以在epoll_wait返回时,能通过events[i].data.ptr获取到struct ntyreactor,然后传入recv_cb、send_cd回调函数进行处理
#endif//socketfd子项
struct ntyevent 
{int fd;int events; //EPOLLOUT、EPOLLIN事件void *arg; //指向reactorint (*callback)(int fd, int events, void *arg);int added; //是否添加过epollchar rbuffer[BUFFER_LENGTH];char wbuffer[BUFFER_LENGTH];int rlength;int wlength;// int method;// char resource[RESOURCE_LENGTH];
};//事件块,每个块中存放 ITEM_LENGTH 个 sock_item
struct eventblock 
{struct eventblock *next;//指向下一个 eventblockstruct ntyevent *events;//数组(由于fd是顺序增加的,可直接将fd与数组的下标一一对应)
};//epoll的fd,与eventblock事件块管理
struct ntyreactor 
{int epfd;int blkcnt;struct eventblock *evblks;
};void nty_event_set(struct ntyevent *ev, int fd, NCALLBACK callback, void *arg);
int nty_event_add(int epfd, int events, struct ntyevent *ev);
int nty_event_del(int epfd, struct ntyevent *ev);
struct ntyevent *ntyreactor_idx(struct ntyreactor *reactor, int sockfd);int recv_cb(int fd, int events, void *arg);
int send_cb(int fd, int events, void *arg);
int accept_cb(int fd, int events, void *arg);struct timeval tv_begin; //listen时赋初值,用于计算连接所需时长//recv时的回调函数
int recv_cb(int fd, int events, void *arg) 
{struct ntyreactor *reactor = (struct ntyreactor*)arg;struct ntyevent *ev = ntyreactor_idx(reactor, fd);if (ev == NULL) return -1;//接收数据int len = recv(fd, ev->rbuffer, BUFFER_LENGTH, 0);nty_event_del(reactor->epfd, ev); //先将fd从epoll中删除!!!if (len > 0) {ev->rlength = len;ev->rbuffer[len] = '\0';printf("recv [%d]:%s\n", fd, ev->rbuffer);//组织应答包ev->wlength = sprintf(ev->wbuffer, "%s", "this is a server!");//将fd添加进epoll中,设置为EPOLLOUT监听,发送数据nty_event_set(ev, fd, send_cb, reactor);nty_event_add(reactor->epfd, EPOLLOUT, ev);} else if (len == 0) //客户端主动断开连接{printf("recv_cb --> disconnect\n");nty_event_del(reactor->epfd, ev); //将fd从epoll中删除close(ev->fd);} else{if (errno == EAGAIN && errno == EWOULDBLOCK) {} else if (errno == ECONNRESET){nty_event_del(reactor->epfd, ev);close(ev->fd);}printf("recv[fd=%d] error[%d]:%s\n", fd, errno, strerror(errno));}return len;
}//send时的回调函数
int send_cb(int fd, int events, void *arg) 
{struct ntyreactor *reactor = (struct ntyreactor*)arg;struct ntyevent *ev = ntyreactor_idx(reactor, fd);if (ev == NULL) return -1;//没有数据要发生if(ev->wlength == 0){//将fd添加进epoll中,设置为EPOLLIN监听nty_event_del(reactor->epfd, ev);nty_event_set(ev, fd, recv_cb, reactor);nty_event_add(reactor->epfd, EPOLLIN, ev);return 0;}int len = send(fd, ev->wbuffer, ev->wlength, 0);if (len > 0) {//将fd添加进epoll中,设置为EPOLLIN监听nty_event_del(reactor->epfd, ev);nty_event_set(ev, fd, recv_cb, reactor);nty_event_add(reactor->epfd, EPOLLIN, ev);} else {nty_event_del(reactor->epfd, ev);close(ev->fd);printf("send[fd=%d] error[%d]:%s\n", fd, errno, strerror(errno));}return len;
}//新client连接时的accept处理回调函数
int accept_cb(int fd, int events, void *arg) 
{static int curfds = 0;struct ntyreactor *reactor = (struct ntyreactor*)arg;if (reactor == NULL) return -1;struct sockaddr_in client_addr;socklen_t len = sizeof(client_addr);int clientfd;//accept连接新的clientif ((clientfd = accept(fd, (struct sockaddr*)&client_addr, &len)) == -1) {if (errno != EAGAIN && errno != EINTR) {}printf("accept: %s\n", strerror(errno));return -1;}//将clientfd设置成非阻塞int flag = 0;if ((flag = fcntl(clientfd, F_SETFL, O_NONBLOCK)) < 0) {printf("%s: fcntl nonblocking failed, %d\n", __func__, MAX_EPOLL_EVENTS);return -1;}//根据sockfd从reactor中 找到对应的ntyeventstruct ntyevent *event = ntyreactor_idx(reactor, clientfd);if (event == NULL) return -1;//将clientfd对应的ntyevent添加到epoll中nty_event_set(event, clientfd, recv_cb, reactor);nty_event_add(reactor->epfd, EPOLLIN, event);//计算连接所花的时间if (curfds++ % 1000 == 999) {struct timeval tv_cur;memcpy(&tv_cur, &tv_begin, sizeof(struct timeval));gettimeofday(&tv_begin, NULL);int time_used = TIME_SUB_MS(tv_begin, tv_cur);printf("connections: %d, sockfd:%d, time_used:%d\n", curfds, clientfd, time_used);}printf("new connect [%s:%d], pos[%d]\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port), clientfd);return 0;
}//设置ntyevent,对各参数赋值
//arg:指向reactor
void nty_event_set(struct ntyevent *ev, int fd, NCALLBACK callback, void *arg) 
{ev->fd = fd;ev->callback = callback;ev->events = 0;ev->arg = arg;//ev->last_active = time(NULL);return ;
}//将fd添加进epoll中
int nty_event_add(int epfd, int events, struct ntyevent *ev) 
{struct epoll_event ep_ev = {0, {0}};ev->events = events;ep_ev.data.ptr = ev; //指向fd对应的ntyeventep_ev.events = events;int op;if (ev->added == 1) {op = EPOLL_CTL_MOD;} else {op = EPOLL_CTL_ADD;ev->added = 1; //fd第一次添加完后,之后都是 EPOLL_CTL_MOD 操作}if (epoll_ctl(epfd, op, ev->fd, &ep_ev) < 0) {printf("event add failed [fd=%d], events[%d]\n", ev->fd, events);return -1;}return 0;
}//将fd从epoll中删除
int nty_event_del(int epfd, struct ntyevent *ev) 
{struct epoll_event ep_ev = {0, {0}};//如果没有添加过,直接返回if (ev->added != 1) {return -1;}ep_ev.data.ptr = ev; //指向fd对应的ntyeventev->added = 0;epoll_ctl(epfd, EPOLL_CTL_DEL, ev->fd, &ep_ev);return 0;
}//添加新块,扩容
int ntyreactor_alloc(struct ntyreactor *reactor) 
{if (reactor == NULL) return -1;if (reactor->evblks == NULL) return -1;struct eventblock *blk = reactor->evblks;//定位到最后一个next为NULL的节点while (blk->next != NULL) {blk = blk->next;}//申请事件块中的数组空间struct ntyevent* evs = (struct ntyevent*)malloc((MAX_EPOLL_EVENTS) * sizeof(struct ntyevent));if (evs == NULL) {printf("ntyreactor_alloc ntyevent failed\n");return -2;}memset(evs, 0, (MAX_EPOLL_EVENTS) * sizeof(struct ntyevent));//申请事件块节点空间struct eventblock *block = malloc(sizeof(struct eventblock));if (block == NULL) {printf("ntyreactor_alloc eventblock failed\n");return -3;}//事件块赋值block->events = evs;block->next = NULL;//添加到reactorblk->next = block;reactor->blkcnt ++;return 0;
}//根据sockfd从reactor中 找到对应的ntyevent
struct ntyevent *ntyreactor_idx(struct ntyreactor *reactor, int sockfd)
{int i = 0;if (reactor == NULL) return NULL;if (reactor->evblks == NULL) return NULL;//计算sockfd对应块的索引(从0开始),如果超过了现有管理的块数后,会添加新的块来扩容//sockfd 0-1023在1块、sockfd 1024-2047在2块、...int blkidx = sockfd / MAX_EPOLL_EVENTS;while (blkidx >= reactor->blkcnt) {ntyreactor_alloc(reactor);}//定位到sockfd所在的块struct eventblock *blk = reactor->evblks;while (i++ != blkidx && blk != NULL) {blk = blk->next;}//返回ntyevent,sockfd % ITEM_LENGTH 就是 sockfd在数组中的索引return &blk->events[sockfd % MAX_EPOLL_EVENTS];
}//初始化reactor:创建epoll、创建1个事件管理块
int ntyreactor_init(struct ntyreactor *reactor) 
{if (reactor == NULL) return -1;memset(reactor, 0, sizeof(struct ntyreactor));//1、创建epollreactor->epfd = epoll_create(1);if (reactor->epfd <= 0) {printf("create epfd in %s err %s\n", __func__, strerror(errno));return -2;}//2、创建1个事件管理块struct ntyevent* evs = (struct ntyevent*)malloc((MAX_EPOLL_EVENTS) * sizeof(struct ntyevent));if (evs == NULL) {printf("create epfd in %s err %s\n", __func__, strerror(errno));close(reactor->epfd);return -3;}memset(evs, 0, (MAX_EPOLL_EVENTS) * sizeof(struct ntyevent));struct eventblock *block = malloc(sizeof(struct eventblock));if (block == NULL) {free(evs);close(reactor->epfd);return -3;}block->events = evs;block->next = NULL;reactor->evblks = block;reactor->blkcnt = 1;return 0;
}//销毁rector
int ntyreactor_destory(struct ntyreactor *reactor) 
{close(reactor->epfd);struct eventblock *blk = reactor->evblks;struct eventblock *blk_next;while (blk != NULL) {blk_next = blk->next;free(blk->events);free(blk);blk = blk_next;}return 0;
}//往reactor中添加listen事件
int ntyreactor_addlistener(struct ntyreactor *reactor, int sockfd, NCALLBACK *acceptor) 
{if (reactor == NULL) return -1;if (reactor->evblks == NULL) return -1;struct ntyevent *event = ntyreactor_idx(reactor, sockfd);if (event == NULL) return -1;nty_event_set(event, sockfd, acceptor, reactor);nty_event_add(reactor->epfd, EPOLLIN, event);return 0;
}//监听epoll
int ntyreactor_run(struct ntyreactor *reactor) 
{if (reactor == NULL) return -1;if (reactor->epfd < 0) return -1;if (reactor->evblks == NULL) return -1;struct epoll_event events[MAX_EPOLL_EVENTS+1];int checkpos = 0, i;while (1){int nready = epoll_wait(reactor->epfd, events, MAX_EPOLL_EVENTS, 1000);if (nready < 0) {printf("epoll_wait error, exit\n");continue;}for (i = 0;i < nready;i ++) {//data.ptr指向fd对应的ntyeventstruct ntyevent *ev = (struct ntyevent*)events[i].data.ptr;//ntyevent->arg指向reactorif ((events[i].events & EPOLLIN) && (ev->events & EPOLLIN)) {ev->callback(ev->fd, events[i].events, ev->arg);}if ((events[i].events & EPOLLOUT) && (ev->events & EPOLLOUT)) {ev->callback(ev->fd, events[i].events, ev->arg);}}}
}//初始化server,完成 socket--->bind--->listen
//ip:INADDR_ANY
//port:自定义
int init_sock(short port) 
{int fd = socket(AF_INET, SOCK_STREAM, 0);fcntl(fd, F_SETFL, O_NONBLOCK);struct sockaddr_in server_addr;memset(&server_addr, 0, sizeof(server_addr));server_addr.sin_family = AF_INET;server_addr.sin_addr.s_addr = htonl(INADDR_ANY);server_addr.sin_port = htons(port);bind(fd, (struct sockaddr*)&server_addr, sizeof(server_addr));if (listen(fd, 20) < 0) {printf("listen failed : %s\n", strerror(errno));return -1;}printf("listen server port : %d\n", port);gettimeofday(&tv_begin, NULL);return fd;
}int main(int argc, char *argv[]) 
{int i = 0;int sockfds[PORT_COUNT] = {0};struct ntyreactor *reactor = NULL;//创建并初始化reactorreactor = (struct ntyreactor*)malloc(sizeof(struct ntyreactor));ntyreactor_init(reactor);//初始化server,开启监听unsigned short port = SERVER_PORT;if (argc == 2) {port = atoi(argv[1]);}for (i = 0;i < PORT_COUNT;i ++) {sockfds[i] = init_sock(port+i);ntyreactor_addlistener(reactor, sockfds[i], accept_cb);}//监听io事件ntyreactor_run(reactor);//销毁rectorntyreactor_destory(reactor);for (i = 0;i < PORT_COUNT;i ++) {close(sockfds[i]);}free(reactor);return 0;
}

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

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

相关文章

Mysql常用SQL:日期转换成周_DAYOFWEEK(date)

有时候需要将查询出来的日期转换成周几&#xff0c;Mysql本身语法就是支持这种转换的&#xff0c;就是DAYOFWEEK()函数 语法格式&#xff1a;DAYOFWEEK(date) &#xff08;date&#xff1a;可以是指定的具体日期&#xff08; 如2024-06-29 &#xff09;&#xff0c;也可以是日期…

为什么word生成的PDF内容显示不全?

在现代办公环境中&#xff0c;将文档从一个格式转换为另一个格式是一个常见的任务。然而&#xff0c;有时候我们可能会遇到意想不到的问题&#xff0c;比如使用Word转换成PDF时&#xff0c;生成的PDF文件只显示了整个界面的四分之一内容。这种问题不仅令人困扰&#xff0c;也可…

斜率优化DP——AcWing 303. 运输小猫

斜率优化DP 定义 斜率优化DP&#xff08;Slope Optimization Dynamic Programming&#xff09;是一种高级动态规划技巧&#xff0c;用于优化具有特定形式的状态转移方程。它主要应用于那些状态转移涉及求极值&#xff08;如最小值或最大值&#xff09;的问题中&#xff0c;通…

CesiumJS【Basic】- #028 天空盒

文章目录 天空盒1 目标2 代码2.1 main.ts3 资源天空盒 1 目标 配置显示天空盒 2 代码 2.1 main.ts import * as Cesium from cesium;// 创建 Cesium Viewer 并配置地形数据和天空盒 const viewer = new Cesium.Viewer(

理解抽象工厂设计模式

目录 抽象工厂模式抽象工厂模式结构抽象工厂模式适合应用场景抽象工厂模式优缺点练手题目题目描述输入描述输出描述提示信息题解 抽象工厂模式 抽象工厂模式是一种创建型设计模式&#xff0c; 它能创建一系列相关的对象&#xff0c; 而无需指定其具体类。 抽象工厂模式结构 抽…

自定义一个MyBaits脱敏插件

自定义一个MyBaits脱敏插件 用于对查询结果中的敏感数据进行脱敏处理。这个插件将拦截ResultSetHandler对象的处理结果&#xff0c;对某些敏感字段进行脱敏。 插件实现步骤 创建脱敏插件类。注册插件。 1. 创建脱敏插件类 首先&#xff0c;我们创建一个自定义插件类 DataM…

深入理解策略梯度算法

策略梯度&#xff08;Policy Gradient&#xff09;算法是强化学习中的一种重要方法&#xff0c;通过优化策略以获得最大回报。本文将详细介绍策略梯度算法的基本原理&#xff0c;推导其数学公式&#xff0c;并提供具体的例子来指导其实现。 策略梯度算法的基本概念 在强化学习…

【Python3的内置函数和使用方法】

目录 Python 特点 Python 中文编码 Python 变量类型 Python列表 Python 元组 元组是另一个数据类型&#xff0c;类似于 List&#xff08;列表&#xff09; Python 字典 Python数据类型转换 Python 运算符 Python算术运算符 Python比较运算符 Python赋值运算符 Pyt…

一篇就够了,为你答疑解惑:锂电池一阶模型-离线参数辨识(附代码)

锂电池一阶模型-参数离线辨识 背景模型简介数据收集1. 最大可用容量实验2. 开路电压实验3. 混合动力脉冲特性实验离线辨识对应模型对应代码总结下期预告文章字数有点多,耐心不够的谨慎点击阅读。 下期继续讲解在线参数辨识方法。 背景 最近又在开始重新梳理锂电池建模仿真与S…

使用stat()函数的例子

代码&#xff1a; #include <sys/types.h> #include <sys/stat.h> #include <unistd.h> #include <stdio.h>int main(void) {struct stat st;if(-1stat("test.txt",&st)){printf("获得文件状态失败\n");return -1;}printf(&q…

Unidbg调用-补环境V2

1.B站 内部依赖自定义的SignedQuery对象,需要找到apk中的类并补充环境。 package com.nb.demo;import com.github.unidbg.AndroidEmulator

llama3模型部署时遇到的问题及解决方案

在llama3模型部署时&#xff0c;会遇到一系列问题&#xff0c;这里就作者所遇到的问题与解决方法分享一下。 注意&#xff1a;这里是从llama3 github主页上给的方法一步步做的&#xff0c;不适用于其他部署大模型的方法。 文章目录 ERROR 403&#xff1a;Forbidden安装依赖时出…

洛谷 P1548 [NOIP1997 普及组] 棋盘问题

题目 洛谷 P1548 [NOIP1997 普及组] 棋盘问题 [NOIP1997 普及组] 棋盘问题 题目背景 NOIP1997 普及组第一题 题目描述 设有一个 N M N \times M NM 方格的棋盘 ( 1 ≤ N ≤ 100 , 1 ≤ M ≤ 100 ) (1≤N≤100,1≤M≤100) (1≤N≤100,1≤M≤100) 求出该棋盘中包含有多少个正…

MySQL高级-MVCC-undo log 版本链

文章目录 1、undo log2、undo log 版本链2.1、然后&#xff0c;有四个并发事务同时在访问这张表。2.1.1、修改id为30记录&#xff0c;age改为32.1.2、修改id为30记录&#xff0c;name改为A32.1.3、修改id为30记录&#xff0c;age改为10 2.2、总结 1、undo log 回滚日志&#xf…

文件系统(操作系统实验)

实验内容 &#xff08;1&#xff09;在内存中开辟一个虚拟磁盘空间作为文件存储器&#xff0c; 在其上实现一个简单单用户文件系统。 在退出这个文件系统时&#xff0c;应将改虚拟文件系统保存到磁盘上&#xff0c; 以便下次可以将其恢复到内存的虚拟空间中。 &#xff08;2&…

数字孪生煤矿智能化综合管控平台

煤矿可视化通过图扑 HT 实现实时数据集成和三维建模仿真&#xff0c;呈现井下环境、设备状态和生产状况等多维度数据&#xff0c;帮助管理人员进行直观监控和精准分析。该技术提升了运营效率和安全水平&#xff0c;为煤矿作业提供了智能化的管理解决方案&#xff0c;有助于减少…

黑马点评DAY1|Redis入门、Redis安装

什么是Redis&#xff1f; redis是一种键值型数据库&#xff0c;内部所存的数据都是键值对的形式&#xff0c;例如&#xff0c;我们可以把一个用户数据存储为如下格式&#xff1a; 键值id$1600name张三age21 但是这样的存储方式&#xff0c;数据会显得非常松散&#xff0c;因…

云计算HCIE+RHCE学员的学习分享

大一下学期&#xff0c;我从学长嘴里了解到誉天教育&#xff0c;当时准备考RHCE&#xff0c;我也了解了很多培训机构&#xff0c;然后学长强烈给我推荐誉天&#xff0c;我就在誉天报名了RHCE的课程。 通过杨峰老师的教学&#xff0c;我学到了许多Linux知识&#xff0c;也了解了…

笔记本电脑部署VMware ESXi 6.0系统

正文共&#xff1a;888 字 18 图&#xff0c;预估阅读时间&#xff1a;1 分钟 前面我们介绍了在笔记本上安装Windows 11操作系统&#xff08;Windows 11升级不了&#xff1f;但Win10就要停服了啊&#xff01;来&#xff0c;我教你&#xff01;&#xff09;&#xff0c;也介绍了…

【单片机毕业设计选题24037】-基于STM32的电力系统电力参数无线监控系统

系统功能: 系统上电后&#xff0c;OLED显示“欢迎使用电力监控系统请稍后”&#xff0c;两秒后显示“Waiting..”等待ESP8266初始化完成&#xff0c; ESP8266初始化成功后进入正常页面显示&#xff0c; 第一行显示电压值&#xff08;单位V&#xff09; 第二行显示电流值&am…