FreeRTOS学习笔记(十一)内存管理

news2024/9/20 12:16:35

文章目录

  • 前言
  • 一、内存管理
    • 1.1 内存管理的引入
    • 1.2 内存碎片
  • 二、内存分配的方法
    • 2.1 heap_1
      • 2.1.1 实现原理
      • 2.1.2 源码解析
    • 2.2 heap_2 内存分配方法
      • 2.2.1 实现原理
      • 2.2.2 源码解析
    • 2.3 heap_3 内存分配方法
    • 2.4 heap_4 内存分配方法
      • 2.4.1 实现原理
      • 2.4.2 源码解析
    • 2.5 heap_5 内存分配方法


前言

  本章是Free RTOS系列的终章,我们来讲述贯穿全系列的一个核心元素——内存管理。


一、内存管理

1.1 内存管理的引入

  内存管理是一个系统基本组成部分,FreeRTOS 中大量使用到了内存管理,比如创建任务、信号量、队列等会自动从堆中申请内存。用户应用层代码也可以 FreeRTOS 提供的内存管理函数来申请和释放内存。FreeRTOS 创建任务、队列、信号量等的时候有两种方法,一种是动态的申请所需的 RAM;一种是由用户自行定义所需的 RAM,这种方法也叫静态方法,使用静态方法的函数一般以“Static”结尾,比如任务创建函数 xTaskCreateStatic(),使用此函数创建任务的时候需要由用户定义任务堆栈,本章我们不讨论这种静态方法。
  使用动态内存管理的时候 FreeRTOS 内核在创建任务、队列、信号量的时候会动态的申请RAM。标准 C 库中的 malloc()和 free()也可以实现动态内存管理,但是出于种种原因限制了其使用,因此一个内存分配算法可以作为系统的可选选项。FreeRTOS 将内存分配作为移植层的一部分,这样 FreeRTOS 使用者就可以使用自己的合适的内存分配方法。
  动态内存分配需要一个内存堆,FreeRTOS 中的内存堆为ucHeap[] ,大小为configTOTAL_HEAP_SIZE,这个前面讲 FreeRTOS 配置的时候就讲过了。不管是哪种内存分配方法,它们的内存堆都为 ucHeap[],而且大小都是 configTOTAL_HEAP_SIZE。

1.2 内存碎片

  在学习 FreeRTOS 的内存分配方法之前我们先来看一下什么叫做内存碎片,看名字就知道是小块的、碎片化的内存。内存碎片是伴随着内存申请和释放而来的,如下图所示:
在这里插入图片描述

  可以看到经过很多次的申请和释放以后,内存块被不断的分割、最终导致大量很小的内存块!也就是图中 80B 和 50B 这两个内存块之间的小内存块,这些内存块由于太小导致大多数应用无法使用,这些没法使用的内存块就沦为了内存碎片!
  内存碎片是内存管理算法重点解决的一个问题,否则的话会导致实际可用的内存越来越少,最终应用程序因为分配不到合适的内存而奔溃!FreeRTOS 的 heap_4.c 就给我们提供了一个解决内存碎片的方法,那就是将内存碎片进行合并组成一个新的可用的大内存块。

二、内存分配的方法

  在Free RTOS的移植一章中,我们提到了其提供了 5 种内存分配方法这 5 种方法是 5 个文件,分别为:heap_1.c、heap_2.c、heap_3.c、heap_4.c 和heap_5.c。这部分我们FreeRTOS 使用者可以其中的某一个方法,或者自定义一个合适的分配方法。
在这里插入图片描述

2.1 heap_1

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

2.1.1 实现原理

  1. heap_1.c 中使用了一个简单的静态数组作为堆空间,并按需从该数组中分配内存
  2. 该实现非常简单,只允许内存分配,不支持内存释放,内存的释放只能在任务结束或系统重启时实现。
  3. 主要适用于小型嵌入式系统中,内存需求相对简单、可预测的场景。

2.1.2 源码解析

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

// 该函数(简化)用于动态分配内存,返回指向分配内存的指针
void *pvPortMalloc( size_t xWantedSize )
{
    void *pvReturn = NULL;
    
    // 确保 xWantedSize 是 8 字节对齐(xWantedSize是用户请求的内存大小,按照默认要求堆分配会将该大小调整为 8 字节对齐)
    if( ( xWantedSize & portBYTE_ALIGNMENT_MASK ) != 0 ) 
    {
        xWantedSize += ( portBYTE_ALIGNMENT - ( xWantedSize & portBYTE_ALIGNMENT_MASK ) );
    }

    // 检查是否有足够的空间,如果有返回堆中当前位置 xNextFreeByte 的地址,并将 xNextFreeByte 向前移动 xWantedSize 字节;不足返回NULL
    if( ( ( xNextFreeByte + xWantedSize ) < configTOTAL_HEAP_SIZE ) &&
        ( ( xNextFreeByte + xWantedSize ) > xNextFreeByte ) )
    {
    	// pucAlignedHeap表示起始地址(ucHeap的起始地址不一定是8字节对齐的,需要我们利用这个参数补齐)
    	// 如果内存够分配并且不会产生越界,那么就将申请到的内存首地址赋给 pvReturn
    	// 值得注意的是如果我们要申请 30 个字节的内存,字节对齐以后实际需要申请 32 字节
        pvReturn =  pucAlignedHeap + xNextFreeByte; 
        xNextFreeByte += xWantedSize;
    }
    
    return pvReturn;
}

  heap_1 的内存释放函数为 pvFree(),可以看出 vPortFree()并没有具体释放内存的过程,这说明使用一旦申请内存成功就不允许释放!

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

  这说明了heap_1适用于在系统一开始就创建好任务、信号量或队列等,在程序运行的整个过程这些任务和内核对象都不会删除。

2.2 heap_2 内存分配方法

  heap_2提供了一个更好的分配算法,不像heap_1那样,heap_2提供了内存释放函数。heap_2不会把释放的内存块合并成一个大块,这样有一个缺点,随着你不断的申请内存,内存堆就被分为很多个大小不一的内存(块),也就是会导致内存碎片
  为了实现内存释放,heap_2 引入了内存块的概念,每分出去的一段内存就是一个内存块,剩下的空闲内存也是一个内存块,内存块大小不定,每个内存块前面都会有一个 BlockLink_t 类型的变量来描述此内存块。为了管理内存块又引入了一个链表结构,链表结构如下:

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

  值得注意的是,例如我们只申请了 16 个字节内存,但是还需要另外为BlockLink_t 类型的结构体变量申请8字节,xBlockSize 记录的是整个内存块的大小(24个字节)。

2.2.1 实现原理

  1. heap_2.c 使用链表管理空闲内存块,每个内存块的前面都有一个头部,存储该块的大小和状态(已分配或空闲)。
  2. 当请求内存时,遍历空闲内存链表,找到第一个足够大的块进行分配。
  3. 当释放内存时,将空闲块合并回链表中,以减少碎片化。

2.2.2 源码解析

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

void *pvPortMalloc( size_t xWantedSize )
{
    BlockLink_t *pxBlock, *pxPreviousBlock, *pxNewBlockLink;
    void *pvReturn = NULL;
    
	// 进入临界区,暂停任务调度
    vTaskSuspendAll();    

    // 确保 xWantedSize 是 8 字节对齐
    if( xWantedSize & portBYTE_ALIGNMENT_MASK )
    {
        xWantedSize += ( portBYTE_ALIGNMENT - ( xWantedSize & portBYTE_ALIGNMENT_MASK ) );
    }

    // 增加头部大小:为了管理内存块,增加A_BLOCK_LINK的大小
    xWantedSize += xHeapStructSize;

    // 遍历空闲链表,找到合适的块
    pxPreviousBlock = &xStart;
    pxBlock = xStart.pxNextFreeBlock;
    while( ( pxBlock->xBlockSize < xWantedSize ) && ( pxBlock->pxNextFreeBlock != NULL ) )
    {
        pxPreviousBlock = pxBlock;
        pxBlock = pxBlock->pxNextFreeBlock;
    }
	// 找到的可用内存块不能是链表尾 xEnd
    if( pxBlock != pxEnd )
    {	
    	// 找到内存块后就将可用内存首地址保存在 pvReturn 中,函数返回的时候返回此值
		// 这个内存首地址要跳过结构体 BlockLink_t
        pvReturn = ( void * ) ( ( ( uint8_t * ) pxPreviousBlock->pxNextFreeBlock ) + xHeapStructSize );
        pxPreviousBlock->pxNextFreeBlock = pxBlock->pxNextFreeBlock;
    }

    xTaskResumeAll();     // 退出临界区,恢复任务调度
    return pvReturn;
}

  内存释放函数 vPortFree()还是很简单的,主要目的就是将需要释放的内存所在的内存块,其源码如下:

void vPortFree( void *pv )
{
	// puc 为要释放的内存首地址
	uint8_t *puc = ( uint8_t * ) pv;
	BlockLink_t *pxLink;
	if( pv != NULL )
	{
		// 指向申请函数中pvReturn 所指向的地址
		puc -= heapSTRUCT_SIZE; 
		pxLink = ( void * ) puc; 
		vTaskSuspendAll();
		{
		// 将内存块添加到空闲内存块链表中
		prvInsertBlockIntoFreeList( ( ( BlockLink_t * ) pxLink ) ); 
		// 更新变量 xFreeBytesRemaining
		xFreeBytesRemaining += pxLink->xBlockSize; 
		traceFREE( pv, pxLink->xBlockSize );
		}
		( void ) xTaskResumeAll();
	}
}

2.3 heap_3 内存分配方法

  这个分配方法是对标准 C 中的函数 malloc()和 free()的简单封装,FreeRTOS 对这两个函数做了线程保护,这里就简单解释一下,不再赘述了。heap_3 它通过封装这些标准函数,保证 FreeRTOS 兼容多任务调度环境中的内存分配和释放需求。vTaskSuspendAll() 和 xTaskResumeAll() 确保了在分配和释放内存时,不会出现任务切换导致的竞态条件。
  pvPortMalloc( )调用标准的 malloc() 函数分配内存,并通过 vTaskSuspendAll() 暂停任务调度。

void *pvPortMalloc( size_t xWantedSize )
{
    void *pvReturn;

    vTaskSuspendAll();
    {
        pvReturn = malloc( xWantedSize );
    }
    xTaskResumeAll();

    return pvReturn;
}

  vPortFree()调用标准的 free() 函数释放内存,并通过 vTaskSuspendAll() 暂停任务调度。

void vPortFree( void *pv )
{
    if( pv != NULL )
    {
        vTaskSuspendAll();
        {
            free( pv );
        }
        xTaskResumeAll();
    }
}

2.4 heap_4 内存分配方法

  这种方法是我们学习的重中之重,heap_4 提供了一个最优的匹配算法,它会将内存碎片合并成一个大的可用内存块。这意味着它可以用在那些需要重复创建和删除任务、队列、信号量和互斥信号量等的应用中。虽然具有不确定性,但是远比 C 标准库中的 malloc()和 free()效率高。

2.4.1 实现原理

  1. heap_4.c 实现了类似 heap_2.c 的内存分配策略,即利用A_BLOCK_LINK 结构体来管理内存块,但在其基础上增加了空闲内存块的合并功能。
  2. 每当内存块被释放时,会尝试将它与相邻的空闲块合并,以减少内存碎片化的发生。
  3. 采用了“最佳适配”算法来分配内存,优先使用最适合的空闲块,进一步降低碎片化。

2.4.2 源码解析

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

void *pvPortMalloc( size_t xWantedSize )
{
    BlockLink_t *pxBlock, *pxPreviousBlock, *pxNewBlockLink;
    void *pvReturn = NULL;
    // 临界区开始
    vTaskSuspendAll(); 

    // 最小块大小检查
    if( xWantedSize > 0 )
    {
        xWantedSize += heapSTRUCT_SIZE;

        // 8 字节对齐
        if( ( xWantedSize & portBYTE_ALIGNMENT_MASK ) != 0x00 )
        {
            xWantedSize += ( portBYTE_ALIGNMENT - ( xWantedSize & portBYTE_ALIGNMENT_MASK ) );
        }
    }

    // 查找合适的块
    pxPreviousBlock = &xStart;
    pxBlock = xStart.pxNextFreeBlock;
    
    // 从空闲内存链表头 xStart 开始,查找满足所需内存大小的内存块
	// pxPreviousBlock 的下一个内存块就是找到的可用内存块
    while( ( pxBlock->xBlockSize < xWantedSize ) && ( pxBlock->pxNextFreeBlock != NULL ) )
    {
        pxPreviousBlock = pxBlock;
        pxBlock = pxBlock->pxNextFreeBlock;
    }

    // 分配内存
    if( pxBlock != pxEnd )
    {
        pvReturn = ( void * ) ( ( ( uint8_t * ) pxPreviousBlock->pxNextFreeBlock ) + heapSTRUCT_SIZE );
        // 将申请到的内存块从空闲内存链表中移除
        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;
            pxNewBlockLink->pxNextFreeBlock = pxPreviousBlock->pxNextFreeBlock;
            pxPreviousBlock->pxNextFreeBlock = pxNewBlockLink;
        }
		// 减少剩余的可用内存,并更新系统曾经最少剩余的内存量
        xFreeBytesRemaining -= pxBlock->xBlockSize;
        if( xFreeBytesRemaining < xMinimumEverFreeBytesRemaining )
        {
            xMinimumEverFreeBytesRemaining = xFreeBytesRemaining;
        }
    }

    xTaskResumeAll(); // 临界区结束
    return pvReturn;
}

  内存释放函数源码如下:

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

    if( pv != NULL )
    {
    	// 获取内存块的 BlockLink_t 类型结构体
        puc -= heapSTRUCT_SIZE;
        pxLink = ( void * ) puc;

        vTaskSuspendAll(); 
		// 更新增加的内存
        xFreeBytesRemaining += pxLink->xBlockSize;
        if( ( ( uint8_t * ) pxLink + pxLink->xBlockSize ) == ( uint8_t * ) pxLink->pxNextFreeBlock )
        {
            pxLink->xBlockSize += pxLink->pxNextFreeBlock->xBlockSize;
            pxLink->pxNextFreeBlock = pxLink->pxNextFreeBlock->pxNextFreeBlock;
        }

        // 合并前面的块
        pxLink->pxNextFreeBlock = xStart.pxNextFreeBlock;
        xStart.pxNextFreeBlock = pxLink;

        xTaskResumeAll(); 
    }
}

2.5 heap_5 内存分配方法

  heap_5 使用了和 heap_4 相同的合并算法,内存管理实现起来基本相同,但是 heap_5 允许内存堆跨越多个不连续的内存段。比如 STM32 的内部 RAM 可以作为内存堆,但是 STM32 内部 RAM 比较小,遇到那些需要大容量 RAM 的应用就不行了,如音视频处理。不过 STM32 可以外接 SRAM 甚至大容量的 SDRAM,如果使用 heap_4 的话你就只能在内部 RAM 和外部SRAM 或 SDRAM 之间二选一了,使用 heap_5 的话就不存在这个问题,两个都可以一起作为内存堆来用。
  如果使用 heap_5 的话,在调用 API 函数之前需要先调用函数vPortDefineHeapRegions ()来对内存堆做初始化处理,在其未执行完之前禁止调用任何可能会调用pvPortMalloc()的 API 函数。函数 vPortDefineHeapRegions()
只有一个参数,参数是一个 HeapRegion_t 类型的数组,HeapRegion 为一个结构体,此结构体在portable.h 中有定义,定义如下:

typedef struct HeapRegion
{
	uint8_t *pucStartAddress; //内存块的起始地址
	size_t xSizeInBytes; //内存段大小
} HeapRegion_t;

  使用 heap_5 的时候在一开始就应该先调用函数 vPortDefineHeapRegions()完成内存堆的初始化!然后才能创建任务、信号量这些东西。heap_5 的内存申请和释放函数和 heap_4 基本一样,这里就不详细讲解了,大家可以对照着前面 heap_4 的相关内容来自行分析。

免责声明:本文参考了网上公开资料,仅用于学习交流,若有错误或侵权请联系笔者。

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

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

相关文章

【论文解读系列】DPD-BiReconstructor的神经网络架构

原标题&#xff1a;Semisupervised Neural Proto-Language Reconstruction 论文地址&#xff1a;https://arxiv.org/pdf/2406.05930 现有实现祖先语言&#xff08;原语言&#xff09;比较重建的工作通常需要完全监督。然而&#xff0c;如果历史重建模型只能用少量标记数据进行训…

二、栈和队列-算法总结

文章目录 二、栈和队列2.1 基本应用2.1.1 逆波兰表达式求值2.1.2 有效的括号 2.2 单调栈2.2.1 柱状图中最大的矩形 二、栈和队列 2.1 基本应用 2.1.1 逆波兰表达式求值 150. 逆波兰表达式求值 class Solution {/**思路分析&#xff1a;遇到数则压栈&#xff0c;遇到运算符…

每日一练12:杨辉三角(含链接)

1.链接 杨辉三角_牛客题霸_牛客网 2.题目 3.代码 #include <iostream> #include<vector> using namespace std; vector<int> arr(35); int main() {int n;cin>>n;for(int i1;i<n;i){if(i1) {printf("%5d",1);arr[1]1;cout<<end…

电商API接口安全:构建稳固的数字防线

电子商务的蓬勃发展带来了前所未有的便利&#xff0c;同时也带来了新的安全挑战。API接口作为电商系统的核心组件&#xff0c;其安全性直接关系到企业的数据安全和业务连续性。因此&#xff0c;评估和加固电商API接口的安全性变得尤为重要。 电商API接口安全的重要性 电商API接…

【Redis】Redis 典型应用 - 分布式锁原理与实现

目录 Redis 典型应⽤ - 分布式锁什么是分布式锁分布式锁的基础实现引⼊过期时间引⼊校验 id引⼊ lua引⼊ watch dog (看⻔狗)引⼊ Redlock 算法其他功能 Redis 典型应⽤ - 分布式锁 什么是分布式锁 在⼀个分布式的系统中&#xff0c; 也会涉及到多个节点访问同⼀个公共资源的…

YOLOv5改进:CA注意力机制【注意力系列篇】(附详细的修改步骤,以及代码)

如果实验环境尚未搭建成功&#xff0c;可以参考这篇文章 ->【YOLOv5超详细环境搭建以及模型训练&#xff08;GPU版本&#xff09;】 文章链接为&#xff1a;http://t.csdnimg.cn/Ke0bb ---------------------------------------------------------------------------​ 1…

09-排序1 排序(C)

这一节&#xff0c;测试各类排序算法的运行速度&#xff08;没有基数排序&#xff08;桶&#xff09; 其实在实际学习中&#xff0c;还是有意义的 给定 n 个&#xff08;长整型范围内的&#xff09;整数&#xff0c;要求输出从小到大排序后的结果。 本题旨在测试各种不同的排序…

Unity Addressables 使用说明(三)构建内容(Build Content)

Build Content 【概述】Build Content 内容构建会处理 Addressables 组&#xff0c;生成内容目录&#xff08;content catalog&#xff09;、运行时设置以及包含你的资源的 AssetBundles。Addressables 使用这些文件在运行时加载内容。 你可以配置 Addressables 系统将 Addr…

重磅!OpenAI正式发布博士水平的推理模型o1!附详细说明

大家好&#xff0c;我是木易&#xff0c;一个持续关注AI领域的互联网技术产品经理&#xff0c;国内Top2本科&#xff0c;美国Top10 CS研究生&#xff0c;MBA。我坚信AI是普通人变强的“外挂”&#xff0c;所以创建了“AI信息Gap”这个公众号&#xff0c;专注于分享AI全维度知识…

【android10】【binder】【2.servicemanager启动——全源码分析】

系列文章目录 可跳转到下面链接查看下表所有内容https://blog.csdn.net/handsomethefirst/article/details/138226266?spm1001.2014.3001.5501文章浏览阅读2次。系列文章大全https://blog.csdn.net/handsomethefirst/article/details/138226266?spm1001.2014.3001.5501 目录 …

登山第九梯:稀疏点云实例分割——又快又准

文章&#xff1a;Fast Range Image-Based Segmentation of Sparse 3D Laser Scans for Online Operation 代码&#xff1a;https://github.com/PRBonn/depth_clustering 1&#xff09;摘要 从 3D 距离数据中分割对象是移动机器人领域的一个重要主题。在动态环境中导航的机器人需…

C51单片机-单按键输入识别,键盘消抖

【实验目的】 独立按键的识别方法、键盘消抖等。 【实验现象】 每按一次独立键盘的S2键&#xff0c;与P1口相连的八个发光二极管中点亮的一个往下移动一位。 【实验说明】 关于按键去抖动的解释&#xff0c;我们在手动按键的时候&#xff0c;由于机械抖动或是其它一些非人为的因…

NR PDSCH/PUSCH支持的maxMIMO layers

这里不考虑UE支持的具体MIMO能力&#xff0c;仅仅讨论协议上定的maxMIMO layers。 PDSCH 根据上面38.331中的结构&#xff0c;PDSCH max MIMO layers 为8 layers&#xff0c;进行8 layers传输时 要enable two codewords&#xff0c;因为 one codeword只能支持4 layers传输&…

【信创】Linux系统如何配置USB存储禁用及例外 _ 统信 _ 麒麟 _ 方德

原文链接&#xff1a;【信创】Linux系统如何配置USB存储禁用及例外 | 统信 | 麒麟 | 方德 Hello&#xff0c;大家好啊&#xff01;今天给大家带来一篇关于如何在Linux系统中配置USB存储禁用及例外的文章。禁用USB存储可以有效防止未经授权的人员从系统中复制数据或注入恶意软件…

CSS——盒子模型

首先CSS将所有的元素都看成一个盒子 盒子的组成&#xff1a; content —— 内容区域padding —— 内边距&#xff08;边框与内容间的距离&#xff09;border —— 边框线margin —— 外边距&#xff08;盒子盒子间的距离&#xff09; 这里着重说一下margin: 水平方向&#xff…

Kafka 基础与架构理解

目录 前言 Kafka 基础概念 消息队列简介&#xff1a;Kafka 与传统消息队列&#xff08;如 RabbitMQ、ActiveMQ&#xff09;的对比 Kafka 的组件 Kafka 的工作原理&#xff1a;消息的生产、分发、消费流程 Kafka 系统架构 Kafka 的分布式架构设计 Leader-Follower 机制与…

新品|瑞芯微RK3588工控机IPC8801适用AI算力、边缘计算、工业视觉

深圳触觉智能重磅推出旗舰级工控机IPC8801&#xff0c;搭载瑞芯微RK3588 ⼋核处理器&#xff1b;全铝紧凑机身、支持无风扇被动散热低噪音&#xff0c;确保设备在恶劣工业环境下稳定运行。 作为AI算力、边缘计算及工业视觉领域的高性能工控机&#xff0c;在国产化智能硬件与系统…

Axure PR 9 标签 设计交互

大家好&#xff0c;我是大明同学。 这期内容&#xff0c;我们将深入探讨Axure中可编辑标签元件设计与交互技巧。 可移除标签元件 创建可移除标签所需的元件 1.打开一个新的 RP 文件并在画布上打开 Page 1。 2.在元件库中拖出一个文本框元件。 3.选中文本框元件&#xff0c…

视频服务器:GB28181网络视频协议

一、前言 某项目中需要集成视频管理平台&#xff0c;实现分布在各省公司的摄像及接入&#xff0c;对视频进行统一管理。本项目中视频管理平台采用GB/T28181实现的监控设备接入管理平台&#xff0c;支持在开放互联网和局域网对监控设备进行远程接入、远程管理、远程调阅、录像回…

【文件包含】——日志文件注入

改变的确很难&#xff0c;但结果值得冒险 本文主要根据做题内容的总结&#xff0c;如有错误之处&#xff0c;还请各位师傅指正 一.伪协议的失效 当我们做到关于文件包含的题目时&#xff0c;常用思路其实就是使用伪协议&#xff08;php:filter,data,inpput等等&#xff09;执行…