FreeRTOS:4.内存管理

news2024/11/30 8:54:25

FreeRTOS内存管理

目录

  • FreeRTOS内存管理
    • 1. 为什么不直接使用C库函数的malloc和free函数
    • 2. FreeRTOS的五种内存管理方式
    • 3. heap4源码分析
      • 3.1 堆内存池
      • 3.2 内存块的链表数据结构
      • 3.3 堆的初始化
      • 3.4 堆的内存分配
      • 3.5 堆的内存释放
    • 4. 总结

1. 为什么不直接使用C库函数的malloc和free函数

在C语言的库函数中,有malloc、free等函数,但是在FreeRTOS中,它们不适用:

  • 不适合用在资源紧缺的嵌入式系统中
  • 这些函数的实现过于复杂、占据的代码空间太大
  • 并非线程安全的(thread-safe)
  • 运行有不确定性:每次调用这些函数时花费的时间可能都不相同
  • 内存碎片化
  • 使用不同的编译器时,需要进行复杂的配置
  • 有时候难以调试

2. FreeRTOS的五种内存管理方式

FreeRTOS中内存管理的接口函数为:pvPortMalloc、vPortFree,对应于C库的malloc、free。

在这里插入图片描述

  • heap_1:只分配,不回收;只实现了pvPortMalloc,没有实现vPortFree,因此不会产生碎片问题,分配时间确定
  • heap_2:采用最佳匹配算法,会产生大量内存碎片,没有对内存碎片进行合并,分配时间不定
  • heap_3:采用标准C库的malloc、free,由于并非线程安全,因此heap_3中先暂停rtos调度器,再进行分配,会产生内存碎片,时间不定
  • heap_4:是目前常用的堆管理方式,采用首次适应算法,能够合并相邻内存块,减少内存碎片,同样分配时间不定
  • heap_5:在heap_4的基础上,它可以管理多块、分隔开的内存。

3. heap4源码分析

目前heap_4的使用最为广泛,本文基于heap_4.c的源码进行进一步分析。

源码路径:Middlewares\Third_Party\FreeRTOS\Source\portable\MemMang\heap_4.c``

3.1 堆内存池

/* Allocate the memory for the heap. */
#if( configAPPLICATION_ALLOCATED_HEAP == 1 )
	/* The application writer has already defined the array used for the RTOS
	heap - probably so it can be placed in a special segment or address. */
	extern uint8_t ucHeap[ configTOTAL_HEAP_SIZE ];
#else
	static uint8_t ucHeap[ configTOTAL_HEAP_SIZE ];
#endif /* configAPPLICATION_ALLOCATED_HEAP */

heap4采用的是线性数组结构,大小为configTOTAL_HEAP_SIZE个字节

如果定义了configAPPLICATION_ALLOCATED_HEAP宏,可以由用户层自行定义内存池的位置

3.2 内存块的链表数据结构

/* Define the linked list structure.  This is used to link free blocks in order
of their memory address. */
typedef struct A_BLOCK_LINK
{
	struct A_BLOCK_LINK *pxNextFreeBlock;	/*<< The next free block in the list. */
	size_t xBlockSize;						/*<< The size of the free block. */
} BlockLink_t;

通过BlockLink_t这个链表对空闲的内存块进行管理,这个结构体包括内存块的大小以及指向下一个内存块头的指针,并且由两个静态链表指针xStart、pxEnd来标识开头和结尾。除了xStart以外,其余内存控制块都是在内存池中,用指针的形式进行访问。也就是说,xStart是作为哨兵节点,xStart的pxNextFreeBlock指针就是第一个空闲块节点。

static BlockLink_t xStart, *pxEnd = NULL;

另外,heapMINIMUM_BLOCK_SIZE限制了内存块的最小值,当内存块小于heapMINIMUM_BLOCK_SIZE时,就不再维护这个内存碎片了。

3.3 堆的初始化

通过prvHeapInit函数完成堆的初始化

static void prvHeapInit( void )
{
BlockLink_t *pxFirstFreeBlock;
uint8_t *pucAlignedHeap;
size_t uxAddress;
size_t xTotalHeapSize = configTOTAL_HEAP_SIZE;

	/* Ensure the heap starts on a correctly aligned boundary. */
	uxAddress = ( size_t ) ucHeap;
	// 内存对齐
	if( ( uxAddress & portBYTE_ALIGNMENT_MASK ) != 0 )
	{
		uxAddress += ( portBYTE_ALIGNMENT - 1 );
		uxAddress &= ~( ( size_t ) portBYTE_ALIGNMENT_MASK );
		xTotalHeapSize -= uxAddress - ( size_t ) ucHeap;
	}

	pucAlignedHeap = ( uint8_t * ) uxAddress;
	// 初始化xStart
	/* xStart is used to hold a pointer to the first item in the list of free
	blocks.  The void cast is used to prevent compiler warnings. */
	xStart.pxNextFreeBlock = ( void * ) pucAlignedHeap;
	xStart.xBlockSize = ( size_t ) 0;

	/* pxEnd is used to mark the end of the list of free blocks and is inserted
	at the end of the heap space. */
    // 初始化pxEnd
	uxAddress = ( ( size_t ) pucAlignedHeap ) + xTotalHeapSize;
	uxAddress -= xHeapStructSize;
	uxAddress &= ~( ( size_t ) portBYTE_ALIGNMENT_MASK );
	pxEnd = ( void * ) uxAddress;
	pxEnd->xBlockSize = 0;
	pxEnd->pxNextFreeBlock = NULL;

	/* To start with there is a single free block that is sized to take up the
	entire heap space, minus the space taken by pxEnd. */
	pxFirstFreeBlock = ( void * ) pucAlignedHeap;
	pxFirstFreeBlock->xBlockSize = uxAddress - ( size_t ) pxFirstFreeBlock;
	pxFirstFreeBlock->pxNextFreeBlock = pxEnd;

	/* Only one block exists - and it covers the entire usable heap space. */
	xMinimumEverFreeBytesRemaining = pxFirstFreeBlock->xBlockSize;
	xFreeBytesRemaining = pxFirstFreeBlock->xBlockSize;

	/* Work out the position of the top bit in a size_t variable. */
	xBlockAllocatedBit = ( ( size_t ) 1 ) << ( ( sizeof( size_t ) * heapBITS_PER_BYTE ) - 1 );
}

该函数主要完成了内存块链表的初始化。初始化完成后,xStart.pxNextFreeBlock指向了内存池对齐后的首地址,pxEnd指针则指向了内存池末尾往前的xHeapStructSize个字节大小位置。

因此,除了内存对齐后减少的空间外,内存池末尾还留有xHeapStructSize个字节存放pxEnd指向的内存头,用于标识末尾。

/* The size of the structure placed at the beginning of each allocated memory
block must by correctly byte aligned. */
static const size_t xHeapStructSize	= ( sizeof( BlockLink_t ) + ( ( size_t ) ( portBYTE_ALIGNMENT - 1 ) ) ) & ~( ( size_t ) portBYTE_ALIGNMENT_MASK );

#define portBYTE_ALIGNMENT			8
#define portBYTE_ALIGNMENT_MASK ( 0x0007 )

在stm32中,xHeapStructSize为8字节。

3.4 堆的内存分配

通过pvPortMalloc函数完成堆的内存分配

void *pvPortMalloc( size_t xWantedSize )
{
BlockLink_t *pxBlock, *pxPreviousBlock, *pxNewBlockLink;
void *pvReturn = NULL;

	vTaskSuspendAll(); // 进入临界区
	{
		/* 如果是第一次调用pvPortMalloc,那么就调用prvHeapInit来初始化堆内存池 */
		if( pxEnd == NULL )
		{
			prvHeapInit();
		}

		// 判断申请的内存大小是否超过上限
		if( ( xWantedSize & xBlockAllocatedBit ) == 0 )
		{
			/* The wanted size is increased so it can contain a BlockLink_t
			structure in addition to the requested amount of bytes. */
			if( xWantedSize > 0 )
			{
				xWantedSize += xHeapStructSize; // 申请的内存要加上额外的内存头8字节

				/* Ensure that blocks are always aligned to the required number
				of bytes. */
				if( ( xWantedSize & portBYTE_ALIGNMENT_MASK ) != 0x00 )
				{
					/* 内存对齐 */
					xWantedSize += ( portBYTE_ALIGNMENT - ( xWantedSize & portBYTE_ALIGNMENT_MASK ) );
					
				}

			}
			// 想要申请的内存比剩余的空闲内存小
			if( ( xWantedSize > 0 ) && ( xWantedSize <= xFreeBytesRemaining ) )
			{
				/* 遍历各个空闲内存块,找到第一个空闲内存块,它的大小比想要申请的内存大 */
				pxPreviousBlock = &xStart;
				pxBlock = xStart.pxNextFreeBlock;
				while( ( pxBlock->xBlockSize < xWantedSize ) && ( pxBlock->pxNextFreeBlock != NULL ) )
				{
					pxPreviousBlock = pxBlock;
					pxBlock = pxBlock->pxNextFreeBlock;
				}

				/* If the end marker was reached then a block of adequate size
				was	not found. */
				if( pxBlock != pxEnd )
				{
					/* Return the memory space pointed to - jumping over the
					BlockLink_t structure at its start. */
					pvReturn = ( void * ) ( ( ( uint8_t * ) pxPreviousBlock->pxNextFreeBlock ) + xHeapStructSize );

					/* 把 pxBlock 从空闲链表中移除 */
					pxPreviousBlock->pxNextFreeBlock = pxBlock->pxNextFreeBlock;

					/* 判断产生的内存碎片是否大于规定的最小内存碎片 */
					if( ( pxBlock->xBlockSize - xWantedSize ) > heapMINIMUM_BLOCK_SIZE )
					{
						/* 创建一个新的空闲内存块 */
						pxNewBlockLink = ( void * ) ( ( ( uint8_t * ) pxBlock ) + xWantedSize );

						/* 计算两个内存块的大小 */
						pxNewBlockLink->xBlockSize = pxBlock->xBlockSize - xWantedSize;
						pxBlock->xBlockSize = xWantedSize;

						/* 将新的内存块插入到空闲链表中 */
						prvInsertBlockIntoFreeList( pxNewBlockLink );
					}
						/* 更新剩余的空闲内存 */
					xFreeBytesRemaining -= pxBlock->xBlockSize;

					if( xFreeBytesRemaining < xMinimumEverFreeBytesRemaining )
					{
						xMinimumEverFreeBytesRemaining = xFreeBytesRemaining;
					}

					/* The block is being returned - it is allocated and owned
					by the application and has no "next" block. */
					pxBlock->xBlockSize |= xBlockAllocatedBit;
					pxBlock->pxNextFreeBlock = NULL;
				}
			}
		}

		traceMALLOC( pvReturn, xWantedSize );
	}
	( void ) xTaskResumeAll(); // 退出临界区
	// 如果定义了分配内存的钩子函数并且分配失败,就去调用
	#if( configUSE_MALLOC_FAILED_HOOK == 1 )
	{
		if( pvReturn == NULL )
		{
			extern void vApplicationMallocFailedHook( void );
			vApplicationMallocFailedHook();
		}
	}
	#endif

	return pvReturn; // 返回分配的内存块(跳过了内存头部的)
}
/*-----------------------------------------------------------*/
  1. 当首次调用pvPortMalloc,会调用prvHeapInit对内存链表进行初始化
  2. 判断申请的内存大小是否超过上限
  3. 遍历空闲内存块链表,找到第一个内存块大小是足够分配的
  4. 将该内存块从链表中剔除,如果剩余空间大小超过了最小内存块大小,那么就创建新的内存块,并插回内存块链表
  5. 如果分配失败,并且用户注册了对应的回调函数,就去调用

注意到:动态内存对于内存的消耗是大于应用层申请的内存大小,原因在于内存对齐和内存块头部的开销导致。

堆的内存分配和内存释放,都涉及到将内存块插回空闲链表中,heap4_c实现了空闲内存可合并,一定程度上解决内存碎片问题,就在prvInsertBlockIntoFreeList函数中体现。

prvInsertBlockIntoFreeList函数如下:

static void prvInsertBlockIntoFreeList( BlockLink_t *pxBlockToInsert )
{
	BlockLink_t *pxIterator;
	uint8_t *puc;

	/* 找到一个空闲块,这个块的地址比插入的块地址低,下一个块的地址比插入的块地址高 */
	for( pxIterator = &xStart; pxIterator->pxNextFreeBlock < pxBlockToInsert; pxIterator = pxIterator->pxNextFreeBlock )
	{
		/* Nothing to do here, just iterate to the right position. */
	}

	/* 找到插入位置后, 判断这个位置的尾地址能否和插入块衔接起来,如果可以就合并 */
	puc = ( uint8_t * ) pxIterator;
	if( ( puc + pxIterator->xBlockSize ) == ( uint8_t * ) pxBlockToInsert )
	{
		pxIterator->xBlockSize += pxBlockToInsert->xBlockSize;
		pxBlockToInsert = pxIterator;
	}

	/* 判断插入块能否和 插入位置的下一个内存块衔接起来,如果可以就合并 */
	puc = ( uint8_t * ) pxBlockToInsert;
	if( ( puc + pxBlockToInsert->xBlockSize ) == ( uint8_t * ) pxIterator->pxNextFreeBlock )
	{
		if( pxIterator->pxNextFreeBlock != pxEnd )
		{
			/* Form one big block from the two blocks. */
			pxBlockToInsert->xBlockSize += pxIterator->pxNextFreeBlock->xBlockSize;
			pxBlockToInsert->pxNextFreeBlock = pxIterator->pxNextFreeBlock->pxNextFreeBlock;
		}
		else
		{
			pxBlockToInsert->pxNextFreeBlock = pxEnd;
		}
	}
	else
	{
		pxBlockToInsert->pxNextFreeBlock = pxIterator->pxNextFreeBlock;
	}

	/* If the block being inserted plugged a gab, so was merged with the block
	before and the block after, then it's pxNextFreeBlock pointer will have
	already been set, and should not be set here as that would make it point
	to itself. */
	if( pxIterator != pxBlockToInsert )
	{
		pxIterator->pxNextFreeBlock = pxBlockToInsert;
	}

}
  1. 首先在空闲内存链表中,找到插入位置,即这个块的地址比插入块地址低,这个块的下一个块地址比插入块地址高
  2. 判断插入块能否和左边的空闲内存块以及右边的空闲内存块合并,也就是边界相等,如果可以就合并成一个大的内存块

所以说:heap4_c所谓的空闲内存可合并,只是合并相邻的内存碎片,这是基于内存池的线性连续而设计的。

因此,为了尽可能的减少内存碎片,提升内存合并的作用,尽可能把上电后不释放的动态内存在初始化阶段申请(比如说动态分配的任务,包括TCB和任务栈),然后对于重复申请释放的动态内存,在初始化阶段结束后再分配和使用。也就是说,堆的前半部分内存都用于不释放的动态内存,然后后半部分就用来一些频繁申请释放的动态内存。

3.5 堆的内存释放

通过vPortFree函数完成堆的内存释放

vPortFree函数如下:

void vPortFree( void *pv )
{
uint8_t *puc = ( uint8_t * ) pv;
BlockLink_t *pxLink;

	if( pv != NULL )
	{
		/* The memory being freed will have an BlockLink_t structure immediately
		before it. */
		puc -= xHeapStructSize;

		/* This casting is to keep the compiler from issuing warnings. */
		pxLink = ( void * ) puc;

		/* Check the block is actually allocated. */
		configASSERT( ( pxLink->xBlockSize & xBlockAllocatedBit ) != 0 );
		configASSERT( pxLink->pxNextFreeBlock == NULL );

		if( ( pxLink->xBlockSize & xBlockAllocatedBit ) != 0 )
		{
			if( pxLink->pxNextFreeBlock == NULL )
			{
				/* The block is being returned to the heap - it is no longer
				allocated. */
				pxLink->xBlockSize &= ~xBlockAllocatedBit;

				vTaskSuspendAll();
				{
					/* Add this block to the list of free blocks. */
					xFreeBytesRemaining += pxLink->xBlockSize;
					traceFREE( pv, pxLink->xBlockSize );
					prvInsertBlockIntoFreeList( ( ( BlockLink_t * ) pxLink ) );
				}
				( void ) xTaskResumeAll();
			}
		}

	}
}
/*-----------------------------------------------------------*/

释放内存比较简单,就是把申请释放的内存地址前移xHeapStructSize找到内存头部的位置,

然后进入临界区,把这个内存块插入到空闲链表当中,并增大xFreeBytesRemaining变量(记录当前空闲内存大小的变量)的大小,最后退出临界区。

4. 总结

  • heap_4的堆内存池是建立在一片线性连续的内存上的(全局数组)。
  • 在申请和释放内存时,有查询空闲块链表的操作,其最坏的时间复杂度是On,时间是不确定的。
  • 另外由于需要内存头部来维护空闲链表以及内存对齐,这导致了实际可分配的动态内存小于分配的这个线性连续的内存。
  • heap4只能合并相邻的内存碎片,并不能彻底解决内存碎片问题。

参考学习:

freeRTOS动态内存heap4源码分析_freertos heap4-CSDN博客

【FreeRTOS】FreeRTOS内存管理的五种方式-CSDN博客

FreeRTOS 内存管理策略_freertos查看内存碎片功能-CSDN博客

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

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

相关文章

【已解决】引用官网的 Element-Message 消息框居然报错为什么呢?

vue 版本 &#xff1a; vue3 编程语言&#xff1a;JavaScript os: macos13 组件 &#xff1a;element-plus 问题组件&#xff1a; Message 信息框 问题&#xff1a;想学习使用 element 官网里的组件&#xff0c;我找到了message 消息提示&#xff0c;然后我就把代码复制下来放到…

日历选择组件(打卡,日期计划,日期选择,特别日期标志)-VUE3

自己封装的目的&#xff1a; 使用场景&#xff1a;打卡&#xff0c;日期计划&#xff0c;日期选择&#xff0c;特别日期标志 根据自己的需求可以定制化何样式 不依赖任何第三方插件或者组件&#xff0c; 效果图&#xff1a; 1、日历组件封装 <template><div clas…

把Vue项目从Window系统迁移到Mac系统的方案

不能启动vue ui 直接运行&#xff0c;会报错如下&#xff1a; failed to load config from /Users/xiaochen/IdeaProjects/ChatViewer-frontend/vite.config.tserror when starting dev server: Error: You installed esbuild for another platform than the one youre curre…

关于LLM:揭秘token与embedding的机制

「GPT4 Turbo 的上下文长度为 128K token」 「Claude 2.1 的上下文长度为 200K token」 听起来像是一些重要的细节&#xff0c;那么token到底是什么&#xff1f; 请看一句话——It’s over 9000&#xff01; 我们可以将其表示为 [“It’s”, “over”, “9000!”] 每个数组…

救命!接手了一个老项目,见到了从业10年以来最烂的代码!

后台回复“书籍”&#xff0c;免费领取《程序员书籍资料一份》 后台回复“5000”&#xff0c;免费领取面试技术学习资料一份 在程序员这个行业从业快10年了&#xff0c;每过几个月回头看看自己写的代码&#xff0c;都会觉得写的也太烂了&#xff0c;不敢想象是自己之前写的。…

CorelDRAW2024破解版看这里!免费分享

亲爱的设计爱好者们&#xff0c;你们好呀&#xff01;今天我要给大家种草一款神奇的软件——CorelDRAW 2024&#xff01;&#x1f929;&#x1f389; 作为一位软件技术爱好者&#xff0c;我一直在寻找那些能让我们事半功倍的工具。最近&#xff0c;我在数字设计领域发现了一个…

【2024最新华为OD-C/D卷试题汇总】[支持在线评测] 目录管理器(200分) - 三语言AC题解(Python/Java/Cpp)

🍭 大家好这里是清隆学长 ,一枚热爱算法的程序员 ✨ 本系列打算持续跟新华为OD-C/D卷的三语言AC题解 💻 ACM银牌🥈| 多次AK大厂笔试 | 编程一对一辅导 👏 感谢大家的订阅➕ 和 喜欢💗 📎在线评测链接 目录管理器(200分) 🌍 评测功能需要订阅专栏后私信联系清隆…

QShop商城-短信通知配置

QShop商城-短信通知配置 本系统短信通知配置可选阿里云/腾讯云,二者二选一即可. 阿里云短信 一、登录阿里云短信平台 阿里云短信平台管理地址&#xff1a;https://dysms.console.aliyun.com/dysms.html 二、账户ID和秘钥&#xff08;AccessKeyId 和 AccessKeySecret&#x…

认识一些分布函数-Frechet分布及其应用

1. 何为Frechet分布 Frechet分布也称为极值分布(EVD)类型II,用于对数据集中的最大值进行建模。它是四种常用极值分布之一。另外三种是古贝尔分布、威布尔分布和广义极值分布(Gumbel Distribution, the Weibull Distribution and the Generalized Extreme Value Distributi…

3D 图片悬停效果

3D 图片悬停效果 效果展示 CSS 知识点 background 属性的综合运用transform 属性的综合运用 页面整体布局 <div class"box"><span style"--i: 0"></span><span style"--i: 1"></span><span style"--i…

数据资产管理的未来趋势:洞察技术前沿,探讨数据资产管理在云计算、大数据、区块链等新技术下的发展趋势

一、引言 随着信息技术的飞速发展&#xff0c;数据已成为企业最重要的资产之一。数据资产管理作为企业核心竞争力的关键组成部分&#xff0c;其发展趋势和技术创新受到了广泛关注。特别是在云计算、大数据、区块链等新技术不断涌现的背景下&#xff0c;数据资产管理面临着前所…

常用的JDK调优监控工具整理

JVM 调优首先要做的就是监控 JVM 的运行状态&#xff0c;这就需要用到各种官方和第三方的工具包了 一、 JDK 工具包 JDK 自带的 JVM 工具可以分为命令行工具和可视化工具 命令行工具 jps: JVM Process status tool&#xff1a;JVM进程状态工具&#xff0c;查看进程基本信息j…

阻塞IO、非阻塞IO、IO复用的区别 ?(非常详细)零基础入门到精通,收藏这一篇就够了

前言 在《Unix网络编程》一书中提到了五种IO模型&#xff0c;分别是&#xff1a;阻塞IO、非阻塞IO、IO复用、信号驱动IO以及异步IO。本篇文章主要介绍IO的基本概念以及阻塞IO、非阻塞IO、IO复用三种模型&#xff0c;供大家参考学习。 一、什么是IO 计算机视角理解IO: 对于计…

关闭kylin(麒麟)系统的安全认证(烦人的安全认证)

打开grub sudo vim /etc/default/grup修改安全认证选项 增加12行&#xff0c;把13行注释掉 保存更改, 然后执行下面的命令&#xff1a; sudo sync sudo reboot重启成功后&#xff0c;就关闭了安全认证了~~~~~。 总体来讲&#xff0c;kylin还是基于ubuntu的内核的&#xff0c;…

多号朋友圈统一管理,自动转发是什么体验?

拥有多个微信号的你&#xff0c;是不是也觉得手动管理和发布多个朋友圈可能会非常耗时&#xff1f; 今天&#xff0c;就分享一个神器给你&#xff0c;让你可以高效管理多个微信号的朋友圈&#xff0c;并实现自动转发。 首先&#xff0c;你需要在个微管理系统上登录所有的微信…

停止游戏中的循环扣血显示

停止游戏中循环扣血并显示的具体实现方式会依赖于你的代码结构和游戏的逻辑。通常情况下&#xff0c;你可以通过以下方式来实现停止循环扣血和显示&#xff1a; 1、问题背景 在使用 Python 代码为游戏开发一个生命值条时&#xff0c;遇到了一个问题。代码使用了循环来减少生命…

【博客718】时序数据库基石:LSM Tree(log-structured merge-tree)

时序数据库基石&#xff1a;LSM Tree(log-structured merge-tree) 1、为什么需要LSM Tree LSM被设计来提供比传统的B树更好的写操作吞吐量&#xff0c;通过消去随机的本地更新操作来达到这个目标&#xff0c;使得写入都是顺序写&#xff0c;而不是随机写。 那么为什么这是一个…

Eclipse 单步调试的时候报错,通过一些设置处理下。

先帖张图&#xff1a; 勾选不提醒。 1、通过Java Compiler&#xff0c;进行设置: 然后设置以后&#xff0c;进入调试&#xff0c;还是 报上面的错&#xff0c;有的小伙伴说是先去勾选&#xff0c;然后确认。 然后再选择&#xff0c;确认。 2、设置Jdk为自己安装的。 设置成功后…

积木搭建游戏-第13届蓝桥杯省赛Python真题精选

[导读]&#xff1a;超平老师的Scratch蓝桥杯真题解读系列在推出之后&#xff0c;受到了广大老师和家长的好评&#xff0c;非常感谢各位的认可和厚爱。作为回馈&#xff0c;超平老师计划推出《Python蓝桥杯真题解析100讲》&#xff0c;这是解读系列的第83讲。 积木搭建游戏&…

LaTeX 学习 第2节 数学结构

----用教授的方式学习 目录 2.1 上标与下标 2.2 上下画线与花括号 2.3 分式 2.4 根式 2.5 矩阵 ​​​​​​​LaTex安装包&#xff1a;https://download.csdn.net/download/weixin_38135241/89416392 LaTex- windows安装包&#xff1a;https://download.csdn.net/down…