手写简易操作系统(二十)--实现堆内存管理

前情提要

前面我们实现了 0x80 中断,并实现了两个中断调用,getpidwrite,其中 write 还由于没有实现文件系统,是个残血版,这一节我们实现堆内存管理。

一、arena

在计算机科学中,“arena” 内存管理通常指的是一种内存分配和管理技术,它通常用于动态内存分配和释放。在这种管理方式下,程序会从系统中获取一大块内存,然后按需分割和管理这块内存,以满足程序中对内存的动态需求。

“arena” 内存管理的优点在于减少了频繁向操作系统请求内存的开销。相比之下,每次调用 malloc 或 free 都需要涉及到系统调用,而 “arena” 内存管理可以减少这种开销,因为它在一开始就获取了一大块内存,然后由程序自己来管理这块内存的分配和释放。

一般来说,“arena” 内存管理会维护一个或多个内存池(memory pool),从中分配内存给程序使用。内存池可以分割成多个固定大小的块,或者按需分配不同大小的内存块。内存分配器会跟踪这些内存块的使用情况,并确保在程序需要时能够高效地分配和释放内存。

许多现代的内存分配器都采用了 “arena” 内存管理的思想,以提高内存分配和释放的效率。这种技术在实际应用中有助于减少内存碎片化、提高性能,并且减少了向操作系统请求内存的次数。

换个说法就是,我准备好很多小块的内存,你需要哪种规格,我给你哪种规格。大于1KB,直接给你通过页框给你分配,小于16B,直接给16B。加入你需要53B,但是53不是一种规格,那么给你64B,64是一种规格,也刚刚好够你使用

1.1、arena数据结构

/* 内存块 */
struct mem_block {struct list_elem free_elem;
};/* 内存块描述符 */
struct mem_block_desc {uint32_t block_size;		 // 内存块大小uint32_t blocks_per_arena;	 // 本arena中可容纳此mem_block的数量.struct list free_list;    	 // 目前可用的mem_block链表
};/* 内存仓库arena元信息 */
struct arena {struct mem_block_desc* desc; // 此arena关联的mem_block_descuint32_t cnt;                // large为ture时,cnt表示的是页框数。否则cnt表示空闲mem_block数量bool large;
};struct mem_block_desc k_block_descs[DESC_CNT];	// 内核内存块描述符数组,其中规格,最小16Byte

首先是内存块描述符,内存块描述符描述的是我们有多少种规格的内存大小分类,这里我们设计的是七种。所以最后我们也生成了一个全局的内存块描述符数组。

内存块描述符中包含三个属性,一个是内存块的大小(16B,32B,64B…),另一个是arena中可以容纳的内存块数量,这个值是一个定值,比如我们的内存块为16B,arena结构体占一定的空间,剩下的空间全部被分为16B的内存块。最后是空闲的内存块的链表,这个链表链接的就是arena仓库中的内存块

arena也包含三个信息,arena关联的内存块描述符的指针,这个是为了标识这个arena的存储的类型,还有就是cnt数量,cnt数量在large为true时表示的页框数,这个很好理解,如果大于1024B的话,那么直接分配页框了,就将large标识为true。

image-20240329195055740

1.2、arena初始化

/* 内核内存块描述符数组初始化 */
void block_desc_init(struct mem_block_desc* desc_array) {uint32_t block_size = 16;// 初始化每个mem_block_desc描述符for (uint32_t desc_idx = 0; desc_idx < DESC_CNT; desc_idx++) {desc_array[desc_idx].block_size = block_size;// 初始化arena中的内存块数量desc_array[desc_idx].blocks_per_arena = (PG_SIZE - sizeof(struct arena)) / block_size;// 初始化每个描述符的空闲块链表list_init(&(desc_array[desc_idx].free_list));// 更新为下一个规格内存块block_size *= 2;}
}/* 返回arena中第idx个内存块的地址 */
static struct mem_block* arena2block(struct arena* a, uint32_t idx) {return (struct mem_block*)((uint32_t)a + sizeof(struct arena) + idx * a->desc->block_size);
}/* 返回内存块b所在的arena地址 */
static struct arena* block2arena(struct mem_block* b) {return (struct arena*)((uint32_t)b & 0xfffff000);
}

内核内存块描述符数组是在内核中是准备好了的,这里我们只是初始化。

二、malloc

/* 在堆中申请size字节内存 */
void* sys_malloc(uint32_t size) {enum pool_flags PF;        // 线程标识struct pool* mem_pool;     // 内核内存池或者用户内存池uint32_t pool_size;        // 内存池大小struct mem_block_desc* descs; // 内存块描述符struct task_struct* cur_thread = running_thread();if (cur_thread->pgdir == NULL) {     // 若为内核线程PF = PF_KERNEL;pool_size = kernel_pool.pool_size;mem_pool = &kernel_pool;descs = k_block_descs;}else {				                 // 若为用户线程PF = PF_USER;pool_size = user_pool.pool_size;mem_pool = &user_pool;descs = cur_thread->u_block_desc;}if (!(size > 0 && size < pool_size)) { // 若申请的内存不在内存池容量范围内则直接返回NULLreturn NULL;}struct arena* a;         // 内存仓库元信息struct mem_block* b;     // 内存块lock_acquire(&mem_pool->lock);if (size > 1024) {// 超过最大内存块1024, 就分配页框,需要的页框数为申请内存大小+内存块元信息uint32_t page_cnt = DIV_ROUND_UP(size + sizeof(struct arena), PG_SIZE);a = malloc_page(PF, page_cnt);if (a != NULL) {memset(a, 0, page_cnt * PG_SIZE);	 // 将分配的内存清0  /* 对于分配的大块页框,将desc置为NULL, cnt置为页框数,large置为true */a->desc = NULL;a->cnt = page_cnt;a->large = true;lock_release(&mem_pool->lock);return (void*)(a + 1);		 // 跨过arena大小,把剩下的内存返回}else {lock_release(&mem_pool->lock);return NULL;}}else {// 若申请的内存小于等于1024,可在各种规格的mem_block_desc中去适配uint8_t desc_idx;// 从内存块描述符中匹配合适的内存块规格for (desc_idx = 0; desc_idx < DESC_CNT; desc_idx++) {if (size <= descs[desc_idx].block_size) {  // 从小往大后,找到后退出break;}}// 若mem_block_desc的free_list中已经没有可用的mem_block, 就创建新的arena提供mem_blockif (list_empty(&descs[desc_idx].free_list)) {// 分配1页框做为arenaa = malloc_page(PF, 1);if (a == NULL) {lock_release(&mem_pool->lock);return NULL;}memset(a, 0, PG_SIZE);// 对于分配的小块内存,将desc置为相应内存块描述符,cnt置为此arena可用的内存块数,large置为falsea->desc = &descs[desc_idx];a->large = false;a->cnt = descs[desc_idx].blocks_per_arena;uint32_t block_idx;enum intr_status old_status = intr_disable();// 开始将arena拆分成内存块,并添加到内存块描述符的free_list中for (block_idx = 0; block_idx < descs[desc_idx].blocks_per_arena; block_idx++) {b = arena2block(a, block_idx);ASSERT(!elem_find(&a->desc->free_list, &b->free_elem));list_append(&a->desc->free_list, &b->free_elem);}intr_set_status(old_status);}/* 开始分配内存块 */b = elem2entry(struct mem_block, free_elem, list_pop(&(descs[desc_idx].free_list)));memset(b, 0, descs[desc_idx].block_size);a = block2arena(b);  // 获取内存块b所在的arenaa->cnt--;		     // 将此arena中的空闲内存块数减1lock_release(&mem_pool->lock);return (void*)b;}
}

malloc函数较为复杂,首先是判断当前是内核线程还是用户进程,这是不一样的,因为内核线程有内核线程的物理内存池,用户进程有用户进程的物理内存池。每次分配内存要在不同的内存池内分配。

申请的内存以byte为单位,如果大于1024,那么直接分配连续的连续的页。计算出需要多少页,然后通过 malloc_page 函数申请,返回其虚拟地址。此时其实已经为销毁这部分占用内存埋下了伏笔。让我们看一下现在我们分配的内存张啥样

image-20240329205115097

可以看到这里分配的页数是2,找到虚拟地址连续的两页,然后cnt=2,large=true,表示我们直接分配的页框,如果药销毁的话,传入的就是现在的地址,根据现在的地址,我们就可以修改相应的线程或者进程的虚拟地址池和物理地址池,达到销毁的目的。

如果申请的是小于1024的内存呢?比如64Byte。

image-20240329210024260

那就申请一个页,然后创建成这样的arena仓库,此时的cnt为当前可用的内存块数量,desc指向了内存描述符,内存描述符中有一个链表,链表把未分配的地址链接了起来,这是由于未分配的地址最前面放置了一个链表节点结构体,并在初始化这个arena仓库时将所有的空闲块都链接了起来,在释放时,也需要将释放地址前端再初始化为链表节点,加入空闲队列。

三、sys_free

理解了mallloc,再看free就简单很多了,他俩就是两个相互对应的过程,内存怎么分配的就怎么释放,

/* 将物理地址pg_phy_addr回收到物理内存池,这里的回收以页为单位 */
void pfree(uint32_t pg_phy_addr) {struct pool* mem_pool;uint32_t bit_idx = 0;if (pg_phy_addr >= user_pool.phy_addr_start) {         // 用户物理内存池mem_pool = &user_pool;bit_idx = (pg_phy_addr - user_pool.phy_addr_start) / PG_SIZE;}else {	                                               // 内核物理内存池mem_pool = &kernel_pool;bit_idx = (pg_phy_addr - kernel_pool.phy_addr_start) / PG_SIZE;}bitmap_set(&mem_pool->pool_bitmap, bit_idx, 0);	 // 将位图中该位清0
}/* 去掉页表中虚拟地址vaddr的映射,只去掉vaddr对应的pte */
static void page_table_pte_remove(uint32_t vaddr) {uint32_t* pte = pte_ptr(vaddr);*pte &= ~PG_P_1;	                                   // 将页表项pte的P位置0,不需要删除pdeasm volatile ("invlpg %0"::"m" (vaddr) : "memory");    // 更新tlb
}/* 在虚拟地址池中释放以_vaddr起始的连续pg_cnt个虚拟页地址 */
static void vaddr_remove(enum pool_flags pf, void* _vaddr, uint32_t pg_cnt) {uint32_t bit_idx_start = 0;uint32_t vaddr = (uint32_t)_vaddr;uint32_t cnt = 0;if (pf == PF_KERNEL) {// 内核虚拟内存池bit_idx_start = (vaddr - kernel_vaddr.vaddr_start) / PG_SIZE;while (cnt < pg_cnt) {bitmap_set(&kernel_vaddr.vaddr_bitmap, bit_idx_start + cnt++, 0);}}else if (pf == PF_USER) {// 用户虚拟内存池struct task_struct* cur_thread = running_thread();bit_idx_start = (vaddr - cur_thread->userprog_vaddr.vaddr_start) / PG_SIZE;while (cnt < pg_cnt) {bitmap_set(&cur_thread->userprog_vaddr.vaddr_bitmap, bit_idx_start + cnt++, 0);}}else {PANIC("vaddr_remove error!\n");}
}/* 释放以虚拟地址vaddr为起始的cnt个页框,vaddr必须是页框起始地址 */
void mfree_page(enum pool_flags pf, void* _vaddr, uint32_t pg_cnt) {uint32_t vaddr = (int32_t)_vaddr;uint32_t page_cnt = 0;// 确保虚拟地址是页框的起始ASSERT(pg_cnt >= 1 && vaddr % PG_SIZE == 0);// 获取虚拟地址vaddr对应的物理地址uint32_t pg_phy_addr = addr_v2p(vaddr);// 确保物理地址也是页框的起始ASSERT((pg_phy_addr % PG_SIZE) == 0);// 确保待释放的物理内存在低端1M+1k大小的页目录+1k大小的页表地址范围外ASSERT(pg_phy_addr >= 0x102000);if (pg_phy_addr >= user_pool.phy_addr_start) {// 位于user_pool内存池,要释放的是用户内存for (page_cnt = 0; page_cnt < pg_cnt; page_cnt++) {vaddr = (int)_vaddr + PG_SIZE * page_cnt;pg_phy_addr = addr_v2p(vaddr);// 确保物理地址属于用户物理内存池 ASSERT((pg_phy_addr % PG_SIZE) == 0);ASSERT(pg_phy_addr >= user_pool.phy_addr_start);// 先将对应的物理页框归还到内存池pfree(pg_phy_addr);// 再从页表中清除此虚拟地址所在的页表项ptepage_table_pte_remove(vaddr);}}else {// 位于kernel_pool内存池,要释放的是内核内存for (page_cnt = 0; page_cnt < pg_cnt; page_cnt++) {vaddr = (int)_vaddr + PG_SIZE * page_cnt;// 获得物理地址pg_phy_addr = addr_v2p(vaddr);// 确保待释放的物理内存只属于内核物理内存池ASSERT((pg_phy_addr % PG_SIZE) == 0);ASSERT(pg_phy_addr >= kernel_pool.phy_addr_start);ASSERT(pg_phy_addr < user_pool.phy_addr_start);// 先将对应的物理页框归还到内存池pfree(pg_phy_addr);// 再从页表中清除此虚拟地址所在的页表项ptepage_table_pte_remove(vaddr);}}// 清空虚拟地址的位图中的相应位vaddr_remove(pf, _vaddr, pg_cnt);
}/* 回收堆内存 */
void sys_free(void* ptr) {ASSERT(ptr != NULL);if (ptr == NULL) return;enum pool_flags PF;        // 回收的是内核还是用户的内存struct pool* mem_pool;     // 内核用户池或者用户内存池/* 判断是线程还是进程 */if (running_thread()->pgdir == NULL) {ASSERT((uint32_t)ptr >= K_HEAP_START);PF = PF_KERNEL;mem_pool = &kernel_pool;}else {PF = PF_USER;mem_pool = &user_pool;}lock_acquire(&mem_pool->lock);struct mem_block* b = ptr;struct arena* a = block2arena(b);	       // 把mem_block转换成arena,获取元信息,元信息在每个块的头部if (a->desc == NULL && a->large == true) { // 大于1024的内存mfree_page(PF, a, a->cnt);}else {// 小于等于1024的内存块,先将内存块回收到描述符的空闲列表list_append(&a->desc->free_list, &b->free_elem);// 将内存块元信息中的块数量加1a->cnt++;// 再判断此arena中的内存块是否都是空闲,如果是就释放这个arena块if (a->cnt == a->desc->blocks_per_arena) {// 先从空闲列表中逐个删除块for (uint32_t block_idx = 0; block_idx < a->desc->blocks_per_arena; block_idx++) {struct mem_block* b = arena2block(a, block_idx);ASSERT(elem_find(&a->desc->free_list, &b->free_elem));list_remove(&b->free_elem);}// 在删除整个页mfree_page(PF, a, 1);}}lock_release(&mem_pool->lock);
}

这里需要注意的一点是,如果一个arena仓库全为空,那么就释放这个仓库所占页面。就是要一点,给一点,内存资源宝贵,只能这样抠抠搜搜。

四、用户调用

/* 初始化系统调用,也就是将syscall_table数组中绑定好确定的函数 */
void syscall_init(void) {put_str("syscall_init begin!\n");syscall_table[SYS_GETPID] = sys_getpid;syscall_table[SYS_WRITE] = sys_write;syscall_table[SYS_MALLOC] = sys_malloc;syscall_table[SYS_FREE] = sys_free;put_str("syscall_init done!\n");
}
/* 申请size字节大小的内存,并返回结果 */
void* malloc(uint32_t size) {return (void*)_syscall1(SYS_MALLOC, size);
}/* 释放ptr指向的内存 */
void free(void* ptr) {_syscall1(SYS_FREE, ptr);
}

用户调用的代码和之前的保持一致。

4.1、仿真

仿真的main如下

// os/src/kernel/main.c
#include "print.h"
#include "init.h"
#include "thread.h"
#include "interrupt.h"
#include "console.h"
#include "process.h"
#include "syscall_init.h"
#include "syscall.h"
#include "stdio.h"
#include "memory.h"void k_thread_a(void*);
void k_thread_b(void*);
void u_prog_a(void);
void u_prog_b(void);int main(void) {put_str("I am kernel\n");init_all();intr_enable();process_execute(u_prog_a, "u_prog_a");process_execute(u_prog_b, "u_prog_b");thread_start("k_thread_a", k_thread_a, "I am thread_a");thread_start("k_thread_b", k_thread_b, "I am thread_b");while (1);return 0;
}/* 在线程中运行的函数 */
void k_thread_a(void* arg) {void* addr1 = sys_malloc(256);void* addr2 = sys_malloc(255);void* addr3 = sys_malloc(254);printk(" k_thread_a malloc addr:0x%x, 0x%x, 0x%x\n", (int)addr1, (int)addr2, (int)addr3);int cpu_delay = 100000;while (cpu_delay-- > 0);sys_free(addr1);sys_free(addr2);sys_free(addr3);while (1);
}/* 在线程中运行的函数 */
void k_thread_b(void* arg) {void* addr1 = sys_malloc(256);void* addr2 = sys_malloc(255);void* addr3 = sys_malloc(254);printk(" k_thread_b malloc addr:0x%x, 0x%x, 0x%x\n", (int)addr1, (int)addr2, (int)addr3);int cpu_delay = 100000;while (cpu_delay-- > 0);sys_free(addr1);sys_free(addr2);sys_free(addr3);while (1);
}/* 测试用户进程 */
void u_prog_a(void) {void* addr1 = malloc(256);void* addr2 = malloc(255);void* addr3 = malloc(254);printf(" prog_a malloc addr:0x%x, 0x%x, 0x%x\n", (int)addr1, (int)addr2, (int)addr3);int cpu_delay = 100000;while (cpu_delay-- > 0);free(addr1);free(addr2);free(addr3);while (1);
}/* 测试用户进程 */
void u_prog_b(void) {void* addr1 = malloc(256);void* addr2 = malloc(255);void* addr3 = malloc(254);printf(" prog_b malloc addr:0x%x, 0x%x, 0x%x\n", (int)addr1, (int)addr2, (int)addr3);int cpu_delay = 100000;while (cpu_delay-- > 0);free(addr1);free(addr2);free(addr3);while (1);
}

image-20240329211057932

结束语

今天实现了系统调用 mallocfree,将堆内存管理实现,下一节将实现硬盘驱动,在硬盘驱动的基础上实现文件系统。

老规矩,本节的代码地址:https://github.com/lyajpunov/os

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

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

相关文章

DC-9靶场

一.环境搭建 1.下载地址 靶机下载地址&#xff1a;https://download.vulnhub.com/dc/DC-9.zip 2.虚拟机配置 设置虚拟机为nat&#xff0c;遇到错误点重试和是 开启虚拟机如下图所示 二.开始渗透 1. 信息收集 查找靶机的ip地址 arp-scan -l 发现靶机的ip地址为192.168.11…

如何使用单片机 pwm 控制 mos 管?

目录 选择适合的硬件 连接电路 编写代码 参考示例 程序一 程序二 测试与调试 注意事项 使用单片机&#xff08;如常见的Arduino、STM32等&#xff09;通过PWM&#xff08;脉冲宽度调制&#xff09;控制MOS管&#xff08;金属氧化物半导体场效应管&#xff09;是一种常见…

Linux使用Docker部署RStudio Server结合内网穿透实现公网访问本地服务

文章目录 前言1. 安装RStudio Server2. 本地访问3. Linux 安装cpolar4. 配置RStudio server公网访问地址5. 公网远程访问RStudio6. 固定RStudio公网地址 前言 RStudio Server 使你能够在 Linux 服务器上运行你所熟悉和喜爱的 RStudio IDE&#xff0c;并通过 Web 浏览器进行访问…

标准版IP地址证书

IP地址证书是一种网络安全工具&#xff0c;用于确保互联网通信中IP地址的所有权和真实性。它类似于为网站颁发的SSL/TLS证书&#xff0c;但专门针对IP地址。这种证书由受信任的第三方机构&#xff08;如证书颁发机构&#xff09;签发&#xff0c;包含公钥、所有者信息和有效期。…

【Linux】开始掌握进程控制吧!

送给大家一句话&#xff1a; 我并不期待人生可以一直过得很顺利&#xff0c;但我希望碰到人生难关的时候&#xff0c;自己可以是它的对手。—— 加缪 开始学习进程控制 1 前言2 进程创建2.1 fork函数初识2.2 fork函数返回值2.3 写时拷贝2.4 fork常规用法2.5 fork调用失败的原因…

Free RTOS day2

1.思维导图 2.使用PWMADC光敏电阻完成光控灯的实验 int adc_val0;//用于保存ADC采样得到的数值 float volt0;//用于保存电压值 int main(void) {MX_GPIO_Init();MX_DMA_Init();MX_TIM1_Init();MX_USART1_UART_Init();MX_ADC_Init();MX_TIM3_Init();HAL_TIM_PWM_Start(&hti…

【linux】基础IO(一)

文件只有站在系统层面才能彻底理解 简单回顾一下文件&#xff1a; 首先我们要明确一点&#xff0c;我们说的打开文件不是写下fopen就打开文件&#xff0c;而是当我们的进程运行起来&#xff0c;进程打开的文件。 我们在C语言一般都会使用过如下的代码进行向文件中写入 但是除…

GUID测试程序

全局唯一标识符&#xff08;GUID&#xff0c;Globally Unique Identifier&#xff09;是一种由算法生成的二进制长度为128位的数字标识符。GUID主要用于在拥有多个节点、多台计算机的网络或系统中。在理想情况下&#xff0c;任何计算机和计算机集群都不会生成两个相同的GUID。G…

Clickhouse-表引擎探索之MergeTree

引言 前文曾说过&#xff0c;Clickhouse是一个强大的数据库Clickhouse-一个潜力无限的大数据分析数据库系统 其中一个强大的点就在于支持各类表引擎以用于不同的业务场景。 MergeTree MergeTree系列的引擎被设计用于插入极大量的数据到一张表当中。数据可以以数据片段的形式一…

手写SpringBoot(二)之动态切换Servlet容器

系列文章目录 手写SpringBoot&#xff08;一&#xff09;之简易版SpringBoot 手写SpringBoot&#xff08;二&#xff09;之动态切换Servlet容器 手写SpringBoot&#xff08;三&#xff09;之自动配置 手写SpringBoot&#xff08;四&#xff09;之bean动态加载 手写SpringBoot&…

衍生品交易概况

场内 场外 交易台架构 报价、交易、研究、程序个股、股指Flow、Exotic线性、非线性 对冲管理 管理风险敞口 做好情景分析 尊重市场选择 及时调整策略 理解头寸 善于学习 场外衍生品交易员的一天 盘前 回顾市场、决定今天总体方向处理隔夜敞口 盘中 处理客户询价…

C语言中入门到实战————动态内存管理

目录 前言 一、为什么要有动态内存分配 二、 malloc和free 2.1 malloc 2.2 free 三、calloc和realloc 3.1 calloc 3.2 realloc 四. 常见的动态内存的错误 4.1 对NULL指针的解引用操作 4.2 对动态开辟空间的越界访问 4.3 对非动态开辟内存使用free释放 4.4 使…

【算法】01背包问题(代码+详解+练习题)

题目&#xff1a; 有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。 第 i 件物品的体积是 vi&#xff0c;价值是 wi。 求解将哪些物品装入背包&#xff0c;可使这些物品的总体积不超过背包容量&#xff0c;且总价值最大。 输出最大价值。 输入格式 第一行两个整…

视频素材库有哪些网站?八大平台视频素材库创作推荐

视频创作的小达人们&#xff0c;是不是经常在想&#xff0c;视频素材库有哪些网站能提供高质量的素材呢&#xff1f;别担心&#xff0c;今天我要为你们揭秘八个超棒的视频素材网站&#xff0c;让你的视频制作更加轻松在创作的路上如鱼得水&#xff01; 蛙学网&#xff1a;海量…

深入探索Yarn:安装与使用指南

Yarn 是一个由 Facebook 开发的 JavaScript 包管理器&#xff0c;旨在提供更快、更可靠的包管理体验。它与 npm 类似&#xff0c;但在某些方面更加高效和可靠。本文将介绍如何安装 Yarn&#xff0c;并展示如何使用它来管理 JavaScript 项目的依赖。 1. 安装 Yarn Yarn 可以通…

三步提升IEDA下载速度——修改IDEA中镜像地址

找到IDEA的本地安装地址 D:\tool\IntelliJ IDEA 2022.2.4\plugins\maven\lib\maven3\conf 搜索阿里云maven仓库 复制https://developer.aliyun.com/mvn/guide中红框部分代码 这里也是一样的&#xff1a; <mirror><id>aliyunmaven</id><mirrorOf>*&…

【c++初阶】类与对象(下)

✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅ ✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨ &#x1f33f;&#x1f33f;&#x1f33f;&#x1f33f;&#x1f33f;&#x1f33f;&#x1f33f;&#x1f33f;&#x1f33f;&#x1f33f;&#x1f33f;&#x1f33f;&#x1f33f;&#x1f33f;&#x1…

ESP32学习---ESP-NOW

ESP32学习---ESP-NOW 基于Arduino IDE环境获取mac地址单播通讯一对多通讯多对一通讯多对多通讯模块1代码模块2模块3 广播通讯 基于ESP-IDF框架 乐鑫编程指南中关于ESP-NOW的介绍&#xff1a;https://docs.espressif.com/projects/esp-idf/zh_CN/v5.2.1/esp32/api-reference/net…

7.1 Mysql shell 定时备份

直接上脚本----linu 定时任务执行 #!/bin/bash# 配置信息 DB_USER"your_username" # 数据库用户名 DB_PASSWORD"your_password" # 数据库密码 DB_NAME"your_database_name" # 要备份的数据库名 BACKUP_DIR"/path/to/backup/directory"…

如何在Ubuntu系统部署Z-blog博客结合cpolar实现无公网IP访问本地网站

文章目录 1. 前言2. Z-blog网站搭建2.1 XAMPP环境设置2.2 Z-blog安装2.3 Z-blog网页测试2.4 Cpolar安装和注册 3. 本地网页发布3.1. Cpolar云端设置3.2 Cpolar本地设置 4. 公网访问测试5. 结语 1. 前言 想要成为一个合格的技术宅或程序员&#xff0c;自己搭建网站制作网页是绕…