FreeRTOS源码分析-13 内存管理

news2024/10/7 18:27:17

1 STM32编译过程及程序组成

1.1STM32编译过程

  • 从源文件到.o文件,使用了armcc工具和armasm工具,这些keil帮我们实现了。
  • .o不是可执行文件,只是目标文件。因为地址没有定义,没有在rom中定义地址
  • 从.o文件到镜像文件使用了armlink,是可执行文件.elf在linux常用,.axf是stm32的可执行文件,通过sw 或stlink可进行烧写。.map 内存分布,这个文件很重要,出现bug问题就要熟读map文件。
  • .axf到.bin 二进制或.hexASCII文件使用了fromelf工具。在线IAP更新的时候需要用bin文件,但是无法烧写.hex,可以通过上位机把.hex转换成二进制烧写

1.2 程序的组成

Stack 栈向下

heap 堆

unintialized data未初始化数据段

initialized data初始化数据段

text 代码段

补充:

栈空间和堆空间是指在计算机内存中用于存储数据和程序的两种不同的分配方式。

  1. 栈空间(Stack Space): 栈空间主要用于存储函数调用时的局部变量、函数参数以及函数调用过程中的一些上下文信息。 特点:

    • 栈空间的分配和释放是由编译器自动完成的,无需手动管理。
    • 栈空间的大小是固定的,由系统或编译器预先分配好,通常较小。
    • 栈空间的数据存取速度快,因为栈的存取操作仅仅是简单的指针移动操作。
  2. 堆空间(Heap Space): 堆空间主要用于存储动态分配的内存,在程序运行过程中通过特定的函数(如malloc或new)手动申请和释放。 特点:

    • 堆空间的分配和释放需要手动管理,开发人员负责申请和释放内存。
    • 堆空间的大小可动态调整,可以根据实际需求进行动态分配和释放。
    • 堆空间的数据存取速度相对较慢,因为需要进行动态内存管理,并且可能产生内存碎片。

总结: 栈空间主要用于存储函数调用过程中的局部变量和上下文信息,大小固定,由编译器自动管理;堆空间主要用于动态分配内存,大小可调,需要手动申请和释放。栈空间的访问速度更快,而堆空间具有更大的灵活性。在编程中,正确管理栈空间和堆空间的使用至关重要,以避免内存泄漏、溢出和访问错误等问题。

1.3 存储与运行

未初始化 数据会清0,初始化数据段程序执行时只读。

程序出现bug时,可分析此文件。 

1.4 MDK生成的主要文件分析

 

  • 1、Section Cross References  所有.o文件的包含关系
  • 2、Removing Unused input sections from the image  可以移除未使用的文件,可以减少文件量,如只需要printf,那么其他C库可以删除
  • 3、Image Symbol Table 镜像符号列表,所有全局变量和C函数,在哪个.o文件占用内存多少可以提现
  • 4、Memory Map of the image 文件中code怎么分布,ram怎么分布
  • 5、lmage component sizes 镜像组件大小

没有用到的移除 

 

 镜像符号列表,.o文件放在哪个地址,大小多少。

 全局

内存分布

整个代码内存分布,如果内存不够了,可以查看哪个文件占用内存最多

上述说明:

LR_IROM1 代表代码放到ROM中的大小

所有*.o 所有RO都可以放到ROM1

RW文件放到 RAM

这个文件主要的作用分散加载文件,可以指定heap、stack,或者外部ram在这个文件中修改加载

hex文件的组成:段大小、地址、长度、数据、校验。

.bin文件一般用于改参数,分析flash读写出现的问题等。

2 内存管理概念及应用

2.1 静态内存与动态内存的区别

静态内存:

静态内存是指在程序开始运行时由编译器分配的内存,它的分配是在程序开始编译时完成的,不占用CPU资源。 程序中的各种变量,在编译时系统已经为其分配了所需的内存空间,当该变量在作用域内使用完毕时,系统会 自动释放所占用的内存空间。 变量的分配与释放,都无须程序员自行考虑。

动态内存:

动态内存分配是按输入信息的大小分配所需要的内存单元,他的特点是按需分配,内存分配在堆区。 用户无法确定空间大小,或者空间太大,栈上无法分配时,会采用动态内存分配。

区别:

  • 时间不同: 静态分配发生在程序编译和连接的时候。动态分配则发生在程序调入和执行的时候。
  • 空间不同: 堆都是动态分配的,没有静态分配的堆。栈有2种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配由函数malloc进行分配。不过栈的动态分配和堆不同,他的动态分配是由编译器进行释放,无需我们手工实现。 

2.2 C标准库动态内存的缺陷

calloc 是一个在 C 语言中使用的函数,用于动态内存分配。它用于在堆内存上分配指定数量的连续字节,并将每个字节都初始化为零。

其中,num 表示要分配的元素数量,size 表示每个元素的大小(以字节为单位)。calloc 函数会分配 num * size 字节的内存,并返回一个指向新分配内存起始位置的指针。如果分配失败,则返回空指针 NULL

malloc 函数不同,calloc 在分配内存后会自动将每个字节初始化为零。这对于需要清零内存的情况非常方便,例如用于存储数组或结构体等数据结构。

下面是一个使用 calloc 函数动态分配一个包含 5 个整数的数组的例子:

#include <stdio.h>
#include <stdlib.h>

int main() {
    int* array = calloc(5, sizeof(int));
    if (array != NULL) {
        for (int i = 0; i < 5; i++) {
            printf("%d ", array[i]);
        }
        free(array); // 释放内存
    }
    return 0;
}

realloc 函数的语法如下:

其中,ptr 是指向之前通过 malloccallocrealloc 分配的内存块的指针,size 是要重新分配的内存块的新大小(以字节为单位)。realloc 函数会尝试将之前分配的内存块的大小调整为新的大小,并返回一个指向重新分配后内存块起始位置的指针。如果分配失败,则返回空指针 NULL

需要注意的是,realloc 在重新分配内存时可能会进行数据的复制和移动。如果旧的内存块可以满足新的大小,那么就会直接扩展或缩小原来的内存块。否则,realloc 会在其他地方重新分配一块新的内存,并将原先内存块中的数据复制到新的内存块中。

下面是一个使用 realloc 函数重新分配一个整数数组内存大小的例子:

#include <stdio.h>
#include <stdlib.h>

int main() {
    int* array = malloc(5 * sizeof(int)); // 初始分配 5 个整数的内存
    if (array != NULL) {
        // 使用内存...
        
        // 重新分配内存大小为 10 个整数
        int* new_array = realloc(array, 10 * sizeof(int));
        if (new_array != NULL) {
            array = new_array; // 更新指针
            
            // 使用重新分配后的内存...
            
            free(array); // 释放内存
        }
    }
    return 0;
}

这些函数在小型嵌入式系统中并不总是可用的,小型嵌入式设备中的 RAM 不足。

它们的实现可能非常的大,占据了相当大的一块代码空间。

他们几乎都不是安全的。

它们并不是确定的,每次调用这些函数执行的时间可能都不一样。

它们有可能产生碎片。

这两个函数会使得链接器配置得复杂。

如果允许堆空间的生长方向覆盖其他变量占据的内存,它们会成为 debug 的灾难。

如果用malloc,当80B和10B释放掉,100B成为了内存碎片,下面看下FreeRTOS怎么解决内存分配

2.3 FreeRTOS动态内存分配介绍

heap1:只能分配不能释放,适合不可能去删除的任务

 heap2:支持分配也支持释放,但是有内存碎片,可用空间会越来越少。释放的空间无法用。

 heap3:在heap2的基础上多了任务保护,类似锁,不会被打断

heap4:有分配,有释放,有管理(默认)

heap5:内部其实也是heap4,有内部ram也可以扩展外部ram。音视频会占用大量内存。

3 内存管理函数应用

3.1 功能需求

  • 1、当按键K3按下 申请内存空间
  • 2、当按键K4按下释放内存空间
  • 3、当按键K5按下打印剩余内存空间

3.2 API

 申请、释放,获取空闲内存

3.3 功能实现

修改按键代码,每个按键按下的时候都去处理

/* USER CODE END Header_Delay_Task */
void Delay_Task(void const * argument)
{
  /* USER CODE BEGIN Delay_Task */
	EventBits_t KeyEventBits;
	uint8_t *HeapBuf = NULL;
  /* Infinite loop */
  for(;;)
  {
	  /*
		1、修改事件标志组等待状态为 触发后清除标志
		2、检测按键,处理相关功能
	  
	  */
	  KeyEventBits = xEventGroupWaitBits(KeyEventGroup,
								KEY3_EVENT_BIT|KEY4_EVENT_BIT|KEY5_EVENT_BIT|KEY6_EVENT_BIT,
								pdTRUE,
								pdFALSE,
								portMAX_DELAY);
	  printf("Key is Down Key Event Bit is %x\r\n",KeyEventBits);
	  switch(KeyEventBits){
		  case KEY3_EVENT_BIT:
			  if(HeapBuf == NULL){
				HeapBuf = pvPortMalloc(100);
			  }
			  else{
				printf("plese press K4 free!\r\n");
			  }
				
			break;
		  case KEY4_EVENT_BIT:
			  if(HeapBuf != NULL){
				vPortFree(HeapBuf);
				HeapBuf = NULL;
			  }
			  else{
				printf("plese press K3 Malloc!\r\n");
			  }
							  
			break;		 
		  case KEY5_EVENT_BIT:
			  printf("Heap Free Size is%d\r\n",xPortGetFreeHeapSize());
			break;	  
		  default:
			  break;
	  
	  
	  }
		osDelay(10);
  }
  /* USER CODE END Delay_Task */
}

效果

第一次申请空间是12152

第二次申请空间是12264 

申请100字节,多出了12字节,为什么?下面分析原理。

4 内存管理实现原理

通用的内存管理需求:

  • 按照调用者的要求分配合适的大小的动态内存区,返回该内存块的首地址  
  • 如果没有足够的内存,则返回空指针
  • 用户不在使用该内存是可以调用Free函数释放该内存块,被释放的内存块在归还系统堆之后,可以被重新分配
  • 由于动态内存分配算法的重要性,要求快速分配算法并尽量减少内存碎片

4.1 heap_4内存管理方案(默认也是最经典)

特点:

  • 内存上是低地址,下是高地址,内存分配的地址是从低到高(可以测试打印一下malloc指针,如果第一次比第二次小,说明是低到高。
  • 内存需要字节对齐,遵循AACPS。为什么要对齐?内存分配并没用到系统堆,还是模拟了内存空间,实际是数组,目的是效率,也是8字节对齐。32位机一般是8字节对齐,64位机一般是16字节对齐。
  • 内存管理实际是单向链表的算法去维护,当释放的时候,会进行合并的判断,如果首地址和末地址相同即可合并。
  • 黑色部分代表已分配内存,红色区域代表未分配内存。其中都有两个变量,nNextFreeBlock表示一个链表的指针。释放的时候,内存进行了合并,图2->图3

首先了解控制块

4.2 heap_4内存块初始化

/*
	内存堆初始化
	1、宏定义
		#if portBYTE_ALIGNMENT == 8					对齐字节数
		#define portBYTE_ALIGNMENT_MASK ( 0x0007 )	对齐掩码
		#endif
	2、内存块链表节点长度
		这里保证可被8整除,内存操作效率高
	static const size_t xHeapStructSize	= ( sizeof( BlockLink_t ) + ( ( size_t ) ( portBYTE_ALIGNMENT - 1 ) ) ) & ~( ( size_t ) portBYTE_ALIGNMENT_MASK );
*/
static void prvHeapInit( void )
{
	BlockLink_t *pxFirstFreeBlock;
	uint8_t *pucAlignedHeap;
	size_t uxAddress;
	size_t xTotalHeapSize = configTOTAL_HEAP_SIZE;

	/* 获取内存堆数组首地址 */
	uxAddress = ( size_t ) ucHeap;
	//当addr 后三位不等于0,就需要字节对齐
	//不能被8整除,8字节对齐,就是能够被8整除
	if( ( uxAddress & portBYTE_ALIGNMENT_MASK ) != 0 )
	{
		//uxAddress = uxAddress +7???
		uxAddress += ( portBYTE_ALIGNMENT - 1 );
		//把后三位变成0,保证在8字节对齐上
		uxAddress &= ~( ( size_t ) portBYTE_ALIGNMENT_MASK );
		//获取了总可用长度,由低地址到高地址增长(对齐后地址减去数组地址)
		xTotalHeapSize -= uxAddress - ( size_t ) ucHeap;
	}
	//对齐后的首地址
	pucAlignedHeap = ( uint8_t * ) uxAddress;

	/* 
		初始化了头结点xStart 
		下一个可用空闲块为对齐后的首地址
		头结点的内存大小为0
		解析:
			xStart 分配在全局内存中
			不用于存储块记录,只用链表操作查找用
	*/
	xStart.pxNextFreeBlock = ( void * ) pucAlignedHeap;
	xStart.xBlockSize = ( size_t ) 0;

	/* 
		初始化尾节点
		1、获取整个内存堆尾地址
		2、减去一个链表节点长度
		3、再去保证地址在8字节对齐上
		4、把尾节点进行赋值
		5、尾节点下一个指向NULL

	*/
	uxAddress = ( ( size_t ) pucAlignedHeap ) + xTotalHeapSize;
	uxAddress -= xHeapStructSize;
	uxAddress &= ~( ( size_t ) portBYTE_ALIGNMENT_MASK );
	pxEnd = ( void * ) uxAddress;
	pxEnd->xBlockSize = 0;
	pxEnd->pxNextFreeBlock = NULL;

	/* 
		初始化内存堆的第一个空闲块
		1、首地址为内存堆可用空闲首地址
		2、减去尾地址,获取到可用空间大小
		3、下个指向尾节点
	*/
	pxFirstFreeBlock = ( void * ) pucAlignedHeap;
	pxFirstFreeBlock->xBlockSize = uxAddress - ( size_t ) pxFirstFreeBlock;
	pxFirstFreeBlock->pxNextFreeBlock = pxEnd;

	/* 更新剩余内存信息 */
	xMinimumEverFreeBytesRemaining = pxFirstFreeBlock->xBlockSize;
	xFreeBytesRemaining = pxFirstFreeBlock->xBlockSize;

	/*标志位置位,32Bit 最高位为1  主要用于判断内存块类型使用*/
	xBlockAllocatedBit = ( ( size_t ) 1 ) << ( ( sizeof( size_t ) * heapBITS_PER_BYTE ) - 1 );
}

初始化的概述图 

 

4.3 heap_4内存块申请

源码分析

/*
	内存块申请
	根据传入大小,返回内存块指针,无可用空间返回NULL
	1、全局变量
		static BlockLink_t xStart, *pxEnd = NULL;//赋值时就为空
*/
void *pvPortMalloc( size_t xWantedSize )
{
BlockLink_t *pxBlock, *pxPreviousBlock, *pxNewBlockLink;
void *pvReturn = NULL;
	//挂起调度器
	vTaskSuspendAll();
	{
		/*  */
		if( pxEnd == NULL )
		{
			//触发内存堆的初始化
			prvHeapInit();
		}
		else
		{
			mtCOVERAGE_TEST_MARKER();
		}

		/*
			1、最高位用于确定谁拥有区块-应用程序还是内核,所以它必须是0。

		*/
		if( ( xWantedSize & xBlockAllocatedBit ) == 0 )
		{
			/*  */
			if( xWantedSize > 0 )
			{
				//加上链表节点长度,这也就是解释了 我们申请了100字节,但是实际占用了112,为什么多出了12个字节
				xWantedSize += xHeapStructSize;

				/* 保证能被8整除 */
				if( ( xWantedSize & portBYTE_ALIGNMENT_MASK ) != 0x00 )
				{
					/* Byte alignment required. */
					xWantedSize += ( portBYTE_ALIGNMENT - ( xWantedSize & portBYTE_ALIGNMENT_MASK ) );
					configASSERT( ( xWantedSize & portBYTE_ALIGNMENT_MASK ) == 0 );
				}
				else
				{
					mtCOVERAGE_TEST_MARKER();
				}
			}
			else
			{
				mtCOVERAGE_TEST_MARKER();
			}
			//是否有可用空闲块
			if( ( xWantedSize > 0 ) && ( xWantedSize <= xFreeBytesRemaining ) )
			{
				/* 
					查找可用空闲块
					单向链表,先从头结点开始
					1、pxPreviousBlock 开始遍历
					
					遍历结束条件 大小满足或者没有空闲块了

				*/
				pxPreviousBlock = &xStart;
				pxBlock = xStart.pxNextFreeBlock;
				while( ( pxBlock->xBlockSize < xWantedSize ) && ( pxBlock->pxNextFreeBlock != NULL ) )
				{
					pxPreviousBlock = pxBlock;
					pxBlock = pxBlock->pxNextFreeBlock;
				}

				/* 是否有空闲块 */
				if( pxBlock != pxEnd )
				{
					/* 获取分配的内存首地址(块的首地址+节点长度) */
					pvReturn = ( void * ) ( ( ( uint8_t * ) pxPreviousBlock->pxNextFreeBlock ) + xHeapStructSize );

					/* 删除 已经分配的节点*/
					pxPreviousBlock->pxNextFreeBlock = pxBlock->pxNextFreeBlock;

					/* If the block is larger than required it can be split into
					two. */
					if( ( pxBlock->xBlockSize - xWantedSize ) > heapMINIMUM_BLOCK_SIZE )
					{
						/*获取剩余的空闲首地址*/
						pxNewBlockLink = ( void * ) ( ( ( uint8_t * ) pxBlock ) + xWantedSize );
						configASSERT( ( ( ( size_t ) pxNewBlockLink ) & portBYTE_ALIGNMENT_MASK ) == 0 );

						/* 计算剩余长度 */
						pxNewBlockLink->xBlockSize = pxBlock->xBlockSize - xWantedSize;
						pxBlock->xBlockSize = xWantedSize;

						/* 插入到空闲链表中去 */
						prvInsertBlockIntoFreeList( pxNewBlockLink );
					}
					else
					{
						mtCOVERAGE_TEST_MARKER();
					}
					//更新我们的剩余空闲大小
					xFreeBytesRemaining -= pxBlock->xBlockSize;

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

					/* 将分配出去的内存块,进行标记 */
					pxBlock->xBlockSize |= xBlockAllocatedBit;
					pxBlock->pxNextFreeBlock = NULL;
				}
				else
				{
					mtCOVERAGE_TEST_MARKER();
				}
			}
			else
			{
				mtCOVERAGE_TEST_MARKER();
			}
		}
		else
		{
			mtCOVERAGE_TEST_MARKER();
		}

		traceMALLOC( pvReturn, xWantedSize );
	}
	//恢复调度器
	( void ) xTaskResumeAll();

	configASSERT( ( ( ( size_t ) pvReturn ) & ( size_t ) portBYTE_ALIGNMENT_MASK ) == 0 );
	return pvReturn;
}

3中的问题:申请100字节,多出了12字节,为什么?

xWantedSize += xHeapStructSize    加上链表节点长度,这也就是解释了 我们申请了100字节,但是实际占用了112,为什么多出了12个字节
                

4.4 heap_4内存块插入

1先找到内存卡

2判断首地址和尾地址是否一样,一样合并

3再判断下一个首地址和尾地址是否一样,一样合并更新

/*
	把内存块插入到空闲内存块中
*/
static void prvInsertBlockIntoFreeList( BlockLink_t *pxBlockToInsert )
{
	BlockLink_t *pxIterator;
	uint8_t *puc;

	/*  
		找到pxBlockToInsert位置
		pxIterator->pxNextFreeBlock > pxBlockToInsert 表示已经找到
		之后pxIterator地址在pxBlockToInsert实际物理地址之前
		
	*/
	for( pxIterator = &xStart; pxIterator->pxNextFreeBlock < pxBlockToInsert; pxIterator = pxIterator->pxNextFreeBlock )
	{
	}

	/* 
		判断是否可以合并 (向上合并)
		1、内存块长度进行累加
		2、要插入的地址,变成合并后的地址

	*/
	puc = ( uint8_t * ) pxIterator;
	if( ( puc + pxIterator->xBlockSize ) == ( uint8_t * ) pxBlockToInsert )
	{
		pxIterator->xBlockSize += pxBlockToInsert->xBlockSize;
		pxBlockToInsert = pxIterator;
	}
	else
	{
		mtCOVERAGE_TEST_MARKER();
	}

	/* 
		判断是否可以合并 (向下合并)
		1、再判断是否为尾节点
		2、内存块长度进行累加
		3、把要合并的内存块从空闲链表中移除
	
	
	*/
	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;
		}
	}
	/*
		1、这里没有向下合并操作,直接插入就可以,连接到空闲链表中
	
	*/
	else
	{
		pxBlockToInsert->pxNextFreeBlock = pxIterator->pxNextFreeBlock;
	}

	/* 
		1、这里没有向上合并操作,直接插入就可以,连接到空闲链表中
	
	*/
	if( pxIterator != pxBlockToInsert )
	{
		pxIterator->pxNextFreeBlock = pxBlockToInsert;
	}
	else
	{
		mtCOVERAGE_TEST_MARKER();
	}
}

 

4.5 heap_4内存块释放

/*

	释放内存块
	参数:传入要释放的内存块地址
*/
void vPortFree( void *pv )
{
uint8_t *puc = ( uint8_t * ) pv;
BlockLink_t *pxLink;

	//判断内存块有效
	if( pv != NULL )
	{
		/*获取传入内存块的节点地址 */
		puc -= xHeapStructSize;
		pxLink = ( void * ) puc;
		//判断最高位为1
		if( ( pxLink->xBlockSize & xBlockAllocatedBit ) != 0 )
		{
			//下个节点为NULL
			if( pxLink->pxNextFreeBlock == NULL )
			{
				/*最高位清除置位为0 */
				pxLink->xBlockSize &= ~xBlockAllocatedBit;
				//挂起调度器
				vTaskSuspendAll();
				{
					/* 更新剩余空间大小 */
					xFreeBytesRemaining += pxLink->xBlockSize;
					//插入到空闲链表中去
					prvInsertBlockIntoFreeList( ( ( BlockLink_t * ) pxLink ) );
				}
				//恢复调度器
				( void ) xTaskResumeAll();
			}
			else
			{
				mtCOVERAGE_TEST_MARKER();
			}
		}
		else
		{
			mtCOVERAGE_TEST_MARKER();
		}
	}
}

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

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

相关文章

C语言每日一练------------Day(7)

本专栏为c语言练习专栏&#xff0c;适合刚刚学完c语言的初学者。本专栏每天会不定时更新&#xff0c;通过每天练习&#xff0c;进一步对c语言的重难点知识进行更深入的学习。 今日练习题关键字&#xff1a;两个数组的交集     双指针 &#x1f493;博主csdn个人主页&#xf…

ELK原理和介绍

为什么用到ELK&#xff1a; 一般我们需要进行日志分析场景&#xff1a;直接在日志文件中 grep、awk 就可以获得自己想要的信息。但在规模较大的场景中&#xff0c;此方法效率低下&#xff0c;面临问题包括日志量太大如何归档、文本搜索太慢怎么办、如何多维度查询。需要集中化…

AUTOSAR配置与实践(深入篇)8.2 BSW的WatchDog功能-窗口狗

AUTOSAR配置与实践&#xff08;深入篇&#xff09;8.2 BSW的WatchDog功能-窗口狗 一、为什么使用窗口狗&#xff08;Window Watchdog&#xff09;二、窗口狗初始化和喂狗流程概要三、Wdg配置项详解四、窗口狗详细流程 一、为什么使用窗口狗&#xff08;Window Watchdog&#xf…

破局主键重复问题的坎坷路 | 京东物流技术团队

伴随着业务的不断发展&#xff0c;逐渐由单库单表向分库分表进行发展。在这个过程中不可避免的一个问题是确保主键要的唯一性&#xff0c;以便于后续的数据聚合、分析等等场景的使用。在进行分库分表的解决方案中有多种技术选型&#xff0c;大概分为两大类客户端分库分表、服务…

C语言每日一练------Day(10)

本专栏为c语言练习专栏&#xff0c;适合刚刚学完c语言的初学者。本专栏每天会不定时更新&#xff0c;通过每天练习&#xff0c;进一步对c语言的重难点知识进行更深入的学习。 今日练习题关键字&#xff1a;自除数 除自身以外数组的乘积 &#x1f493;博主csdn个人主页&#xff…

Redis 持久化和发布订阅

一、持久化 Redis 是内存数据库&#xff0c;如果不将内存中的数据库状态保存到磁盘&#xff0c;那么一旦服务器进程退出&#xff0c;服务器中的数据库状态也会消失。所以 Redis 提供了持久化功能&#xff01; 1.1、RDB&#xff08;Redis DataBase&#xff09; 1.1.1 …

web SSTI 刷题记录

文章目录 前言[CISCN 2019华东南]Web11[HDCTF 2023]SearchMaster[HNCTF 2022 WEEK2]ez_SSTI[HNCTF 2022 WEEK3]ssssti[NCTF 2018]flask真香方法一方法二 [安洵杯 2020]Normal SSTI[CISCN 2019华东南]Double Secret[HZNUCTF 2023 preliminary]flask方法一方法二 前言 学习ctf也…

安达发|模拟车间模型生成生产排产计划

根据车间模型生成排产计划的一般程序可简单地描述为下面6个步骤。 1. 建模 车间模型必须详细地捕捉生产流程的特征和相应的物流&#xff0c;以便以最小的成本生成可行的计划。由于一个系统的产出率只受潜在瓶颈资源的限制&#xff0c;因此&#xff0c;我们只需对车间现有全部资…

qt day 1

this->setWindowIcon(QIcon("D:\\zhuomian\\wodepeizhenshi.png"));//設置窗口的iconthis->setWindowTitle("鵬哥快聊");//更改名字this->setFixedSize(500,400);//設置尺寸QLabel *qlnew QLabel(this);//創建一個標簽ql->resize(QSize(500,20…

浅析SOLIDWORKS空模板警告 | 使用技巧

在使用SOLIDWORKS时&#xff0c;个别用户会遇到这样的问题&#xff1a;使用SOLIDWORKS打开【.step】【.x_t】等交换档会出现如下错误提示&#xff08;或者是另存实体为零件或装配体时&#xff09;&#xff1a; 这个时候可以点击“取消”来手动载入模板&#xff0c;但是下一次打…

说说Flink中的State

分析&回答 基本类型划分 在Flink中&#xff0c;按照基本类型&#xff0c;对State做了以下两类的划分&#xff1a; Keyed State&#xff0c;和Key有关的状态类型&#xff0c;它只能被基于KeyedStream之上的操作&#xff0c;方法所使用。我们可以从逻辑上理解这种状态是一…

论文浅尝 | 利用对抗攻击策略缓解预训练语言模型中的命名实体情感偏差问题...

笔记整理&#xff1a;田家琛&#xff0c;天津大学博士&#xff0c;研究方向为文本分类 链接&#xff1a;https://ojs.aaai.org/index.php/AAAI/article/view/26599 动机 近年来&#xff0c;随着预训练语言模型&#xff08;PLMs&#xff09;在情感分类领域的广泛应用&#xff0c…

性能测试平台RunnerGo

在当今的软件开发环境中&#xff0c;测试是确保产品质量和稳定性的重要环节。RunnerGo是一款基于Go语言研发的轻量级测试平台&#xff0c;以其强大的功能和易用性成为了测试领域的佼佼者。 RunnerGo具有轻量级、全栈式、易用性和高效率等特点&#xff0c;为用户提供了全面的测…

【C语言】冒泡排序的快排模拟

说到排序&#xff0c;必然绕不开两个排序&#xff0c;冒泡排序与快速排序 冒泡排序是大多数人的启蒙排序&#xff0c;因为他的算法简单。但效率不高&#xff0c;便于新手理解&#xff1b; 而快速排序是集大成之作&#xff0c;效率最高&#xff0c;使用最为广泛。 今天这篇文章带…

Ae 效果:CC Glass Wipe

过渡/CC Grid Wipe Transition/CC Grid Wipe CC Glass Wipe&#xff08;CC 玻璃擦除&#xff09;效果用于创建一种基于亮度信息的擦除方式&#xff0c;过渡边缘有类似于玻璃的质感。 ◆ ◆ ◆ 效果属性说明 Completion 完成度 控制过渡效果的完成进度。 值从 0 %&#xff08;…

自动驾驶车辆换道过程建模与分析

目 录 第1 章 绪论 ................................................................................................................. 1 1.1 研究背景及意义.............................................................................................. 1 1.2 研究…

ACM模式数组构建二叉树Go语言实现

目的 想输入一个数组&#xff0c;然后构造二叉树 例如数组为[6, 2, 8, 0, 4, 7, 9, -1, -1, 3, 5] 对应的二叉树为&#xff1a; 参考资料 ACM模式数组构建二叉树 重点&#xff1a;如果父节点的数组下标是i&#xff0c;那么它的左孩子下标就是i*21&#xff0c;右孩子下标就是…

Leetcode 易错题整理(二)40. 45. 46. 47. 49. 56. 62. 63.

40. 组合总和 II 给定一个候选人编号的集合 candidates 和一个目标数 target &#xff0c;找出 candidates 中所有可以使数字和为 target 的组合。 candidates 中的每个数字在每个组合中只能使用 一次 。 **注意&#xff1a;**解集不能包含重复的组合。 示例 1: 输入: candidat…

Doris数据库BE——Stream load

Doris是一款快速、可靠的分布式大数据仓库&#xff0c;是由阿里巴巴集团在2016年底开源发起的。它采用了分布式存储和计算技术&#xff0c;可以处理海量的数据&#xff0c;并且可以实现实时查询和快速分析。 Doris 数据仓库有以下特点&#xff1a; 分布式计算&#xff1a;利用…

结构体(个人学习笔记黑马学习)

1、结构体的定义和使用 #include <iostream> using namespace std; #include <string>struct Student {string name;int age;int score; }s3;int main() {//1、struct Student s1;s1.name "张三";s1.age 18;s1.score 100;cout << "姓名&a…