LwIP系列--内存管理(堆内存)详解

news2024/11/16 7:40:54

一、目的

小型嵌入式系统中的内存资源(SRAM)一般都比较有限,LwIP的运行平台一般都是资源受限的MCU。基于此为了能够更加高效的运行,LwIP设计了基于内存池、内存堆的内存管理以及在处理数据包时的pbuf数据结构。

本篇的主要目的是介绍基于内存堆的内存管理原理。

内存堆内存管理的特点:

  • 按需分配,需要多少内存就分配多少内存(存在最小分配内存限制)

  • 内存易碎片化

  • 内存回收时一般会进行头部和尾部拼接,尽量减少内存碎片的产生

内存堆本质上是一大块连续内存(可以理解为数组),当需要内存时就从这个数组中按照特定算法切分一块所需大小的内存块(包含管理此块内存的内存块管理结构),将这块内存的地址提供出去(可以认为分蛋糕,需要吃多少就切多少,这样就不存在浪费);剩余内存仍然存放在内存堆中以便继续分配。

二、介绍

在正式介绍之前,我们需要理解几个宏定义和内存堆相关的数据结构

内存块管理结构

/**
 * The heap is made up as a list of structs of this type.
 * This does not have to be aligned since for getting its size,
 * we only use the macro SIZEOF_STRUCT_MEM, which automatically aligns.
 */
struct mem {
  /** index (-> ram[next]) of the next struct */
  mem_size_t next;
  /** index (-> ram[prev]) of the previous struct */
  mem_size_t prev;
  /** 1: this area is used; 0: this area is unused */
  u8_t used;
};

其中mem_size_t的定义如下

#if MEM_SIZE > 64000L
typedef u32_t mem_size_t;
#define MEM_SIZE_F U32_F
#else
typedef u16_t mem_size_t;
#define MEM_SIZE_F U16_F
#endif /* MEM_SIZE > 64000 */
#endif

根据MEM_SIZE是否大于64000字节,决定mem_size_t是u32_t还是u16_t类型。

struct mem内存块各字段含义如下:

next:下一个内存块的位置(注意此处是数组下标,并不是地址)

prev:上一个内存块的位置

used:表明此内存块是否已经使用(分配)


为了帮助大家更好的理解,大家可以按照下图所示从整体上理解内存堆中的内存块

内存堆示意图

上图中struct mem A和X都是内存块管理结构,紧跟着struct mem A的是A实际管理的内存区域,注意A当前管理的内存块是未使用的。

struct mem X是最后一个内存管理结构(作为tailer标志,作为内存堆中的最后一个内存管理块),其next/prev都是指向自身位置,并且是标记为已经使用的。


内存堆管理结构

为了更好的管理内存堆,在LwIP中还额外定义了几个全局方便管理内存堆,其定义如下

/** pointer to the heap (ram_heap): for alignment, ram is now a pointer instead of an array */
static u8_t *ram;
/** the last entry, always unused! */
static struct mem *ram_end;
/** pointer to the lowest free block, this is used for faster search */
static struct mem *lfree;

/** concurrent access protection */
#if !NO_SYS
static sys_mutex_t mem_mutex;
#endif

ram指向整个对齐后的内存堆首地址(开发者提供的内存空间的首地址未必是4字节对齐,所以需要对齐)


ram_end指向内存堆的末尾地址(结合上图就是指向struct mem X的位置,因为struct mem X是作为一个占位符,本身不管理实际内存数据)


lfree指向内存堆中可用内存块的第一个

上图中黄色的块代表已经使用的内存,白色的块代表未使用的数据块;可以看到白色的快和黄色的块是会出现随机交叉的情况的(经过多次内存分配后会出现内存空间碎片化的情况),此处的lfree就是指向上图的中第一个白色的数据块


mem_mutex是在rtos中通过互斥锁保护内存操作的


宏定义说明

/** All allocated blocks will be MIN_SIZE bytes big, at least!
 * MIN_SIZE can be overridden to suit your needs. Smaller values save space,
 * larger values could prevent too small blocks to fragment the RAM too much. */
#ifndef MIN_SIZE
#define MIN_SIZE             12
#endif /* MIN_SIZE */
/* some alignment macros: we define them here for better source code layout */
#define MIN_SIZE_ALIGNED     LWIP_MEM_ALIGN_SIZE(MIN_SIZE)
#define SIZEOF_STRUCT_MEM    LWIP_MEM_ALIGN_SIZE(sizeof(struct mem))
#define MEM_SIZE_ALIGNED     LWIP_MEM_ALIGN_SIZE(MEM_SIZE)

/** If you want to relocate the heap to external memory, simply define
 * LWIP_RAM_HEAP_POINTER as a void-pointer to that location.
 * If so, make sure the memory at that location is big enough (see below on
 * how that space is calculated). */
#ifndef LWIP_RAM_HEAP_POINTER
/** the heap. we need one struct mem at the end and some room for alignment */
LWIP_DECLARE_MEMORY_ALIGNED(ram_heap, MEM_SIZE_ALIGNED + (2U*SIZEOF_STRUCT_MEM));
#define LWIP_RAM_HEAP_POINTER ram_heap
#endif /* LWIP_RAM_HEAP_POINTER */

MIN_SIZE:表示内存块最小管理的内存空间不能小于此值

MIN_SIZE_ALIGNED:表示MIN_SIZE四字节对齐的值

SIZEOF_STRUCT_MEM:表示结构体struct mem大小对齐后的值

MEM_SIZE:定义了内存堆最大可以分配的大小

MEM_SIZE_ALIGNED:表示MEM_SIZE四字节对齐后的值

LWIP_RAM_HEAP_POINTER:用户可以定义内存堆的首地址;如果未定义,是使用LwIP定义的ram_heap数组作为内存堆,数组大小为MEM_SIZE_ALIGNED + (2U*SIZEOF_STRUCT_MEM);

这边的这个数组大小是有讲究的,再次看下内存堆示意图,其中一个SIZEOF_STRUCT_MEM是sstruct mem X需要的空间,另外一个SIZEOF_STRUCT_MEM作为内存首地址对齐需求而预留的。

内存堆的初始化

从内存堆中分配内存之前必须先对内存堆进行初始化

/**
 * Zero the heap and initialize start, end and lowest-free
 */
void
mem_init(void)
{
  struct mem *mem;

  LWIP_ASSERT("Sanity check alignment",
    (SIZEOF_STRUCT_MEM & (MEM_ALIGNMENT-1)) == 0);     //①

  /* align the heap */
  ram = (u8_t *)LWIP_MEM_ALIGN(LWIP_RAM_HEAP_POINTER);    //②
  /* initialize the start of the heap */
  mem = (struct mem *)(void *)ram;
  mem->next = MEM_SIZE_ALIGNED;
  mem->prev = 0;
  mem->used = 0;                                        //③
  /* initialize the end of the heap */
  ram_end = (struct mem *)(void *)&ram[MEM_SIZE_ALIGNED];
  ram_end->used = 1;
  ram_end->next = MEM_SIZE_ALIGNED;
  ram_end->prev = MEM_SIZE_ALIGNED;                        //④

  /* initialize the lowest-free pointer to the start of the heap */
  lfree = (struct mem *)(void *)ram;                    //⑤

  MEM_STATS_AVAIL(avail, MEM_SIZE_ALIGNED);                //⑥

  if (sys_mutex_new(&mem_mutex) != ERR_OK) {                //⑦
    LWIP_ASSERT("failed to create mem_mutex", 0);
  }
}

mem_init负责内存堆的初始化。

①检查SIZEOF_STRUCT_MEM的值是否4字节对齐

②将全局ram指针设置为对齐后的内存堆的内存首地址,注意这边通过宏LWIP_MEM_ALIGN对LWIP_RAM_HEAP_POINTER地址对齐,因为LWIP_RAM_HEAP_POINTER有可能是开发者定义的值,未必是对齐的地址值;

③从内存堆的首地址处切割出一个新的内存块A,将第一个内存块A进行各个字段初始化,其next的值设置为MEM_SIZE_ALIGNED(就是设置为struct mem X在数组中的位置),prev设置为0,说明struct mem A是第一个内存管理块,并且used设置为0,说明是未使用的内存块区域;也就是说struct mem A管理的内存空间有MEM_SIZE_ALIGNED - SIZEOF_STRUCT_MEM字节大小。

④将ram_end指向数组ram[MEM_SIZE_ALIGNED]的地址,也就是struct mem X的地址,并且其prev/next的值是自身在数组中的相对位置,表明其是内存堆中的最后一个位置(used设置为1)

⑤lfee指向第一个内存块的位置,也就是struct mem A

⑥LwIP中对各个模块都有特定的debug字段,此处avail字段记录的内存堆中的可用内存

⑦初始化互斥锁

初始化后的内存堆如下图所示

初始化后的内存堆

注意观察上图中各个字段的连线关系,也就是说初始状态下,只有一个空闲内存块,其管理的内存大小为MEM_SIZE_ALIGNED - SIZEOF_STRUCT_MEM。


内存分配函数

/**
 * Allocate a block of memory with a minimum of 'size' bytes.
 *
 * @param size is the minimum size of the requested block in bytes.
 * @return pointer to allocated memory or NULL if no free memory was found.
 *
 * Note that the returned value will always be aligned (as defined by MEM_ALIGNMENT).
 */
void *
mem_malloc(mem_size_t size)
{
  mem_size_t ptr, ptr2;
  struct mem *mem, *mem2;
#if LWIP_ALLOW_MEM_FREE_FROM_OTHER_CONTEXT
  u8_t local_mem_free_count = 0;
#endif /* LWIP_ALLOW_MEM_FREE_FROM_OTHER_CONTEXT */
  LWIP_MEM_ALLOC_DECL_PROTECT();

  if (size == 0) {
    return NULL;
  }

  /* Expand the size of the allocated memory region so that we can
     adjust for alignment. */
  size = LWIP_MEM_ALIGN_SIZE(size);

  if (size < MIN_SIZE_ALIGNED) {
    /* every data block must be at least MIN_SIZE_ALIGNED long */
    size = MIN_SIZE_ALIGNED;
  }                                                    //①

  if (size > MEM_SIZE_ALIGNED) {                           //②
    return NULL;
  }

  /* protect the heap from concurrent access */
  sys_mutex_lock(&mem_mutex);                                //③
  LWIP_MEM_ALLOC_PROTECT();
#if LWIP_ALLOW_MEM_FREE_FROM_OTHER_CONTEXT
  /* run as long as a mem_free disturbed mem_malloc or mem_trim */
  do {
    local_mem_free_count = 0;
#endif /* LWIP_ALLOW_MEM_FREE_FROM_OTHER_CONTEXT */

    /* Scan through the heap searching for a free block that is big enough,
     * beginning with the lowest free block.
     */
    for (ptr = (mem_size_t)((u8_t *)lfree - ram); ptr < MEM_SIZE_ALIGNED - size;
         ptr = ((struct mem *)(void *)&ram[ptr])->next) {    //④
      mem = (struct mem *)(void *)&ram[ptr];
#if LWIP_ALLOW_MEM_FREE_FROM_OTHER_CONTEXT
      mem_free_count = 0;
      LWIP_MEM_ALLOC_UNPROTECT();
      /* allow mem_free or mem_trim to run */
      LWIP_MEM_ALLOC_PROTECT();
      if (mem_free_count != 0) {
        /* If mem_free or mem_trim have run, we have to restart since they
           could have altered our current struct mem. */
        local_mem_free_count = 1;
        break;
      }
#endif /* LWIP_ALLOW_MEM_FREE_FROM_OTHER_CONTEXT */

      if ((!mem->used) &&
          (mem->next - (ptr + SIZEOF_STRUCT_MEM)) >= size) {    //⑤
        /* mem is not used and at least perfect fit is possible:
         * mem->next - (ptr + SIZEOF_STRUCT_MEM) gives us the 'user data size' of mem */

        if (mem->next - (ptr + SIZEOF_STRUCT_MEM) >= (size + SIZEOF_STRUCT_MEM + MIN_SIZE_ALIGNED)) {    //⑥
          /* (in addition to the above, we test if another struct mem (SIZEOF_STRUCT_MEM) containing
           * at least MIN_SIZE_ALIGNED of data also fits in the 'user data space' of 'mem')
           * -> split large block, create empty remainder,
           * remainder must be large enough to contain MIN_SIZE_ALIGNED data: if
           * mem->next - (ptr + (2*SIZEOF_STRUCT_MEM)) == size,
           * struct mem would fit in but no data between mem2 and mem2->next
           * @todo we could leave out MIN_SIZE_ALIGNED. We would create an empty
           *       region that couldn't hold data, but when mem->next gets freed,
           *       the 2 regions would be combined, resulting in more free memory
           */
          ptr2 = ptr + SIZEOF_STRUCT_MEM + size;            //⑦
          /* create mem2 struct */
          mem2 = (struct mem *)(void *)&ram[ptr2];
          mem2->used = 0;
          mem2->next = mem->next;
          mem2->prev = ptr;
          /* and insert it between mem and mem->next */
          mem->next = ptr2;
          mem->used = 1;

          if (mem2->next != MEM_SIZE_ALIGNED) {
            ((struct mem *)(void *)&ram[mem2->next])->prev = ptr2;
          }
          MEM_STATS_INC_USED(used, (size + SIZEOF_STRUCT_MEM));
        } else {
          /* (a mem2 struct does no fit into the user data space of mem and mem->next will always
           * be used at this point: if not we have 2 unused structs in a row, plug_holes should have
           * take care of this).
           * -> near fit or exact fit: do not split, no mem2 creation
           * also can't move mem->next directly behind mem, since mem->next
           * will always be used at this point!
           */
          mem->used = 1;
          MEM_STATS_INC_USED(used, mem->next - (mem_size_t)((u8_t *)mem - ram));
        }
#if LWIP_ALLOW_MEM_FREE_FROM_OTHER_CONTEXT
mem_malloc_adjust_lfree:
#endif /* LWIP_ALLOW_MEM_FREE_FROM_OTHER_CONTEXT */
        if (mem == lfree) {        //⑦
          struct mem *cur = lfree;
          /* Find next free block after mem and update lowest free pointer */
          while (cur->used && cur != ram_end) {
#if LWIP_ALLOW_MEM_FREE_FROM_OTHER_CONTEXT
            mem_free_count = 0;
            LWIP_MEM_ALLOC_UNPROTECT();
            /* prevent high interrupt latency... */
            LWIP_MEM_ALLOC_PROTECT();
            if (mem_free_count != 0) {
              /* If mem_free or mem_trim have run, we have to restart since they
                 could have altered our current struct mem or lfree. */
              goto mem_malloc_adjust_lfree;
            }
#endif /* LWIP_ALLOW_MEM_FREE_FROM_OTHER_CONTEXT */
            cur = (struct mem *)(void *)&ram[cur->next];
          }
          lfree = cur;
          LWIP_ASSERT("mem_malloc: !lfree->used", ((lfree == ram_end) || (!lfree->used)));
        }
        LWIP_MEM_ALLOC_UNPROTECT();
        sys_mutex_unlock(&mem_mutex);
        LWIP_ASSERT("mem_malloc: allocated memory not above ram_end.",
         (mem_ptr_t)mem + SIZEOF_STRUCT_MEM + size <= (mem_ptr_t)ram_end);
        LWIP_ASSERT("mem_malloc: allocated memory properly aligned.",
         ((mem_ptr_t)mem + SIZEOF_STRUCT_MEM) % MEM_ALIGNMENT == 0);
        LWIP_ASSERT("mem_malloc: sanity check alignment",
          (((mem_ptr_t)mem) & (MEM_ALIGNMENT-1)) == 0);

        return (u8_t *)mem + SIZEOF_STRUCT_MEM;        //⑧
      }
    }
#if LWIP_ALLOW_MEM_FREE_FROM_OTHER_CONTEXT
    /* if we got interrupted by a mem_free, try again */
  } while (local_mem_free_count != 0);
#endif /* LWIP_ALLOW_MEM_FREE_FROM_OTHER_CONTEXT */
  LWIP_DEBUGF(MEM_DEBUG | LWIP_DBG_LEVEL_SERIOUS, ("mem_malloc: could not allocate %"S16_F" bytes\n", (s16_t)size));
  MEM_STATS_INC(err);
  LWIP_MEM_ALLOC_UNPROTECT();
  sys_mutex_unlock(&mem_mutex);
  return NULL;
}

①首先对输入的size参数进行对齐并且对其大小进行限定,如果小于MIN_SIZE_ALIGNED,则设置为size为MIN_SIZE_ALIGNED(避免过多的小内存块)

②如果需要分配的内存大于MEM_SIZE_ALIGNED(最大可分配的内存),则直接返回NULL

③内存分配之前先上锁,防止多线程访问问题

④从lfree位置开始搜索未使用的内存块

for (ptr = (mem_size_t)((u8_t *)lfree - ram); ptr < MEM_SIZE_ALIGNED - size;
         ptr = ((struct mem *)(void *)&ram[ptr])->next) {
}

注意这边的ptr是索引号(即数组下标),lfree是指针,ram是内存堆的首地址,lfree - ram获取的是内存堆的空闲块在数组中的索引值;

⑤查找空闲的内存块并且其大小要满足

if ((!mem->used) &&
          (mem->next - (ptr + SIZEOF_STRUCT_MEM)) >= size) {

因为将一个内存块切割一个新的内存块出来,除了需要的内存size外,还要一个struct mem结构体用于管理剩余的块,所以这边判断条件中需要加入SIZEOF_STRUCT_MEM

⑥如果找到的空闲块足够大,分配需要的内存后还比最小内存块(SIZEOF_STRUCT_MEM + MIN_SIZE_ALIGNED)还要大,就需要进行分割;否则直接将找到的空闲块设置为使用的,并将这边内存空间返回

if (mem->next - (ptr + SIZEOF_STRUCT_MEM) >= (size + SIZEOF_STRUCT_MEM + MIN_SIZE_ALIGNED)) {
}

⑦ptr2为切割后剩余内存块的数组索引号

mem2 = (struct mem *)(void *)&ram[ptr2];

通过下标ptr2获取内存地址,mem2就是新分割后的内存块的首地址

mem2->used = 0;
mem2->next = mem->next;
mem2->prev = ptr;
/* and insert it between mem and mem->next */
mem->next = ptr2;
mem->used = 1;

将mem2的next的值设置为mem->next;mem2->prev设置为ptr,mem->next设置为ptr2,并标记为使用的。

⑦更新lfree指针

⑧返回分配的内存指针(注意返回的地址需要便宜内存管理块的结构体大小(u8_t *)mem + SIZEOF_STRUCT_MEM;)

为了帮助大家理解这个分割过程,大家可以根据下图进行理解

上图中struct mem A是已经分配的内存,struct mem B是新切割出来的内存块,注意每个内存管理块的prev/next的赋值

另外我们从上面的分割算法来看,其搜索可用内存块的算法就是遍历,做到足够大的就行,但未必是最匹配的,所以在切割完后特别容易产生较多的小内存块。


内存释放函数

/**
 * Put a struct mem back on the heap
 *
 * @param rmem is the data portion of a struct mem as returned by a previous
 *             call to mem_malloc()
 */
void
mem_free(void *rmem)
{
  struct mem *mem;
  LWIP_MEM_FREE_DECL_PROTECT();

  if (rmem == NULL) {                //①
    LWIP_DEBUGF(MEM_DEBUG | LWIP_DBG_TRACE | LWIP_DBG_LEVEL_SERIOUS, ("mem_free(p == NULL) was called.\n"));
    return;
  }
  LWIP_ASSERT("mem_free: sanity check alignment", (((mem_ptr_t)rmem) & (MEM_ALIGNMENT-1)) == 0);

  LWIP_ASSERT("mem_free: legal memory", (u8_t *)rmem >= (u8_t *)ram &&
    (u8_t *)rmem < (u8_t *)ram_end);

  if ((u8_t *)rmem < (u8_t *)ram || (u8_t *)rmem >= (u8_t *)ram_end) { //②
    SYS_ARCH_DECL_PROTECT(lev);
    LWIP_DEBUGF(MEM_DEBUG | LWIP_DBG_LEVEL_SEVERE, ("mem_free: illegal memory\n"));
    /* protect mem stats from concurrent access */
    SYS_ARCH_PROTECT(lev);
    MEM_STATS_INC(illegal);
    SYS_ARCH_UNPROTECT(lev);
    return;
  }
  /* protect the heap from concurrent access */
  LWIP_MEM_FREE_PROTECT();
  /* Get the corresponding struct mem ... */
  /* cast through void* to get rid of alignment warnings */
  mem = (struct mem *)(void *)((u8_t *)rmem - SIZEOF_STRUCT_MEM);   //③
  /* ... which has to be in a used state ... */
  LWIP_ASSERT("mem_free: mem->used", mem->used);
  /* ... and is now unused. */
  mem->used = 0;

  if (mem < lfree) {        //④
    /* the newly freed struct is now the lowest */
    lfree = mem;
  }

  MEM_STATS_DEC_USED(used, mem->next - (mem_size_t)(((u8_t *)mem - ram)));

  /* finally, see if prev or next are free also */
  plug_holes(mem);                                            //⑤
#if LWIP_ALLOW_MEM_FREE_FROM_OTHER_CONTEXT
  mem_free_count = 1;
#endif /* LWIP_ALLOW_MEM_FREE_FROM_OTHER_CONTEXT */
  LWIP_MEM_FREE_UNPROTECT();
}

①②检查rmem指针的合法性

③根据rmem指针找到管理此内存块的struct mem的地址,并标记此内存块为未使用的

④如果当前未使用的内存块的地址比空闲块的指针还小,则将lfree指向这个新释放的内存块

⑤拼接当前内存块前后的空闲内存块(动态内存堆中容易出现内存碎片,故在释放时需要检查当前块前一个和后一个内存块是否是空闲块,如果是则进行拼接)

/**
 * "Plug holes" by combining adjacent empty struct mems.
 * After this function is through, there should not exist
 * one empty struct mem pointing to another empty struct mem.
 *
 * @param mem this points to a struct mem which just has been freed
 * @internal this function is only called by mem_free() and mem_trim()
 *
 * This assumes access to the heap is protected by the calling function
 * already.
 */
static void
plug_holes(struct mem *mem)
{
  struct mem *nmem;
  struct mem *pmem;

  LWIP_ASSERT("plug_holes: mem >= ram", (u8_t *)mem >= ram);        //①
  LWIP_ASSERT("plug_holes: mem < ram_end", (u8_t *)mem < (u8_t *)ram_end);
  LWIP_ASSERT("plug_holes: mem->used == 0", mem->used == 0);

  /* plug hole forward */
  LWIP_ASSERT("plug_holes: mem->next <= MEM_SIZE_ALIGNED", mem->next <= MEM_SIZE_ALIGNED);

  nmem = (struct mem *)(void *)&ram[mem->next];                        //②-1
  if (mem != nmem && nmem->used == 0 && (u8_t *)nmem != (u8_t *)ram_end) {
    /* if mem->next is unused and not end of ram, combine mem and mem->next */
    if (lfree == nmem) {                                            //②-2
      lfree = mem;
    }
    mem->next = nmem->next;
    ((struct mem *)(void *)&ram[nmem->next])->prev = (mem_size_t)((u8_t *)mem - ram);
  }

  /* plug hole backward */
  pmem = (struct mem *)(void *)&ram[mem->prev];            //③-1
  if (pmem != mem && pmem->used == 0) {                    //③-2
    /* if mem->prev is unused, combine mem and mem->prev */
    if (lfree == mem) {            //③-3
      lfree = pmem;
    }
    pmem->next = mem->next;                        //③-4
    ((struct mem *)(void *)&ram[mem->next])->prev = (mem_size_t)((u8_t *)pmem - ram);
  }
}

①检查mem地址的合法性

②-1检查当前块的后一个块是否是空闲块

②-2如果当前块的后一个块是空闲块则将当前的next设置为下一个块的next,下一个块的prev设置为当前块的索引值;如果lfree指向当前块的下一个块,则lfree需要指向当前块

③-1检查当前块的上一个块

③-2如果当前块的上一个块未使用,则进行拼接

③-3如果lfree指向当前块,则lfree需要指向当前块的上一个块

③-4设置上一个块的next的值为当前块的next,当前块的下一个块的prev设置为当前块的上一个块的下标值

至此,我们基本上对LwIP的内存堆实现算法进行了分析,总体上看还是比较简单的。

其每个内存统一管理,在寻找一个新的空闲块时,需要编译整个内存堆中的所有块,但为了尽量加快搜索速度,专门有个lfree指针定位当前空闲块中地址最低的一个块。

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

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

相关文章

Ubuntu22.10安装和配置R/FSL/Freesurfer

2018年购入的台式机&#xff0c;一直处于吃灰状态&#xff0c;决定安装Ubuntu方便学习和使用一些基于Linux系统的软件。本文记录相关软件的安装和配置。>安装Ubuntu22.10<到官网下载ISO文件。将一个闲置U盘通过Rufus制作为安装盘。重启按F11设置引导盘&#xff0c;选择U盘…

国家实用新型发明专利:一种机器视觉的流水线智能检测报警系统

国家实用新型发明专利&#xff1a;一种机器视觉的流水线智能检测报警系统 【系统装置设计图】 文章目录国家实用新型发明专利&#xff1a;一种机器视觉的流水线智能检测报警系统【说明书摘要】【权利要求书】【说明书】***技术领域******背景技术******发明内容******有益效果…

C++类和对象(上): 封装与this指针

目录 一.前言 二. 类的引入和定义 1.C和C结构体的区别 2.C类的定义 3.类的成员方法的声明和定义是可分离的 三.面向对象之封装特性 1.封装思想的介绍 2.类封装编程模式的优点 四. 类实例(对象)的内存模型 五.this指针 章节导图&#xff1a; 一.前言 面向过程和面向对…

分享167个PHP源码,总有一款适合您

PHP源码 分享167个PHP源码&#xff0c;总有一款适合您 下面是文件的名字&#xff0c;我放了一些图片&#xff0c;文章里不是所有的图主要是放不下...&#xff0c; 167个PHP源码下载链接&#xff1a;https://pan.baidu.com/s/1fzoQ4_4VXc1e1ZHOUKuhbQ?pwdsb6s 提取码&#x…

数学表达式的处理

概述 在OJ上 会遇到一些这样的题目&#xff1a; 小明同学写数学四则运算&#xff0c;有把括号写多、写少、写错的情况&#xff0c;比如&#xff08;AB)*(C-D &#xff0c;请你输入一个表达式&#xff0c;判断此表达式的括号是否正确(不考虑运算的结果正确性)。 每次我看到 &q…

【操作系统】—— Windows压缩工具 “ Bandizip与7-zip ”(带你快速了解)

&#x1f4dc; “作者 久绊A” 专注记录自己所整理的Java、web、sql等&#xff0c;IT技术干货、学习经验、面试资料、刷题记录&#xff0c;以及遇到的问题和解决方案&#xff0c;记录自己成长的点滴。 &#x1f341; 操作系统【带你快速了解】对于电脑来说&#xff0c;如果说…

Python OpenCV 图片滑块验证码 滑块图片验证码 自动识别方案 模板匹配识别 识别成功率调试 源码分析 通用解决方案

前言 通过本专栏前面两篇文章大家已对图片滑块验证码有了初步的了解,对于滑块验证的实现和校验原理有了一定的了解,通过由浅入深的实战案例可直接应用于实战,对于滑块如何在前端实现滑动或接口调用可自行查阅相关资料实现,本文主要讲解 滑块验证码 模板匹配 识别的通用解决…

史上最详细的AVL树的实现(万字+动图讲解旋转)

&#x1f525;&#x1f525; 欢迎来到小林的博客&#xff01;&#xff01;       &#x1f6f0;️博客主页&#xff1a;✈️小林爱敲代码       &#x1f6f0;️文章专栏&#xff1a;✈️小林的C之路       &#x1f6f0;️欢迎关注&#xff1a;&#x1f44d…

[Python从零到壹] 六十三.图像识别及经典案例篇之图像漫水填充分割应用

祝大家新年快乐&#xff0c;阖家幸福&#xff0c;健康快乐&#xff01; 欢迎大家来到“Python从零到壹”&#xff0c;在这里我将分享约200篇Python系列文章&#xff0c;带大家一起去学习和玩耍&#xff0c;看看Python这个有趣的世界。所有文章都将结合案例、代码和作者的经验讲…

创建者模式-原型模式

1.概述 用一个已经创建的实例作为原型&#xff0c;通过复制该原型对象来创建一个和原型对象相同的新对象 2.结构 原型模式包含如下角色&#xff1a; 抽象原型类&#xff1a;规定了具体原型对象必须实现的的 clone() 方法。具体原型类&#xff1a;实现抽象原型类的 clone() …

Spring AOP与Spring 事务

一、AOP讲解 创建普通对象UserService Component public class UserService{Autowiredprivate OrderService orderService;public void test(){System.out.println(orderService);}}创建代理对象UserServiceProxy&#xff0c;对test&#xff08;&#xff09;方法进行切面编程…

SSM项目 - 博客系统

1.SSM 版本的博客系统相较于 Servlet 版本的升级1. 框架升级 : SSM (SpringBoot Spring MVC MyBatis) MySQL Redis jQuery.2. 密码升级: 明文存储/md5存储 -> 加盐处理.3. 用户登录状态持久化升级: session 持久化到内存 - > session 持久化到 Redis. (后期有空实现…

cmake 03 一个可用的 cmake 工程应当是什么样的

cmake 学习笔记 代码地址: https://gitcode.net/u014254963/cmake-study/-/tree/master/hello_cmake_project https://gitcode.net/u014254963/cmake-study/-/tree/master/hello_cmake_project_vs 本文目标 多目录构建引用自己写的动态库关于单元测试的一些实践使用 python 脚…

Pandas-DataFrame基础知识点总结

1、DataFrame的创建 DataFrame是一种表格型数据结构&#xff0c;它含有一组有序的列&#xff0c;每列可以是不同的值。DataFrame既有行索引&#xff0c;也有列索引&#xff0c;它可以看作是由Series组成的字典&#xff0c;不过这些Series公用一个索引。 DataFrame的创建有多种…

JavaEE-多线程初阶4

✏️作者&#xff1a;银河罐头 &#x1f4cb;系列专栏&#xff1a;JavaEE &#x1f332;“种一棵树最好的时间是十年前&#xff0c;其次是现在” 目录多线程案例阻塞队列阻塞队列是什么生产者消费者模型标准库中的阻塞队列阻塞队列实现定时器定时器是什么标准库中的定时器实现定…

(第107篇)C规范编辑笔记(十三)

往期文章&#xff1a; C规范编辑笔记(一) C规范编辑笔记(二) C规范编辑笔记(三) C规范编辑笔记(四) C规范编辑笔记(五) C规范编辑笔记(六) C规范编辑笔记(七) C规范编辑笔记(八) C规范编辑笔记(九) C规则编辑笔记(十) C规范编辑笔记(十一) C规范编辑笔记(十二) 正文&#xff…

行人属性识别研究综述(二)

文章目录6 PAR&#xff08;行人属性识别&#xff09;算法综述6.1全局基于图像的模型6.1.1 ACN (iccvw-2015)6.1.2 DeepSAR and DeepMAR (ACPR-2015) [6]6.1.3 MTCNN (TMM-2015) [7]6.2 基于部件的模型6.2.1 Poselets (ICCV-2011)6.2.2 rad (iccv-2013)6.2.3 PANDA (cvp -2014) …

Java-IO知识详解(一)

分类分类&#xff08;传输&#xff0c;操作&#xff09;IO理解分类 - 从传输方式上字节流字符流字节流和字符流的区别IO理解分类 - 从数据操作上文件(file)数组([])管道操作基本数据类型缓冲操作打印对象序列化反序列化转换装饰者模式分类&#xff08;传输&#xff0c;操作&…

迈百瑞冲刺创业板上市:关联收入占比较高,房健民为加拿大籍

撰稿|汤汤 来源|贝多财经 近日&#xff0c;烟台迈百瑞国际生物医药股份有限公司&#xff08;下称”迈百瑞“&#xff09;在深圳证券交易所提交更新后的招股书&#xff08;申报稿&#xff09;。据贝多财经了解&#xff0c;迈百瑞于2022年9月在递交IPO申请材料&#xff0c;准备…

指定不同版本的pcl

18.04里面安装了两个版本的pcl&#xff0c;一个是安装ros的时候安装的pcl1.8&#xff0c;另一个是安装的源码pcl1.12版本。一直相安无事&#xff0c;今天在我编译lego-loam的时候&#xff0c;突然就冲突了。卡了我两个小时&#xff0c;到处找原因&#xff0c;网上基本上没有相似…