FreeRTOS 内存管理

news2024/11/16 13:55:52

文章目录

  • 一、FreeRTOS 内存管理简介
  • 二、 内存碎片
  • 三、heap_1 内存分配方法
    • 1. 分配方法简介
    • 2. 内存申请函数详解
    • 3. 内存释放函数详解
  • 四、heap_2 内存分配方法
    • 1. 分配方法简介
    • 2. 内存块详解
    • 3. 内存堆初始化函数详解
    • 4. 内存块插入函数详解
    • 5. 内存申请函数详解
    • 6. 内存释放函数详解
  • 五、未完期待。。。


一、FreeRTOS 内存管理简介

FreeRTOS 创建任务、队列、信号量等的时候有两种方法,一种是动态的申请所需的 RAM。一种是由用户自行定义所需的 RAM,这种方法也叫静态方法,使用静态方法的函数一般以“Static”结尾,比如任务创建函数 xTaskCreateStatic(),使用此函数创建任务的时候需要由用户定义任务堆栈,本章我们不讨论这种静态方法。

使用动态内存管理的时候 FreeRTOS 内核在创建任务、队列、信号量的时候会动态的申请RAM。标准 C 库中的 malloc()和 free()也可以实现动态内存管理,但是如下原因限制了其使用:
● 在小型的嵌入式系统中效率不高。
● 会占用很多的代码空间。
● 它们不是线程安全的。
● 具有不确定性,每次执行的时间不同。
● 会导致内存碎片。
● 使链接器的配置变得复杂。

不同的嵌入式系统对于内存分配和时间要求不同,因此一个内存分配算法可以作为系统的可选选项。FreeRTOS 将内存分配作为移植层的一部分,这样 FreeRTOS 使用者就可以使用自己的合适的内存分配方法。

当内核需要 RAM 的时候可以使用 pvPortMalloc()来替代 malloc()申请内存,不使用内存的时候可以使用 vPortFree()函数来替代 free()函数释放内存。函数 pvPortMalloc()、vPortFree()与函数 malloc()、free()的函数原型类似。

FreeRTOS 提供了 5 种内存分配方法,FreeRTOS 使用者可以其中的某一个方法,或者自己的内存分配方法。这 5 种方法是 5 个文件,分别为:heap_1.c、heap_2.c、heap_3.c、heap_4.c 和heap_5.c。这 5 个文件再 FreeRTOS 源码中,路径:FreeRTOS->Source->portable->MemMang。

二、 内存碎片

在看 FreeRTOS 的内存分配方法之前我们先来看一下什么叫做内存碎片,看名字就知道是小块的、碎片化的内存。那么内存碎片是怎么来的呢?内存碎片是伴随着内存申请和释放而来的,如下图所示:
在这里插入图片描述
(1)、此时内存堆还没有经过任何操作,为全新的。

(2)、此时经过第一次内存分配,一共分出去了 4 块内存块,大小分别为 80B、80B、10B 和100B。

(3)、有些应用使用完内存,进行了释放,从左往右第一个 80B 和后面的 10B 这两个内存块就是释放的内存。如果此时有个应用需要 50B 的内存,那么它可以从两个地方来获取到,一个是最前面的还没被分配过的剩余内存块,另一个就是刚刚释放出来的 80B 的内存块。但是很明显,刚刚释放出来的这个 10B 的内存块就没法用了,除非此时有另外一个应用所需要的内存小于 10B。

(4)、经过很多次的申请和释放以后,内存块被不断的分割、最终导致大量很小的内存块!也就是图中 80B 和 50B 这两个内存块之间的小内存块,这些内存块由于太小导致大多数应用无法使用,这些没法使用的内存块就沦为了内存碎片!

内存碎片是内存管理算法重点解决的一个问题,否则的话会导致实际可用的内存越来越少,最终应用程序因为分配不到合适的内存而奔溃!FreeRTOS 的 heap_4.c 就给我们提供了一个解决内存碎片的方法,那就是将内存碎片进行合并组成一个新的可用的大内存块。

三、heap_1 内存分配方法

1. 分配方法简介

动 态 内 存 分 配 需 要 一 个 内 存 堆 , FreeRTOS 中 的 内 存 堆 为 ucHeap[] , 大 小 为configTOTAL_HEAP_SIZE,这个前面讲 FreeRTOS 配置的时候就讲过了。不管是哪种内存分配方法,它们的内存堆都为 ucHeap[],而且大小都是configTOTAL_HEAP_SIZE。内存堆在文件heap_x.c(x 为 1~5)中定义的,比如 heap_1.c 文件就有如下定义:

#if( configAPPLICATION_ALLOCATED_HEAP == 1 )
extern uint8_t ucHeap[ configTOTAL_HEAP_SIZE ]; //需要用户自行定义内存堆
#else
static uint8_t ucHeap[ configTOTAL_HEAP_SIZE ]; //编译器决定
#endif 

当宏 configAPPLICATION_ALLOCATED_HEAP 为 1 的时候需要用户自行定义内存堆,否则的话由编译器来决定,默认都是由编译器来决定的。如果自己定义的话就可以将内存堆定义到外部 SRAM 或者 SDRAM 中。

heap_1 实现起来就是当需要 RAM 的时候就从一个大数组(内存堆)中分一小块出来,大数组(内存堆)的容量为 configTOTAL_HEAP_SIZE,上面已经说了。使用函数 xPortGetFreeHeapSize()可以获取内存堆中剩余内存大小。

heap_1 特性如下:
1、适用于那些一旦创建好任务、信号量和队列就再也不会删除的应用,实际上大多数的
FreeRTOS 应用都是这样的。
2、具有可确定性(执行所花费的时间大多数都是一样的),而且不会导致内存碎片。
3、代码实现和内存分配过程都非常简单,内存是从一个静态数组中分配到的,也就是适合于那些不需要动态内存分配的应用。

2. 内存申请函数详解

heap_1 的内存申请函数 pvPortMalloc()源码如下:

void *pvPortMalloc( size_t xWantedSize )
{
	void *pvReturn = NULL;
	static uint8_t *pucAlignedHeap = NULL;
	//确保字节对齐
	#if( portBYTE_ALIGNMENT != 1 ) (1)
	{
		if( xWantedSize & portBYTE_ALIGNMENT_MASK ) (2)
		{
			//需要进行字节对齐
			xWantedSize += ( portBYTE_ALIGNMENT - ( xWantedSize &\ (3)
			 portBYTE_ALIGNMENT_MASK ) );
		}
	}
	#endif
	vTaskSuspendAll(); (4)
	{
		if( pucAlignedHeap == NULL )
		{
			//确保内存堆的开始地址是字节对齐的
			pucAlignedHeap = ( uint8_t * ) ( ( ( portPOINTER_SIZE_TYPE )\ (5)
			 &ucHeap[ portBYTE_ALIGNMENT ] ) &\
			 ( ~( ( portPOINTER_SIZE_TYPE )\
			portBYTE_ALIGNMENT_MASK ) ) );
		}
		//检查是否有足够的内存供分配,有的话就分配内存
		if( ( ( xNextFreeByte + xWantedSize ) < configADJUSTED_HEAP_SIZE ) && (6)
		( ( xNextFreeByte + xWantedSize ) > xNextFreeByte ))
		{
			pvReturn = pucAlignedHeap + xNextFreeByte; (7)
			xNextFreeByte += xWantedSize; (8)
		}
		traceMALLOC( pvReturn, xWantedSize );
	}
	( void ) xTaskResumeAll(); (9)
	#if( configUSE_MALLOC_FAILED_HOOK == 1 ) (10)
	{
		if( pvReturn == NULL )
		{
			extern void vApplicationMallocFailedHook( void );
			vApplicationMallocFailedHook();
		}
	}
	#endif
	return pvReturn; (11)
}

(1)、是否需要进行字节对齐,宏 portBYTE_ALIGNMENT 是需要对齐的字节数,默认为 8,需要进行 8 字节对齐,也就是说参数 xWantedSize 要为 8 的倍数,如果不是的话就需要调整为8 的倍数。

(2)、参数xWantedSize与宏portBYTE_ALIGNMENT_MASK进行与运算来判断xWantedSize是否为 8 字节对齐,如果结果等于 0 就说明 xWantedSize 是 8 字节对齐的,否则的话不为 8 字节对齐,portBYTE_ALIGNMENT_MASK为0x0007。假如xWantedSize为13,那么13&0x0007=5,5大于0,所以13不为8的倍数,需要做字节对齐处理。当xWantedSize为16的时候,16&0x0007=0,所以 16 是 8 的倍数,无需字节对齐。

(3)、当 xWantedSize 不是 8 字节对齐的时候就需要调整为 8 字节对齐,调整方法就是找出大于它并且离它最近的那个 8 字节对齐的数,对于 13 来说就是 16。体现在代码中就是本行的这个公式,同样以 xWantedSize 为 13 为例,计算公式就是:
xWantedSize=13+(8-(13&0x0007))=13+(8-5)=16;

(4)、调用函数 vTaskSuspendAll()挂起任务调度器,因为申请内存过程中要做保护,不能被其他任务打断。

(5)、确保内存堆的可用起始地址也是 8 字节对齐的,内存堆 ucHeap 的起始地址是由编译器分配的,ucHeap 的起始地址不一定是 8 字节对齐的。但是我们在使用的时候肯定要使用一个8 字节对齐的起始地址,这个地址用 pucAlignedHeap 表示,同样需要用公式计算一下,公式就是本行代码,ucHeap 和 pucAlignedHeap 如下图所示:
在这里插入图片描述
图中内存堆 ucHeap 实际起始地址为 0x200006C4,这个地址不是 8 字节对齐的,所 以 不 能 拿 来 使 用 , 经 过 字 节 对 齐 以 后 可 以 使 用 的 开 始 地 址 是 0x200006C8 , 所 以pucAlignedHeap 就为 0x200006C8。

(6)、检查一下可用内存是否够分配,分配完成以后是否会产生越界(超出内存堆范围),
xNextFreeByte 是个全局变量,用来保存 pucAlignedHeap 到内存堆剩余内存首地址之间的偏移值,如下图所示:
在这里插入图片描述
(7)、如果内存够分配并且不会产生越界,那么就将申请到的内存首地址赋给 pvReturn,比如我们要申请 30 个字节(字节对齐以后实际需要申请 32 字节)的内存,申请过程如下图 所示:
在这里插入图片描述
(8)、内存申请完成以后更新一下变量 xNextFreeByte。

(9)、调用函数 xTaskResumeAll()恢复任务调度器。

(10)、宏 configUSE_MALLOC_FAILED_HOOK 为 1 的话就说明使能了内存申请失败钩子函数,因此会调用钩子函数 vApplicationMallocFailedHook(),此函数需要用户自行编写实现。

(11)、返回 pvRerurn 值,如果内存申请成功的话就是申请到的内存首地址,内存申请失败的话就返回 NULL。

3. 内存释放函数详解

heap_1 的内存释放函数为 pvFree(),可以看一下 pvFree()的源码,如下:

void vPortFree( void *pv )
{
	( void ) pv;
	configASSERT( pv == NULL );
}

可以看出 vPortFree()并没有具体释放内存的过程。因此如果使用 heap_1,一旦申请内存成功就不允许释放!但是 heap_1 的内存分配过程简单,如此看来 heap_1 似乎毫无任何使用价值啊。千万不能这么想,有很多小型的应用在系统一开始就创建好任务、信号量或队列等,在程序运行的整个过程这些任务和内核对象都不会删除,那么这个时候使用 heap_1 就很合适的。

四、heap_2 内存分配方法

1. 分配方法简介

heap_2提供了一个更好的分配算法,不像heap_1那样,heap_2提供了内存释放函数。heap_2不会把释放的内存块合并成一个大块,这样有一个缺点,随着你不断的申请内存,内存堆就会被分为很多个大小不一的内存(块),也就是会导致内存碎片!heap_4 提供了空闲内存块合并的功能。

heap_2 的特性如下:
1、可以使用在那些可能会重复的删除任务、队列、信号量等的应用中,要注意有内存碎片产生!

2、如果分配和释放的内存 n 大小是随机的,那么就要慎重使用了,比如下面的示例:
● 如果一个应用动态的创建和删除任务,而且任务需要分配的堆栈大小都是一样的,
那么 heap_2 就非常合适。如果任务所需的堆栈大小每次都是不同,那么 heap_2 就
不适合了,因为这样会导致内存碎片产生,最终导致任务分配不到合适的堆栈!不过 heap_4 就很适合这种场景了。
● 如果一个应用中所使用的队列存储区域每次都不同,那么 heap_2 就不适合了,和上
面一样,此时可以使用 heap_4。
● 应用需要调用 pvPortMalloc()和 vPortFree()来申请和释放内存,而不是通过其他
FreeRTOS 的其他 API 函数来间接的调用,这种情况下 heap_2 不适合。

3、如果应用中的任务、队列、信号量和互斥信号量具有不可预料性(如所需的内存大小不能确定,每次所需的内存都不相同,或者说大多数情况下所需的内存都是不同的)的话可能会导致内存碎片。虽然这是小概率事件,但是还是要引起我们的注意!

4、具有不可确定性,但是也远比标准 C 中的 mallo()和 free()效率高!

heap_2 基本上可以适用于大多数的需要动态分配内存的工程中,而 heap_4 更是具有将内存碎片合并成一个大的空闲内存块(就是内存碎片回收)的功能。

2. 内存块详解

同 heap_1 一样,heap_2 整个内存堆为 ucHeap[],大小为configTOTAL_HEAP_SIZE。可以通过函数 xPortGetFreeHeapSize()来获取剩余的内存大小。

为了实现内存释放,heap_2 引入了内存块的概念,每分出去的一段内存就是一个内存块,剩下的空闲内存也是一个内存块,内存块大小不定。为了管理内存块又引入了一个链表结构,链表结构如下:

typedef struct A_BLOCK_LINK
{
	struct A_BLOCK_LINK *pxNextFreeBlock; //指向链表中下一个空闲内存块
	size_t xBlockSize; //当前空闲内存块大小
} BlockLink_t;

每个内存块前面都会有一个 BlockLink_t 类型的变量来描述此内存块,比如我们现在申请
了一个 16 个字节的内存块,那么此内存块结构就如下图所示:
在这里插入图片描述
图中内存块的总大小是 24 字节,虽然我们只申请了 16 个字节,但是还需要另外8 字节来保存 BlockLink_t 类型的结构体变量,xBlockSize 记录的是整个内存块的大小。

为了方便管理,可用的内存块会被全部组织在一个链表内,局部静态变量 xStart, xEnd 用来记录这个链表的头和尾,这两个变量定义如下:

static BlockLink_t xStart, xEnd;

3. 内存堆初始化函数详解

内存堆初始化函数为 prvHeapInit(),函数源码如下:

static void prvHeapInit( void )
{
	BlockLink_t *pxFirstFreeBlock;
	uint8_t *pucAlignedHeap;
	//确保内存堆的开始地址是字节对齐的
	pucAlignedHeap = ( uint8_t * ) ( ( ( portPOINTER_SIZE_TYPE )\ (1)
	 &ucHeap[ portBYTE_ALIGNMENT ] ) & \
	( ~( ( portPOINTER_SIZE_TYPE )\
	portBYTE_ALIGNMENT_MASK ) ) );
	//xStart 指向空闲内存块链表首。
	xStart.pxNextFreeBlock = ( void * ) pucAlignedHeap; (2)
	xStart.xBlockSize = ( size_t ) 0;
	//xEnd 指向空闲内存块链表尾。
	xEnd.xBlockSize = configADJUSTED_HEAP_SIZE; (3)
	xEnd.pxNextFreeBlock = NULL;
	//刚开始只有一个空闲内存块,空闲内存块的总大小就是可用的内存堆大小。
	pxFirstFreeBlock = ( void * ) pucAlignedHeap; (4)
	pxFirstFreeBlock->xBlockSize = configADJUSTED_HEAP_SIZE;
	pxFirstFreeBlock->pxNextFreeBlock = &xEnd;
}

(1)、同 heap_1 一样,确保内存堆的可用起始地址为 8 字节对齐。
(2)、初始化 xStart 变量。
(3)、初始化 xEnd 变量。
(4)、每个内存块前面都会保存一个 BlockLink_t 类型的结构体变量,这个结构体变量用来描述此内存块的大小和下一个空闲内存块的地址。

初始化以后的内存堆如下图所示:
在这里插入图片描述

4. 内存块插入函数详解

heap_2 允 许 内 存 释 放 , 释 放 的 内 存 肯 定 是 要 添 加 到 空 闲 内 存 链 表 中 的 , 宏prvInsertBlockIntoFreeList()用来完成内存块的插入操作,宏定义如下:

#define prvInsertBlockIntoFreeList( pxBlockToInsert )
{
	BlockLink_t *pxIterator;
	size_t xBlockSize;
	xBlockSize = pxBlockToInsert->xBlockSize;
	//遍历链表,查找插入点
	for( pxIterator = &xStart; pxIterator->pxNextFreeBlock->xBlockSize < xBlockSize; (1)
	pxIterator = pxIterator->pxNextFreeBlock )
	{
		//不做任何事情
	}
	//将内存块插入到插入点
	pxBlockToInsert->pxNextFreeBlock = pxIterator->pxNextFreeBlock; (2)
	pxIterator->pxNextFreeBlock = pxBlockToInsert;
}

(1)、寻找内存块的插入点,内存块是按照内存大小从小到大连接起来的,因为只是用来寻找插入点的,所以 for 循环体内没有任何代码。

(2)、找到内存插入点以后就将内存块插入到链表中。
假如我们现在需要将大小为 80 字节的内存块插入到链表中,过程如下图所示:
在这里插入图片描述

5. 内存申请函数详解

heap_2 的内存申请函数源码如下:

void *pvPortMalloc( size_t xWantedSize )
{
	BlockLink_t *pxBlock, *pxPreviousBlock, *pxNewBlockLink;
	static BaseType_t xHeapHasBeenInitialised = pdFALSE;
	void *pvReturn = NULL;
	vTaskSuspendAll();
	{
		//如果是第一次申请内存的话需要初始化内存堆
		if( xHeapHasBeenInitialised == pdFALSE ) (1)
		{
			prvHeapInit();
			xHeapHasBeenInitialised = pdTRUE;
		}
		//内存大小字节对齐,实际申请的内存大小还要加上结构体
		//BlockLink_t 的大小
		if( xWantedSize > 0 ) (2)
		{
			xWantedSize += heapSTRUCT_SIZE; (3)
			//xWantedSize 做字节对齐处理
			if( ( xWantedSize & portBYTE_ALIGNMENT_MASK ) != 0 )
			{
				xWantedSize += ( portBYTE_ALIGNMENT - ( xWantedSize &
				 portBYTE_ALIGNMENT_MASK ) );
			}
		}
		//所申请的内存大小合理,进行内存分配。
		if( ( xWantedSize > 0 ) && ( xWantedSize < configADJUSTED_HEAP_SIZE ) )
		{
			//从 xStart(最小内存块)开始,查找大小满足所需要内存的内存块。
			pxPreviousBlock = &xStart;
			pxBlock = xStart.pxNextFreeBlock;
			while( ( pxBlock->xBlockSize < xWantedSize ) &&\ (4)
			 ( pxBlock->pxNextFreeBlock != NULL ) )
			{
				pxPreviousBlock = pxBlock;
				pxBlock = pxBlock->pxNextFreeBlock;
			}
			if( pxBlock != &xEnd ) (5)
			{
				//返回申请到的内存首地址
				pvReturn = ( void * ) ( ( ( uint8_t * ) pxPreviousBlock->pxNextFreeBlock ) +\(6)
				heapSTRUCT_SIZE );
				pxPreviousBlock->pxNextFreeBlock = pxBlock->pxNextFreeBlock; (7)
				if( ( pxBlock->xBlockSize - xWantedSize ) >\ (8)
					heapMINIMUM_BLOCK_SIZE )
				{
					pxNewBlockLink = ( void * ) ( ( ( uint8_t * ) pxBlock ) + xWantedSize );
					pxNewBlockLink->xBlockSize = pxBlock->xBlockSize - xWantedSize;
					pxBlock->xBlockSize = xWantedSize;
					prvInsertBlockIntoFreeList( ( pxNewBlockLink ) ); (9)
				}
				xFreeBytesRemaining -= pxBlock->xBlockSize; (10)
			}
		}
		traceMALLOC( pvReturn, xWantedSize );
	}
	( void ) xTaskResumeAll();
	#if( configUSE_MALLOC_FAILED_HOOK == 1 ) (11)
	{
		if( pvReturn == NULL )
		{
			extern void vApplicationMallocFailedHook( void );
			vApplicationMallocFailedHook();
		}
	}
	#endif
	return pvReturn;
}

(1)、如果是第一次调用函数 pvPortMalloc()申请内存的话就需要先初始化一次内存堆。

(2)、所申请的内存大小进行字节对齐。

(3)、实际申请的内存大小需要再加上结构体 BlockLink_t 的大小,因为每个内存块都会保存一个 BlockLink_t 类型变量,BlockLink_t 结构体的大小为 heapSTRUCT_SIZE。

(4)、从空闲内存链表头 xStart 开始,查找满足所需内存大小的内存块,pxPreviousBlock 所指向的下一个内存块就是找到的可用内存块。

(5)、找到的可用内存块不能是链表尾 xEnd!

(6)、找到内存块以后就将可用内存首地址保存在 pvReturn 中,函数返回的时候返回此值,这个内存首地址要跳过结构体 BlockLink_t,如下图所示:
在这里插入图片描述
(7)、内存块已经被申请了,所以需要将这个内存块从空闲内存块链表中移除。

(8)、存在这样一种情况(不考虑结构体 BlockLink_t 的大小),我需要申请 100 个字节的内存,但是经过上面几步我得到了一个 1K 字节的内存块,实际使用中我只需要 100 个字节,剩下的 900 个字节就浪费掉了。这个明显是不合理的,所以需要判断,如果申请到的实际内存减去所需的内存大小(xBlockSize-xWantedSize)大于某个阈值的时候就把多余出来的内存重新组合成一个新的可用空闲内存块。这个阈值由宏heapMINIMUM_BLOCK_SIZE 来设置,这个阈值要大于 heapSTRUCT_SIZE。

(9)、将新的空闲内存块插入到空闲内存块链表中。

(10)、更新全局变量 xFreeBytesRemaining,此变量用来保存内存堆剩余内存大小。

(11)、如果使能了钩子函数的话就调用钩子函数 vApplicationMallocFailedHook()。

6. 内存释放函数详解

内存释放函数 vPortFree()的源码如下:

void vPortFree( void *pv )
{
	uint8_t *puc = ( uint8_t * ) pv;
	BlockLink_t *pxLink;
	if( pv != NULL )
	{
		puc -= heapSTRUCT_SIZE; (1)
		pxLink = ( void * ) puc; (2)
		vTaskSuspendAll();
		{
			//将内存块添加到空闲内存块链表中
			prvInsertBlockIntoFreeList( ( ( BlockLink_t * ) pxLink ) ); (3)
			xFreeBytesRemaining += pxLink->xBlockSize; (4)
			traceFREE( pv, pxLink->xBlockSize );
		}
		( void ) xTaskResumeAll();
	}
}

(1)、puc 为要释放的内存首地址,这个地址就是图 20.3.2.4 中 pvReturn 所指向的地址。所以必须减去 heapSTRUCT_SIZE 才是要释放的内存段所在内存块的首地址。

(2)、防止编译器报错。

(3)、将内存块添加到空闲内存块列表中。

(4)、更新变量 xFreeBytesRemaining。

内存释放函数 vPortFree()还是很简单的,主要目的就是将需要释放的内存所在的内存块添加到空闲内存块链表中。

五、未完期待。。。

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

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

相关文章

操作系统考试复习——第四章 4.3连续分配存储管理方式

在这里的开头需要讲述一下碎片&#xff0c;碎片分为内碎片和外碎片两种。 内碎片&#xff1a;分区之内未被利用的空间外碎片&#xff1a;分区之间难以利用的空闲分区&#xff08;通常是小空闲分区&#xff09;。 连续分配存储管理方式: 为了能将用户程序装入内存&#xff0c…

力扣刷题Day12_2

144.二叉树的前序遍历 测试代码main() class TreeNode:def __init__(self, valNone, leftNone, rightNone):self.val valself.left leftself.right rightfrom typing import Listclass Solution:def preorderTraversal(self, root: TreeNode) -> List[int]:s Solution…

C++易错编程练习题(1)

0 编程练习 基础不牢靠&#xff0c;回头来补课。小白这个系列主要是为了重新打基础&#xff0c;为一些常见的易错编程练习题提供记录。其间若有错误&#xff0c;欢迎指出&#xff0c;轻喷勿骂。毕竟小白确实是基础不牢靠。 1 题目 自定义函数之整数处理。 题目描述 输入10个…

多视图局部共现和全局一致性学习提高乳腺图像分类的综合性

文章目录 Multi-view Local Co-occurrence and Global Consistency Learning Improve Mammogram Classification Generalisation摘要本文方法global consistency modulelocal co-occurrence module (LCM) 实验结果 Multi-view Local Co-occurrence and Global Consistency Lear…

okio篇3-超时机制

关于System.nanoTime System.currentTimeMills与System.nanoTime实际都是时间间隔&#xff0c;只不过两个时间的起始时间衡量不一致。 我们比较常用的&#xff0c;实际是System.currentTimeMills()&#xff0c;这个时间是以1970-01-01起始&#xff0c;到系统显示时间的间隔。…

聚浪成潮,网易数帆CodeWave智能开发平台开启低代码新时代

‍数据智能产业创新服务媒体 ——聚焦数智 改变商业 随着全球范围内新一代人工智能技术发展突飞猛进&#xff0c;社会各领域从数字化、网络化向智能化转变&#xff0c;如何进一步释放数据生产力、加速智能化转型已成为企业发展的必修课。 2023年4月25日&#xff0c;“网易数帆…

K8S管理系统项目实战[API开发]-2

后端: gogin 后端代码地址GitHub - yunixiangfeng/k8s-platform: K8s管理系统后端: gogin 5、存储与配置 5.1 ConfigMap 5.2 Secret 5.3 PersistentVolumeClaims 6、工作流 6.1 流程设计 6.2 数据库操作&#xff08;GORM&#xff09; &#xff08;1&#xff09;初始化…

Mysql Sharding-JDBC读写分离 原理

0 课程视频 深入Sharding-JDBC分库分表从入门到精通【黑马程序员】_哔哩哔哩_bilibili 1 基本概念 1.1应用逻辑 1.1.1 msyql 多库 多表 多服务器 1.1.2 通过Sharding-JDBC jar包->增强JDBC 访问多数据源 -> 自动处理成一个数据源 1.1.3 使用数据的人 -> 使用Sh…

Java面试题总结 | Java面试题总结12- 测试模块

测试 测试需要具备的素质 基础的理论知识、编程语言的功底、自动化测试工具、计算机基础知识 业务分析能力&#xff1a;分析业务的流程&#xff0c;分析被测业务数据、分析被测系统的框架、分析业务模块、分析测试所需资源、分析测试完成目标 缺陷洞察能力&#xff1a;一般…

【ChatGPT】吴恩达教程笔记(预备篇)

本文概要 众所周知&#xff0c;吴恩达老师与OpenAI联合推出了一门面向开发者的Prompt课程&#xff08;https://www.deeplearning.ai/short-courses/chatgpt-prompt-engineering-for-developers &#xff09;&#xff0c;时隔几天&#xff0c;吴恩达老师发推说已经有超过20万人…

Netty基础

2.1Netty是什么 是一个基于异步的&#xff08;多线程处理结果和接收&#xff09;、事件驱动的网络应用框架&#xff0c;用于基于快速开发可维护、高性能的网络服务器和客户端 异步是指调用时的异步&#xff0c;他的IO还是多路复用的IO 许多中间件都依赖与Netty zookperhado…

GUITAR PRO8吉他软件好不好用?值不值得下载

所谓“工欲善其事&#xff0c;必先利其器”&#xff0c;想成为一名专业甚至著名的音乐人&#xff0c;用到的工具软件非常多&#xff0c;在众多款软件工具中&#xff0c;Guitar Pro 8能满足乐谱创作者、学习者的所有需要。很多人在听到Guitar Pro这个名词时&#xff0c;本能反应…

spark的RDD算子计算

一、环境配置 import osfrom pyspark import SparkConf, SparkContextif __name__ __main__:os.environ[SPARK_HOME] /export/server/sparkos.environ["PYSPARK_PYTHON"] "/root/anaconda3/envs/pyspark_env/bin/python"os.environ["PYSPARK_DRIV…

JavaWeb07(MVC应用01[家居商城]连接数据库)

目录 一.什么是MVC设计模式&#xff1f; 1.2 MVC设计模式有什么优点&#xff1f; 二.MVC运用&#xff08;家居商城&#xff09; 2.1 实现登录 2.2 绑定轮播【随机三个商品】 2.2.1 效果预览 index.jsp 2.3 绑定最新上架&热门家居 2.3.1 效果预览 2.3.2 代码实现 数据…

linux进程基本知识

1.什么是程序&#xff0c;什么是进程&#xff1f; 程序是静态的概念&#xff0c;例如 gcc xx.c -o pro 磁盘中生成pro文件&#xff0c;叫做程序 进程是程序的一次运行活动&#xff0c;意思是程序跑起来了&#xff0c;系统中就多了一个进程 2.如何查看系统中有哪些进程&…

EMC VNX登录Unisphere错误 certificate has invalid date问题处理

经常有用户反应说&#xff0c;突然用浏览器登录EMC VNX或者Clarrion CX系统的时候出现“certificate has invalid date”的故障&#xff0c;然后无法正常登录图形界面。具体报错如下图所示&#xff1a; 导致这个问题的原因在于VNX系统中的certification认证过期&#xff0c;既然…

SpringBoot整合Echarts实现用户人数和性别展示

一、背景 在Web应用开发中&#xff0c;经常需要使用图表来展示数据&#xff0c;而Echarts是一个非常优秀的图表库。SpringBoot是一个非常流行的Java Web框架&#xff0c;它可以快速搭建Web应用。本文将介绍如何使用SpringBoot集成Echarts&#xff0c;实现展示用户人数和性别的…

百度百科如何创建?创建百度百科的秘诀你值得掌握(经验分享)

百度百科是中国最大的百科全书式的中文网站之一&#xff0c;是广大用户在互联网上获取知识的重要途径之一。任何人都可以在百度百科创建新的词条&#xff0c;为网站的发展作出贡献。 小媒同学将从如何创建百度百科词条和注意事项两个方面来详细介绍百度百科词条的创建流程和相关…

css3 flex弹性布局详解

css3 flex弹性布局详解 一、flexbox弹性盒子 2009年&#xff0c;W3C 提出了一种新的方案----Flex 布局&#xff0c;可以简便、完整、响应式地实现各种页面布局。目前&#xff0c;它已经得到了所有浏览器的支持&#xff0c;这意味着&#xff0c;现在就能很安全地使用这项功能。…

在选择数据库时需要考虑的因素

在文章的第一部分中&#xff0c;我们奠定了理解各种数据库类型及其用例的基础。随着我们继续探索数据库选择的艺术&#xff0c;我们现在将更深入地探讨影响这个决策过程的关键因素。通过更详细地检查每个因素&#xff0c;我们可以更好地装备自己做出符合项目要求的知情选择&…