FreeRTOS 内存管理源码解析

news2024/9/28 16:09:27

目录

  • 一、heap_1
    • 1、源码讲解
    • 2、总结
  • 二、heap_2
    • 1、源码讲解
      • 1.1 堆的初始化
      • 1.2 内存分配
      • 1.3 内存释放
    • 2、总结
  • 三、heap_3
  • 四、heap_4
    • 1、源码
      • 1.1 插入链表
      • 1.2 堆的初始化
      • 1.3 内存的申请
      • 1.4 内存的释放
    • 2、总结
  • 五、heap_5
    • 1、源码
      • 1.1 堆的初始化
      • 1.2 链表的插入、内存分配、释放
    • 2、总结


FreeRTOS 提供了 5 种不同的内存管理策略以应对不同的应用场景,本章将对这 5 种不同的内存管理策略的实现进行分析。

我参考的源码是:FreeRTOS-Kernel-10.4.3-LTS-Patch-3\portable\MemMang,该路径下记录了 heap_1.cheap_2.cheap_3.cheap_4.cheap_5.c,讲解会从这 5 个文件的源码入手。

一、heap_1

1、源码讲解

首先,FreeRTOS 将堆定义为一个大数组,并使用变量 xNextFreeByte 记录已用内存大小。

/* 字节对齐堆起始地址可能会丢失几个字节 */
#define configADJUSTED_HEAP_SIZE    ( configTOTAL_HEAP_SIZE - portBYTE_ALIGNMENT )

static uint8_t ucHeap[ configTOTAL_HEAP_SIZE ];

/* Index into the ucHeap array. */
static size_t xNextFreeByte = ( size_t ) 0;

核心代码 pvPortMalloc() 函数实现如下:

void * pvPortMalloc( size_t xWantedSize )
{
    void * pvReturn = NULL;
    static uint8_t * pucAlignedHeap = NULL;

    /* 如果对齐字节数不为1,则对请求的字节数做调整
     * 这里portBYTE_ALIGNMENT等于8 */
    #if ( portBYTE_ALIGNMENT != 1 )
        {
            /* 检查 xWantedSize 是否满足字节对齐的要求 
             * 如果不满足才会进入这句判断语句 */
            if( xWantedSize & portBYTE_ALIGNMENT_MASK )
            {
                /* 计算调整后的内存大小,并检查可能的溢出
                 * 这句判断计算需要调整的字节数,即需要添加的字节,以确保对齐 */
                if ( (xWantedSize + ( portBYTE_ALIGNMENT - ( xWantedSize & portBYTE_ALIGNMENT_MASK ) )) > xWantedSize )
                {
                    // 没有溢出
                    xWantedSize += ( portBYTE_ALIGNMENT - ( xWantedSize & portBYTE_ALIGNMENT_MASK ) );
                } 
                else 
                {
                    // 如果在调整大小时发生了溢出,这意味着请求的大小不合法。
                    xWantedSize = 0;
                }
            }
        }
    #endif

    /* 挂起任务 */
    vTaskSuspendAll();
    {
        if( pucAlignedHeap == NULL )
        {
            /* 作用:确保堆从正确对齐的边界开始
             * 宏定义:portBYTE_ALIGNMENT == 8,portBYTE_ALIGNMENT_MASK == 0x0007,portPOINTER_SIZE_TYPE 为 uint32_t
             * 可以整理一下方便看:
             * ( ( portPOINTER_SIZE_TYPE ) &ucHeap[ portBYTE_ALIGNMENT ] ) = x
             * ~( ( portPOINTER_SIZE_TYPE ) portBYTE_ALIGNMENT_MASK ) = y
             * (uint8_t *)( x & y );
             * 所以这里的操作为:
             * 第一句的作用:获取地址指向 ucHeap 数组中偏移 portBYTE_ALIGNMENT 的位置,
             *              并将指针转换为 portPOINTER_SIZE_TYPE 可以确保我们以适当的大小处理指针,
             *              避免潜在的数据丢失或不正当的操作。
             * 第二局的作用:将掩码转换为 portPOINTER_SIZE_TYPE 并按位取反,相当于变成了:0xFFF8(1111 1111 1111 1000)
             * 所以这一句的作用为清除 ucHeap 的低 3 位,使得分配的起始位置是满足 portBYTE_ALIGNMENT 对齐要求(8位)的 */
            pucAlignedHeap = ( uint8_t * ) ( ( ( portPOINTER_SIZE_TYPE ) & ucHeap[ portBYTE_ALIGNMENT ] ) & ( ~( ( portPOINTER_SIZE_TYPE ) portBYTE_ALIGNMENT_MASK ) ) );
        }

        /* 检查是否有足够的空间分配 */
        if( ( xWantedSize > 0 ) && /* valid size */
            ( ( xNextFreeByte + xWantedSize ) < configADJUSTED_HEAP_SIZE ) &&
            ( ( xNextFreeByte + xWantedSize ) > xNextFreeByte ) ) /* 防止数值溢出 */
        {
            /* 返回首地址 */
            pvReturn = pucAlignedHeap + xNextFreeByte;
            /* 记录已分配空间大小 */
            xNextFreeByte += xWantedSize;
        }

        traceMALLOC( pvReturn, xWantedSize );
    }
    ( void ) xTaskResumeAll();  /* 恢复任务 */

    return pvReturn;
}

需要注意的是,并未使用 configTOTAL_HEAP_SIZE 代表堆大小,而是用 configADJUSTED_HEAP_SIZE 表示堆大小,configADJUSTED_HEAP_SIZE 定义如下

#define configADJUSTED_HEAP_SIZE  ( configTOTAL_HEAP_SIZE - portBYTE_ALIGNMENT )

这里简单粗暴的丢弃掉了对齐字节数个字节,以此来表示堆的起始地址对齐的操作中损失的字节数(最多不会损失掉对齐字节数个字节)

heap_1.c 剩下的还有如下 3 个函数:

void vPortFree( void * pv )
{
    /* heap_1 没有实现如何释放内存!!! */
    ( void ) pv;

    /* 强制 assert,因为调用此函数无效 */
    configASSERT( pv == NULL );
}

void vPortInitialiseBlocks( void )
{
    /* 仅在未清除静态内存时需要 */
    xNextFreeByte = ( size_t ) 0;
}

size_t xPortGetFreeHeapSize( void )
{
    return( configADJUSTED_HEAP_SIZE - xNextFreeByte );
}

2、总结

heap_1.c 所实现的内存管理方法十分简单,其可以使用 pvPortMalloc() 函数来申请内存,一旦申请成功了,便无法被释放。其实现大致可以用一句话概括,在堆空间剩余时,按需分割出内存,并记录已用的内存大小heap_1.c 使用的内存管理算法虽然简单,但对于许多嵌入式应用场景是适用且有效的。

二、heap_2

1、源码讲解

/* 字节对齐堆起始地址可能会丢失几个字节 */
#define configADJUSTED_HEAP_SIZE    ( configTOTAL_HEAP_SIZE - portBYTE_ALIGNMENT )

static uint8_t ucHeap[ configTOTAL_HEAP_SIZE ];

/* 定义链表结构,这用于按大小的顺序链接空闲块. */
typedef struct A_BLOCK_LINK
{
    struct A_BLOCK_LINK * pxNextFreeBlock; /* 指向列表中的下一个空闲块. */
    size_t xBlockSize;                     /* 空闲块的大小(包括BlockLink_t 头部大小) */
} BlockLink_t;

/* 确定结构 BlockLink_t 的大小,同时考虑到内存的字节对齐要求 */
static const uint16_t heapSTRUCT_SIZE = ( ( sizeof( BlockLink_t ) + ( portBYTE_ALIGNMENT - 1 ) ) & ~portBYTE_ALIGNMENT_MASK );
#define heapMINIMUM_BLOCK_SIZE    ( ( size_t ) ( heapSTRUCT_SIZE * 2 ) )  /* 空闲块的最小大小 */

/* 创建两个列表链接以标记列表的开头和结尾. */
static BlockLink_t xStart, xEnd;

/* 跟踪剩余的空闲字节数,但不说明碎片 */
static size_t xFreeBytesRemaining = configADJUSTED_HEAP_SIZE;

然后 FreeRTOS 还定义了一个将内存块插入链表的宏:

/*
 * 将数据块插入到可用数据块列表中
 * 该列表按数据块大小排序。列表开头的块是小块和列表末尾的块是大块。
 */
#define prvInsertBlockIntoFreeList( pxBlockToInsert )                                                                               \
    {                                                                                                                               \
        BlockLink_t * pxIterator;                                                                                                   \
        size_t xBlockSize;                                                                                                          \
                                                                                                                                    \
        xBlockSize = pxBlockToInsert->xBlockSize;                                                                                   \
                                                                                                                                    \
        /* 遍历列表,直到找到一个比我们插入的块更大的块 */                                                                            \
        for( pxIterator = &xStart; pxIterator->pxNextFreeBlock->xBlockSize < xBlockSize; pxIterator = pxIterator->pxNextFreeBlock ) \
        {                                                                                                                           \
            /* 等待迭代到正确的位置 */                                                                                               \ 
        }                                                                                                                           \
                                                                                                                                    \
        /* 更新列表以包括插入到正确位置的块。 */                                                                                     \
        pxBlockToInsert->pxNextFreeBlock = pxIterator->pxNextFreeBlock;                                                             \
        pxIterator->pxNextFreeBlock = pxBlockToInsert;                                                                              \
    }

简单来说就是先查找适当的位置,然后将内存块插入到链表中,非常简单,不多说。

BlockLink_t 只描述了内存块的大小和内存块的链接关系,具体分配出的内存表示方式如下图所示:

1.1 堆的初始化

static void prvHeapInit( void )
{
    BlockLink_t * pxFirstFreeBlock;
    uint8_t * pucAlignedHeap;

    /* 确保堆从正确对齐的边界开始 
     * 和刚才 heap_1 中讲的一模一样,不理解再回去看 */
    pucAlignedHeap = ( uint8_t * ) ( ( ( portPOINTER_SIZE_TYPE ) & ucHeap[ portBYTE_ALIGNMENT ] ) & ( ~( ( portPOINTER_SIZE_TYPE ) portBYTE_ALIGNMENT_MASK ) ) );

    /* xStart 用于保存指向 free 块列表中第一项的指针。
     * void 强制转换用于防止编译器警告. */
    xStart.pxNextFreeBlock = ( void * ) pucAlignedHeap;
    xStart.xBlockSize = ( size_t ) 0;

    /* xEnd 用于标记空闲块列表的结尾 */
    xEnd.xBlockSize = configADJUSTED_HEAP_SIZE;
    xEnd.pxNextFreeBlock = NULL;

    /* 初始化第一个内存块,块大小为整个堆 */
    pxFirstFreeBlock = ( void * ) pucAlignedHeap;
    pxFirstFreeBlock->xBlockSize = configADJUSTED_HEAP_SIZE;
    pxFirstFreeBlock->pxNextFreeBlock = &xEnd;
}

大体来看,这个函数干了三件事,分别初始化链表起始时的三个内存块:

  1. 一块是链表的头部,被定义为 xStart,内存块大小为 0;
  2. 另一块是链表的尾部,被定义为 xEnd,内存块大小为整个堆大小 configADJUSTED_HEAP_SIZEconfigADJUSTED_HEA_SIZE 是堆对齐操作后的大小);
  3. 最后一个内存块是实际用于内存分配的普通块,其被定义在堆对齐后的起始地址上,块大小为整个堆大小 configADJUSTED_HEAP_SIZE

普通内存块相比,xStartxEnd 具有一些特殊性。

  • 不参与实际的内存分配操作。
  • xStartxEnd 都不存储在堆上。

经过该函数初始化后,整个堆的状态可以用下图表示:

1.2 内存分配

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

    vTaskSuspendAll();  /* 挂起任务 */
    {
        /* 如果这是第一次调用 malloc,则堆将需要初始化以设置空闲块列. */
        if( xHeapHasBeenInitialised == pdFALSE )
        {
            prvHeapInit();                      /* 刚才第一小节讲到的函数 */
            xHeapHasBeenInitialised = pdTRUE;   /* 记录下状态,表示堆空间初始化过 */
        }

        /* 必须增加所需的大小,以便除了请求的字节数之外,它还可以包含 BlockLink_t 结构 */
        if( ( xWantedSize > 0 ) && 
            ( ( xWantedSize + heapSTRUCT_SIZE ) >  xWantedSize ) ) /* 溢出检查 */
        {
            /* 重新计算所需块大小 */
            xWantedSize += heapSTRUCT_SIZE;

            /* 字节对齐操作,调整实际申请字节数,并检查溢出 */
            if( ( xWantedSize + ( portBYTE_ALIGNMENT - ( xWantedSize & portBYTE_ALIGNMENT_MASK ) ) ) 
                    > xWantedSize )
            {
                // 没有溢出
                xWantedSize += ( portBYTE_ALIGNMENT - ( xWantedSize & portBYTE_ALIGNMENT_MASK ) );
                configASSERT( ( xWantedSize & portBYTE_ALIGNMENT_MASK ) == 0 );
            }
            else
            {
                // 发生溢出,申请的内存不合法
                xWantedSize = 0;
            }       
        }
        else 
        {
            // 发生溢出,申请的内存不合法
            xWantedSize = 0; 
        }

        /* 申请内存合法,且有足够的空闲内存 */
        if( ( xWantedSize > 0 ) && ( xWantedSize <= xFreeBytesRemaining ) )
        {
            /* 块按字节顺序存储,从开始(最小)块遍历列表,直到找到足够大小的块。 */
            pxPreviousBlock = &xStart;
            pxBlock = xStart.pxNextFreeBlock;

            /* 遍历空闲块列表,直到找到一个合适大小的块 */
            while( ( pxBlock->xBlockSize < xWantedSize ) && ( pxBlock->pxNextFreeBlock != NULL ) )
            {
                pxPreviousBlock = pxBlock;
                pxBlock = pxBlock->pxNextFreeBlock;
            }

            /* 如果找到匹配的内存块,进行内存分配操作 */
            if( pxBlock != &xEnd )
            {
                /* 返回首地址值(跳过heapSTRUCT_SIZE ) */
                pvReturn = ( void * ) ( ( ( uint8_t * ) pxPreviousBlock->pxNextFreeBlock ) + heapSTRUCT_SIZE );

                /* 此块已被分配使用,因此必须从空闲块列表中删除. */
                pxPreviousBlock->pxNextFreeBlock = pxBlock->pxNextFreeBlock;

                /* 如果区块大于要求,则可以将其一分为二 */
                if( ( pxBlock->xBlockSize - xWantedSize ) > heapMINIMUM_BLOCK_SIZE )
                {
                    /* 初始化相应的块头部BlockLink_t 结构体 */
                    pxNewBlockLink = ( void * ) ( ( ( uint8_t * ) pxBlock ) + xWantedSize );

                    /* 更新相应的块地址大小 */
                    pxNewBlockLink->xBlockSize = pxBlock->xBlockSize - xWantedSize;
                    pxBlock->xBlockSize = xWantedSize;

                    /* 将分割出的新块插入链表中 */
                    prvInsertBlockIntoFreeList( ( pxNewBlockLink ) );
                }

                /* 记录堆剩余字节数 */
                xFreeBytesRemaining -= pxBlock->xBlockSize;
            }
        }

        traceMALLOC( pvReturn, xWantedSize );
    }
    ( void ) xTaskResumeAll();  /* 恢复任务 */

    return pvReturn;
}

代码看似很长,其实逻辑比较简单,大致分为如下几个步骤:

  1. 调整实际需要申请的内存(内存对齐)
  2. 检测申请字节数是否合法,若合法,则寻找合适的内存块进行分配。
  3. 将分配出去的内存块从链表中移除,若剩余内存大于最小内存块大小,则将剩余的内存块重新添加回链表中。
  4. 记录剩余字节数,返回分配内存空间的地址。

堆在初始状态下,进行一次成功的内存分配后,其状态如下图所示:

1.3 内存释放

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

    if( pv != NULL )
    {
        /* 定位内存块头部 */
        puc -= heapSTRUCT_SIZE;

        /* 这种意外的强制转换是为了防止某些编译器发出字节对齐警告 */
        pxLink = ( void * ) puc;

        vTaskSuspendAll();
        {
            /* 将此数据块添加到空闲数据块列表中 */
            prvInsertBlockIntoFreeList( ( ( BlockLink_t * ) pxLink ) );
            xFreeBytesRemaining += pxLink->xBlockSize;
            traceFREE( pv, pxLink->xBlockSize );  /* 跟踪内存分配,不用管它 */
        }
        ( void ) xTaskResumeAll();
    }
}

释放内存,就是把这个块重新插入到空闲数据块链表中。

释放内存后,堆的空间状态如下图所示:

2、总结

heap_1.c 不同,heap_2.c 允许使用 vPortFree() 函数来释放申请的内存,其算法原理是将空闲堆分为若干个大小不等的内存块,并将其按大小排序,使用单向链表连接起来。

申请内存时,便从这些链表中寻找最小可满足申请需求的内存块进行分配。分配过程分为两步,首先将原先的内存块的链表项从链表中删除,其次是对当前内存块进行分割,将多余申请数的那部分内存变为新的链表项重新插入到链表中。释放过程则更为简单,只需要将释放的内存块重新插入到链表中即可。

从源码分析中,可以看出:随着申请释放的次数增加,heap_2.c 将使得内存块被越分越小(内存碎片),这会导致以下两个问题:

  1. 当需要再次请求一个大的内存块时,即使 xFreeBytesRemaining 大于请求的内存块,其也无法进行分配了。
  2. 大量的内存被 BlockLink_t 头部占用,导致堆的利用率降低

那有什么改进办法呢?学习操作系统的时候,提到将相邻的内存块合并可以缓解碎片化的问题,在我以前写的 Linux 内存管理(三)之分页机制 中也曾提到过这个。而 FreeRTOSheap_4.c 中实现了内存块合并。

三、heap_3

这个就没什么好说的了,纯粹是封装了 C 标准库中的 mallocfree 函数:

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

    vTaskSuspendAll();
    {
        pvReturn = malloc( xWantedSize );
        traceMALLOC( pvReturn, xWantedSize );
    }
    ( void ) xTaskResumeAll();

    return pvReturn;
}

void vPortFree( void * pv )
{
    if( pv )
    {
        vTaskSuspendAll();
        {
            free( pv );
            traceFREE( pv, 0 );
        }
        ( void ) xTaskResumeAll();
    }
}

下面重点看 heap_4 的原理和实现。

四、heap_4

1、源码

/* 区块大小不能太小 */
#define heapMINIMUM_BLOCK_SIZE    ( ( size_t ) ( xHeapStructSize << 1 ) )

/* 假设 8 位字节 */
#define heapBITS_PER_BYTE         ( ( size_t ) 8 )

PRIVILEGED_DATA static uint8_t ucHeap[ configTOTAL_HEAP_SIZE ];

/* 定义链表结构。 这用于按内存地址的顺序链接空闲块 */
typedef struct A_BLOCK_LINK
{
    struct A_BLOCK_LINK * pxNextFreeBlock; /* 指向列表中的下一个空闲块 */
    size_t xBlockSize;                     /* 空闲块的大小 */
} BlockLink_t;

/* 位于每个已分配内存块开头的结构体的大小,必须正确地进行字节对齐. */
static const size_t xHeapStructSize = ( sizeof( BlockLink_t ) + ( ( size_t ) ( portBYTE_ALIGNMENT - 1 ) ) ) & ~( ( size_t ) portBYTE_ALIGNMENT_MASK );

/* 创建两个列表链接以标记列表的开头和结尾 */
PRIVILEGED_DATA static BlockLink_t xStart, * pxEnd = NULL;

/* 跟踪要分配和释放内存的调用数以及剩余的空闲字节数,但不说明碎片 */
PRIVILEGED_DATA static size_t xFreeBytesRemaining = 0U;
PRIVILEGED_DATA static size_t xMinimumEverFreeBytesRemaining = 0U;
PRIVILEGED_DATA static size_t xNumberOfSuccessfulAllocations = 0;
PRIVILEGED_DATA static size_t xNumberOfSuccessfulFrees = 0;

/* 设置为 size_t 类型的顶部。
 * 如果 xBlockAllocatedBit 的值为 0,那么这个内存块还没有被分配;
 * 如果 xBlockAllocatedBit 的值为 1,那么这个内存块已经被分配 */
PRIVILEGED_DATA static size_t xBlockAllocatedBit = 0;

1.1 插入链表

内存块合并算法主要是在 prvInsertBlockIntoFreeList() 函数中实现。与 heap_2.c 中按内存块大小顺序插入不同,heap_4.c 是按地址大小的顺序插入,这样便于合并相邻的内存块。插入过程分为以下两步:

  1. 查找链表中链接的下一个内存块地址大于待插入内存块地址的第一个内存块(也就是与待插入内存块相邻的且地址较低的那一个内存块)的地址。
  2. 检测待插入内存块是否能与相邻的内存块合并。
    • 若能与低地址的相邻内存块合并,直接在低地址相邻的内存块大小上加上待插入内存块大小;
    • 若能与高地址的相邻内存块合并或可以同时将高低地址邻近内存块相连,则需要同时调整链表指针与内存块大小。

如上图,要插入 BlockToInsert

  1. 其会先遍历链表寻找 LowAddressAdjacentBlock,然后判断:
    • BlockToInsert 仅能和 LowAddressAdja-centBlock 合并,则将 LowAddressAdjacentBlock 的块大小更改为 LowAddressAdjacentBlockBlockToInsert 大小的和;
    • BlockToInsert 仅能和 HighAddressAdjacentBlock 合并,则用 BlockToInsert 替换 HighAddressAdjacentBlock 在链表中的位置,并修改块大小为两者之和;
    • BlockToInsert 能将 LowAddressAdjacentBlockHighAddressAdjacentBlock 连接成一整块,则从链表中删除 HighAddressAdjacentBlock,并将 LowAddressAdjacentBlock 的块大小变为三者之和;
    • BlockToInsert 是一个孤立的内存块则将其正常的插入到 LowAddressAdjacentBlockHighAddressAdjacentBlock 之间。
static void prvInsertBlockIntoFreeList( BlockLink_t * pxBlockToInsert ) /* PRIVILEGED_FUNCTION */
{
    BlockLink_t * pxIterator;
    uint8_t * puc;

    /* 遍历列表,直到找到地址高于要插入的块的块 */
    for( pxIterator = &xStart; pxIterator->pxNextFreeBlock < pxBlockToInsert; pxIterator = pxIterator->pxNextFreeBlock )
    {

    }

    /* 检验待插入内存块是否紧接在低地址邻近内存块后 */
    puc = ( uint8_t * ) pxIterator;
    if( ( puc + pxIterator->xBlockSize ) == ( uint8_t * ) pxBlockToInsert )
    {
        /* 如果是,改变低地址邻近内存块的内存块大小 */
        pxIterator->xBlockSize += pxBlockToInsert->xBlockSize;
        /* 改变待插入内存块地址,将插入内存块与低地址邻近内存块邻近内存块合并后的块看作是新的待插入块 */
        pxBlockToInsert = pxIterator;
    }
    else
    {
        mtCOVERAGE_TEST_MARKER();
    }

    /* 检验待高地址邻近内存块是否紧接在待插入内存块(或待插入内存块与低地址邻近内存块邻近内存块合并后的块)后 */
    puc = ( uint8_t * ) pxBlockToInsert;
    if( ( puc + pxBlockToInsert->xBlockSize ) == ( uint8_t * ) pxIterator->pxNextFreeBlock )
    {
        if( pxIterator->pxNextFreeBlock != pxEnd )
        {
            /* 计算合并的内存块大小 */
            pxBlockToInsert->xBlockSize += pxIterator->pxNextFreeBlock->xBlockSize;
            /* 调整链表链接位置 */
            pxBlockToInsert->pxNextFreeBlock = pxIterator->pxNextFreeBlock->pxNextFreeBlock;
        }
        else
        {
            /* pxEnd 特殊处理 */
            pxBlockToInsert->pxNextFreeBlock = pxEnd;
        }
    }
    else
    {
        /* 如果待插入内存块与高地址邻近内存块不能合并,调整待插入内存块的下一链接为高地址邻近内存块 */
        pxBlockToInsert->pxNextFreeBlock = pxIterator->pxNextFreeBlock;
    }

    /* 如果pxIterator 与pxBlockToInsert 值不等,意味着低地址邻近内存块的内存块与待插入内存块并未合并,
     * 因此需要将待插入内存块挂接在pxIterator 后面 */
    if( pxIterator != pxBlockToInsert )
    {
        pxIterator->pxNextFreeBlock = pxBlockToInsert;
    }
    else
    {
        mtCOVERAGE_TEST_MARKER();
    }
}

这样每次插入时,便可自动的合并掉相邻的内存块,以生成更大的内存块。但这并不意味着内存的碎片化问题被解决了。可以看以下的一个示例,当其中的 Used2 被释放,是其仍然会产生内存碎片,除非 Used1Used3 被释放,其才可能被拼接成较大的内存块。

1.2 堆的初始化

heap_4.c 的堆初始化与 heap_2.c 的初始化大同小异,不同点有以下两点:

  1. 其使用 BlockLink_t 结构体成员 xBlockSize 的最高位来标记一个内存块是否被使用,1 表示在使用,0 表示空闲。
  2. 原本的 xEnd 被定义在了堆上,且是堆的尾部,用 pxEnd 指向其地址。

heap_4.c 初始化堆后,堆状态为:

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

    /* 确保堆从正确对齐的边界开始 */
    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 用于保存指向 free 块列表中第一项的指针
     * void 强制转换用于防止编译器警告. */
    xStart.pxNextFreeBlock = ( void * ) pucAlignedHeap;
    xStart.xBlockSize = ( size_t ) 0;

    /* pxEnd 用于标记可用块列表的末尾,并插入到堆空间的末尾 */
    uxAddress = ( ( size_t ) pucAlignedHeap ) + xTotalHeapSize;
    uxAddress -= xHeapStructSize;
    uxAddress &= ~( ( size_t ) portBYTE_ALIGNMENT_MASK );
    pxEnd = ( void * ) uxAddress;
    pxEnd->xBlockSize = 0;
    pxEnd->pxNextFreeBlock = NULL;

    /* 初始化第一个内存块,块大小为整个堆空间减去 pxEnd 占用的空间. */
    pxFirstFreeBlock = ( void * ) pucAlignedHeap;
    pxFirstFreeBlock->xBlockSize = uxAddress - ( size_t ) pxFirstFreeBlock;
    pxFirstFreeBlock->pxNextFreeBlock = pxEnd;

    /* 只有一个块存在 - 它覆盖了整个可用的堆空间 */
    xMinimumEverFreeBytesRemaining = pxFirstFreeBlock->xBlockSize;
    xFreeBytesRemaining = pxFirstFreeBlock->xBlockSize;

    /* 计算出 size_t 变量中最高位的位置 */
    xBlockAllocatedBit = ( ( size_t ) 1 ) << ( ( sizeof( size_t ) * heapBITS_PER_BYTE ) - 1 );
}

1.3 内存的申请

heap_4.c 的内存的申请与释放过程与 heap_2.c 相比除了增加了对 xBlockSize 最高位的处理外,没有太大的不同。

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

    vTaskSuspendAll();
    {
        /* 如果这是第一次调用 malloc,则堆将需要初始化以设置空闲块列表 */
        if( pxEnd == NULL )
        {
            prvHeapInit();
        }
        else
        {
            mtCOVERAGE_TEST_MARKER();
        }

        /* 检查最高位是否设置为 1,以确定是否已经分配了内存块 */
        if( ( xWantedSize & xBlockAllocatedBit ) == 0 )
        {
            /* 必须增加所需的大小,以便除了请求的字节数之外,它还可以包含 BlockLink_t 结构 */
            if( ( xWantedSize > 0 ) && 
                ( ( xWantedSize + xHeapStructSize ) >  xWantedSize ) ) /* 检查溢出 */
            {
                xWantedSize += xHeapStructSize;

                /* 确保块是对齐的 */
                if( ( xWantedSize & portBYTE_ALIGNMENT_MASK ) != 0x00 )
                {
                    /* 没有溢出 */
                    if( ( xWantedSize + ( portBYTE_ALIGNMENT - ( xWantedSize & portBYTE_ALIGNMENT_MASK ) ) ) 
                            > xWantedSize )
                    {
                        xWantedSize += ( portBYTE_ALIGNMENT - ( xWantedSize & portBYTE_ALIGNMENT_MASK ) );
                        configASSERT( ( xWantedSize & portBYTE_ALIGNMENT_MASK ) == 0 );
                    }
                    else
                    {
                        /* 发生溢出,申请的内存太大 */
                        xWantedSize = 0;
                    }  
                }
                else
                {
                    mtCOVERAGE_TEST_MARKER();
                }
            } 
            else 
            {
                xWantedSize = 0;
            }

            /* 申请的内存合法,且有空闲内存 */
            if( ( xWantedSize > 0 ) && ( xWantedSize <= xFreeBytesRemaining ) )
            {
                /* 从起始(最低地址)块遍历列表,直到找到足够大小的块 */
                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( ( 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;
                    xNumberOfSuccessfulAllocations++;
                }
                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;
}

1.4 内存的释放

heap_2.c 版本的 vPortFree 类似,不过多了对 xBlockSize 最高位的处理。

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

    if( pv != NULL )
    {
        puc -= xHeapStructSize;

        pxLink = ( void * ) puc;

        /* 确保块对齐且是已分配的 */
        configASSERT( ( pxLink->xBlockSize & xBlockAllocatedBit ) != 0 );
        configASSERT( pxLink->pxNextFreeBlock == NULL );

        /* 块是已分配的 */
        if( ( pxLink->xBlockSize & xBlockAllocatedBit ) != 0 )
        {
            if( pxLink->pxNextFreeBlock == NULL )
            {
                /* 重新标志位未分配 */
                pxLink->xBlockSize &= ~xBlockAllocatedBit;

                vTaskSuspendAll();
                {
                    /* 将块插入到空闲块链表中 */
                    xFreeBytesRemaining += pxLink->xBlockSize;
                    traceFREE( pv, pxLink->xBlockSize );
                    prvInsertBlockIntoFreeList( ( ( BlockLink_t * ) pxLink ) );
                    xNumberOfSuccessfulFrees++;
                }
                ( void ) xTaskResumeAll();
            }
            else
            {
                mtCOVERAGE_TEST_MARKER();
            }
        }
        else
        {
            mtCOVERAGE_TEST_MARKER();
        }
    }
}

2、总结

相比 heap_2.cheap_4.c 可以实现相邻小内存块的合并,在一定程度上缓解内存碎片化的问题。

五、heap_5

1、源码

/* 块大小不能太小 */
#define heapMINIMUM_BLOCK_SIZE    ( ( size_t ) ( xHeapStructSize << 1 ) )

/* 假设 8 位字节 */
#define heapBITS_PER_BYTE         ( ( size_t ) 8 )

/* 定义链表结构,这用于按内存地址的顺序链接空闲块。 */
typedef struct A_BLOCK_LINK
{
    struct A_BLOCK_LINK * pxNextFreeBlock; /* 指向列表中的下一个空闲块 */
    size_t xBlockSize;                     /* 空闲块的大小 */
} BlockLink_t;

/* 位于每个已分配内存块开头的结构体的大小必须正确地进行字节对齐 */
static const size_t xHeapStructSize = ( sizeof( BlockLink_t ) + ( ( size_t ) ( portBYTE_ALIGNMENT - 1 ) ) ) & ~( ( size_t ) portBYTE_ALIGNMENT_MASK );

/* 创建两个列表链接来标记列表的开头和结尾 */
static BlockLink_t xStart, * pxEnd = NULL;

/* 跟踪要分配和释放内存的调用数以及剩余的空闲字节数,但不说明碎片 */
static size_t xFreeBytesRemaining = 0U;
static size_t xMinimumEverFreeBytesRemaining = 0U;
static size_t xNumberOfSuccessfulAllocations = 0;
static size_t xNumberOfSuccessfulFrees = 0;

/* 设置为 size_t 类型的顶部。
 * 如果 xBlockAllocatedBit 的值为 0,那么这个内存块还没有被分配;
 * 如果 xBlockAllocatedBit 的值为 1,那么这个内存块已经被分配 */
static size_t xBlockAllocatedBit = 0;

1.1 堆的初始化

heap_5.c 的堆初始化由 vPortDefineHeapRegions() 这一函数实现,其传入参数是一个具有特定格式的结构体数组。

HeapRegion_t xHeapRegions[] =
{
	/* 起始地址为0x80000000 ,大小为0x10000 的内存块 */
    { ( uint8_t * ) 0x80000000UL, 0x10000 }, 
    /* 起始地址为0x90000000 ,大小为0xa0000 的内存块,地址递增排序 */
    { ( uint8_t * ) 0x90000000UL, 0xa0000 }, 
    /* 结束标识符 */
    { NULL, 0 }             
};

vPortDefineHeapRegions() 所做的工作就是读取结构体数组中的每一个内存块信息,并将其编入链表中。以上面的参数为例,初始化后堆的状态如下图所示:

void vPortDefineHeapRegions( const HeapRegion_t * const pxHeapRegions )
{
    BlockLink_t * pxFirstFreeBlockInRegion = NULL, * pxPreviousFreeBlock;
    size_t xAlignedHeap;
    size_t xTotalRegionSize, xTotalHeapSize = 0;
    BaseType_t xDefinedRegions = 0;
    size_t xAddress;
    const HeapRegion_t * pxHeapRegion;

    /* 只能调用一次! */
    configASSERT( pxEnd == NULL );

    pxHeapRegion = &( pxHeapRegions[ xDefinedRegions ] );

    while( pxHeapRegion->xSizeInBytes > 0 )
    {
        xTotalRegionSize = pxHeapRegion->xSizeInBytes;

        /* 确保内存对齐 */
        xAddress = ( size_t ) pxHeapRegion->pucStartAddress;
        if( ( xAddress & portBYTE_ALIGNMENT_MASK ) != 0 )
        {
            xAddress += ( portBYTE_ALIGNMENT - 1 );
            xAddress &= ~portBYTE_ALIGNMENT_MASK;

            /* 调整因对齐而丢失的字节的大小 */
            xTotalRegionSize -= xAddress - ( size_t ) pxHeapRegion->pucStartAddress;
        }

        xAlignedHeap = xAddress;

        /* 如果尚未设置 xStart,就设置 xStart */
        if( xDefinedRegions == 0 )
        {
            /* xStart 用于保存指向空闲块列表中第一项的指针 */
            xStart.pxNextFreeBlock = ( BlockLink_t * ) xAlignedHeap;
            xStart.xBlockSize = ( size_t ) 0;
        }
        else
        {
            /* 仅当已将一个区域添加到堆时,才应到达此处 */
            configASSERT( pxEnd != NULL );

            /* 检查块的传入增加了起始地址 */
            configASSERT( xAddress > ( size_t ) pxEnd );
        }

        /* 记住结束标记在上一个区域中的位置(如果有) */
        pxPreviousFreeBlock = pxEnd;

        /* 将 pxEnd 插入到区域空间的末尾 */
        xAddress = xAlignedHeap + xTotalRegionSize;
        xAddress -= xHeapStructSize;
        xAddress &= ~portBYTE_ALIGNMENT_MASK;
        pxEnd = ( BlockLink_t * ) xAddress;
        pxEnd->xBlockSize = 0;
        pxEnd->pxNextFreeBlock = NULL;

        /* 初始化第一个内存块,块大小为整个堆空间减去 free block 结构所占用的空间。 */
        pxFirstFreeBlockInRegion = ( BlockLink_t * ) xAlignedHeap;
        pxFirstFreeBlockInRegion->xBlockSize = xAddress - ( size_t ) pxFirstFreeBlockInRegion;
        pxFirstFreeBlockInRegion->pxNextFreeBlock = pxEnd;

        /* 如果这不是构成整个堆空间的第一个区域,则将上一个区域链接到此区域 */
        if( pxPreviousFreeBlock != NULL )
        {
            pxPreviousFreeBlock->pxNextFreeBlock = pxFirstFreeBlockInRegion;
        }

        xTotalHeapSize += pxFirstFreeBlockInRegion->xBlockSize;

        xDefinedRegions++;
        pxHeapRegion = &( pxHeapRegions[ xDefinedRegions ] );
    }

    xMinimumEverFreeBytesRemaining = xTotalHeapSize;
    xFreeBytesRemaining = xTotalHeapSize;

    configASSERT( xTotalHeapSize );

    /* 计算出 size_t 变量中最高位的位置 */
    xBlockAllocatedBit = ( ( size_t ) 1 ) << ( ( sizeof( size_t ) * heapBITS_PER_BYTE ) - 1 );
}

1.2 链表的插入、内存分配、释放

heap_4 的实现,参照前面即可。

2、总结

之前的 heap_1.cheap_2.cheap_4.c 都将堆定义成了一个大数组,这意味着堆的地址必须是连续的,但在实际使用时,有时需要管理两大块或更多的不连续内存,这时便可以使用 heap_5.c 这一实现。其实在之前的 heap_2.cheap_4.c 中,已经实现了对不连续内存的管理,与 heap_4.c 相比,heap_5.c 的改变仅仅是在堆的初始化上,其将多个堆内存块加入了链表而已。

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

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

相关文章

五.海量数据实时分析-FlinkCDC+DorisConnector实现数据的全量增量同步

前言 前面四篇文字都在学习Doris的理论知识&#xff0c;也是比较枯燥&#xff0c;当然Doris的理论知识还很多&#xff0c;我们后面慢慢学&#xff0c;本篇文章我们尝试使用SpringBoot来整合Doris完成基本的CRUD。 由于 Doris 高度兼容 Mysql 协议&#xff0c;两者在 SQL 语法…

【滑动窗口算法】——定长滑动窗口——Python(附题)

一.定长滑动窗口是什么及其使用场景 定长滑动窗口算法的核心思想是使用两个指针&#xff0c;通常称为“左指针”和“右指针”。窗口的大小是固定的&#xff0c;右指针用于扩展窗口&#xff0c;直到达到指定大小&#xff0c;而左指针则在窗口移动时逐步向右滑动。这样可以高效地…

3款照片人物开口说话AI工具,跟真人说话一样~免费!短视频带货必备!(附教程)

大家好&#xff0c;我是画画的小强 今天给大家分享一个AI图片口播数字人讲认知思维&#xff0c;单号佣金赚5W的AI带货信息差玩法&#xff0c;许多小伙伴表示对这类AI带货玩法感兴趣。 说实话&#xff0c;现在AI照片人物对口型工具&#xff0c;越来越逼真&#xff0c;很难辨识出…

Codesys trace工具右键菜单框呼出异常的问题解决

这个问题困扰了好久&#xff0c;添加trace工具之后&#xff0c;一但在模型图位置右键后&#xff0c;整个codesys界面都无法呼出右键菜单&#xff0c;甚至会出现键盘输入失效的问题。 解决办法&#xff1a;更新trace工具 1、工具 —> CODESYS Installer 或者搜索CODESYS In…

AI新时代序幕!大模型研究报告(附AI名词详解)

AI新时代序幕&#xff01;大模型研究报告&#xff08;附AI名词详解&#xff09; 前言AI 大模型研究报告 前言 AI通过大规模的数据训练和先进的算法构建而成的&#xff0c;能够模拟人类的智能&#xff0c;处理各种复杂的任务。它可以理解我们的语言&#xff0c;回答我们的问题&…

PyQt5 statusbar 放图片并设置图片大小和左右间距

在 PyQt5 中&#xff0c;状态栏&#xff08;QStatusBar&#xff09;通常用于显示窗口的状态信息或提示。虽然 PyQt5 的 QStatusBar 没有直接提供设置图片作为状态栏项&#xff08;QStatusBarItem&#xff09;的 API&#xff0c;但你可以通过添加一个 QWidget&#xff08;如 QLa…

Java实现找色和找图功能

某天&#xff0c;张三接到一个任务需求&#xff0c;将一个Excel表格里面的员工信息&#xff0c;录入到员工系统里面&#xff0c;由于数据量非常大&#xff0c;操作起来巨慢。经过一段时间的操作和观察&#xff0c;他发现这种操作&#xff0c;非常有规律&#xff0c;基本就是一些…

html+css+js实现Pagination 分页

效果图 HTML部分 <body><div class"pagination"><button class"prev"><</button><ul><li class"active">1</li><li>2</li><li>3</li><li>4</li><li>5…

基于springboot+vue+mysql公益旧物捐赠系统(源码+参考文档+定制)

博主介绍&#xff1a; ✌我是阿龙&#xff0c;一名专注于Java技术领域的程序员&#xff0c;全网拥有10W粉丝。作为CSDN特邀作者、博客专家、新星计划导师&#xff0c;我在计算机毕业设计开发方面积累了丰富的经验。同时&#xff0c;我也是掘金、华为云、阿里云、InfoQ等平台…

3d可视化图片:通过原图和深度图实现

1、depthy 在线体验demo: https://depthy.stamina.pl/#/ 也可以docker安装上面服务: docker run --rm -t -i -p 9000:9000 ndahlquist/depthy http://localhost:90001)首先传原图 2)再传对应深度图 3)效果

云栖大会观察:云计算第三次浪潮下的暗流涌动

如果跳脱出今年云栖大会的“云启智跃产业蝶变”、“云计算第三次浪潮”这些设定好的视角&#xff0c;站在“AI有着太多的未知性”这个角度观察这次大会&#xff0c;会看到什么&#xff1f; 我们会看到&#xff0c;对于现在的阿里云而言&#xff0c;AI带来的并非都是机遇&#…

【Gitee自动化测试4】本地Git分支的增删查,本地Git分支中文件的增删查,本地文件的暂存/提交,本地分支的推送

一、流程 本地创建分支&#xff0c;设定连接什么云分支本地创建文件&#xff0c;暂存、提交–>本地分支本地分支推送所有修改–>云仓库 二、分支概念 在版本回退里&#xff0c;每次提交&#xff0c;git都把它们串成一条时间线&#xff0c;这条时间线可以理解为是一个分…

Meta Sapiens 人体AI模型

Meta 一直是开发图像和视频模型的领导者&#xff0c;现在他们又增加了一个新东西&#xff1a;Meta Sapiens。和Homo sapiens一样&#xff0c;这个模型也是关于人类的。它旨在执行与人类相关的任务&#xff0c;例如理解身体姿势、识别身体部位、预测深度&#xff0c;甚至确定皮肤…

小学三年级数学拓展填空题

用传统思维来看小学的学习是错误的。 学校考核老师主要看学生成绩&#xff0c;导致学生作业很多。 而且&#xff0c;现在的中小学生是不是太卷了&#xff1f;都开始卷远超过自己年龄阶段应该掌握的内容了&#xff1f;——这才是很不正常的现象。 如果大家都这么卷&#xff0c…

自然资源部最新Nature正刊!!!

2024年8月21日&#xff0c;国际顶级期刊《Nature》发表了自然资源部第二海洋研究所李家彪院士为通讯作者&#xff0c;张涛为第一作者的论文“超慢速扩张加克洋中脊的高变化岩浆增生”。这一成果颠覆了国际海洋学术界半个多世纪以来一直认为的超慢速扩张洋中脊岩浆供给极度贫瘠的…

9--苍穹外卖-SpringBoot项目中Redis的介绍及其使用实例 详解

目录 Redis入门 Redis简介 Redis服务启动与停止 服务启动命令 Redis数据类型 5种常用数据类型介绍 各种数据类型的特点 Redis常用命令 字符串操作命令 哈希操作命令 列表操作命令 集合操作命令 有序集合操作命令 通用命令 在java中操作Redis Redis的Java客户端 …

excel统计分析(4): 多元线性回归分析

用途&#xff1a;研究多个自变量&#xff08;也称为预测变量或解释变量&#xff09;与一个因变量&#xff08;也称为响应变量&#xff09;之间的线性关系。 多元线性回归分析模型&#xff1a;Yβ0β1X1β2X2…βkXkϵ Y 是因变量。1,X2,…,Xk 是自变量。β0 是截距项。β1,β2,…

ROSTCM6+Gephi的网络语义分析详细教程(附案例实战)

&#x1f935;‍♂️ 个人主页&#xff1a;艾派森的个人主页 ✍&#x1f3fb;作者简介&#xff1a;Python学习者 &#x1f40b; 希望大家多多支持&#xff0c;我们一起进步&#xff01;&#x1f604; 如果文章对你有帮助的话&#xff0c; 欢迎评论 &#x1f4ac;点赞&#x1f4…

经济不好,但是遍地都是赚钱的机会

现在职场越来越内卷&#xff0c;裁员风波也是不断&#xff0c;前些天看到一个帖子&#xff0c;裁员都裁到应届生头上了。 都说00后整治职场&#xff0c;在如今环境下也要掂量一下了。 大家都在抱怨环境&#xff0c;可是你有没有想过&#xff0c;有些人在闷声发着小财。 下面…

vue2 vconsole有助于移动端开发页面调试

项目场景&#xff1a; pc项目开发中&#xff0c;有浏览器自带的调试工具。但在移动端&#xff0c;就需要自己搭建调试工具了。vconsole一种非常方便的前端调试依赖库&#xff0c;有助于我们在移动端开发式进行调试&#xff0c;快速排查移动端问题。 搭建步骤 1、安装依赖库。…