【STM32】TCP/IP通信协议(2)--LwIP内存管理

news2024/11/27 11:41:17

五、LWIP内存管理

1.什么是内存管理?

(1)内存管理,是指软件运行时对计算机内存资源的分配的使用的技术,其主要目的是如何高效、快速的分配,并且在适当的时候释放和回收内存资源(就比如C语言当中的malloc 、free分配和释放)

内存分配:大数组,完成之后返回内存地址

内存释放:传入内存地址让算法进行释放

(2)LWIP内存管理策略

1.内存堆:提供合适大小的内存,剩余内存返回堆中

2.内存池:只能申请固定大小的内存,能有效防止内存碎片

3.C库: C运行时库自带的内存分配策略(不建议使用)

lwip内存池和内存堆本质上直接操作数组实现

(3)lwip内存堆和内存池的应用

  • 接收数据:MAC内核数组【内存堆和内存池可适用,正点原子阿波罗版本的使用的是内存池】

  • 发送数据:用户调用lwip的API接口【lwip一般选用内存堆申请内存】

  • 用户调用:可调用lwip的内存池和内存堆API接口申请内存

  • 接口控制块:netconn、socket、raw接口

  • 构建消息:API消息、数据包消息

2.lwip内存堆简介

lwip内存堆是一种可变长的分配策略,可以随意申请任意大小的内存,lwip内存堆采用的是First Fit(首次拟合)内存算法

(1)First Fit算法

从低地址空间往高地址空间查找,从中切割成合适的块,并把剩余的部分返回到动态内存堆中。

优点:

  • 内存浪费小、比较简单、适合小内存管理

  • 确保高地址空间具有足够的内存

  • 要求分配最小值以及相邻空间块合并,有效防止内存碎片

缺点:

  • 分配与释放频繁,会造成内存碎片

  • 分配和释放时,从低地址开始寻找,会导致效率慢

3.lwip内存堆原理解析

1

通过开辟一个内存堆,使用模拟IC运行时库的内存分配策略实现【大数组】

(1)管理内存块的结构体:

如下源码所示:

struct mem {
  mem_size_t next;      //指向下一个节点索引
  mem_size_t prev;      //指向上一个节点索引
  u8_t used;            //描述内存块是否可用    0:未使用   1:已使用
};
(2)最小内存分配
#ifndef MIN_SIZE
#define MIN_SIZE             12//最小分配内存
#endif /* MIN_SIZE */

为了防止内存碎片,lwip内核定义了最小分配大小MIN_SIZE。当用户申请的内存小于最小分配内存时,系统将分配MIN_SIZE大小的内存,

(3)对齐定义
//对齐操作
#define MIN_SIZE_ALIGNED     LWIP_MEM_ALIGN_SIZE(MIN_SIZE)//最小分配内存大小对齐---12字节
#define SIZEOF_STRUCT_MEM    LWIP_MEM_ALIGN_SIZE(sizeof(struct mem))//内存控制块对齐---8字节
#define MEM_SIZE_ALIGNED     LWIP_MEM_ALIGN_SIZE(MEM_SIZE)//内存堆对齐

SIZEOF_STRUCT_MEM宏定义用于确保内存大小进行4字节对齐,这不仅可以大大提升CPU的访问速度还因为某些硬件平台只能从特定地址处获取特定类型的数据。

(4)定义内存堆的空间
#ifndef LWIP_RAM_HEAP_POINTER 
​
/*定义堆内存空间*/
​
LWIP_DECLARE_MEMORY_ALIGNED(ram_heap, MEM_SIZE_ALIGNED + (2U*SIZEOF_STRUCT_MEM)); 
​
#define LWIP_RAM_HEAP_POINTER ram_heap 
​
#endif

无论是内存池还是内存堆都是对一个大数组进行操作。这种大数组被称为ram_heap数组,其大小常被定义为 MEM_SIZE_ALIGNED + (2U*SIZEOF_STR UCT_MEM)。这个能让用户申请内存时在这个大数组中分配相应大小的内存块,减少内存碎片。

(5)操作内存堆变量
/* 指向对齐后的内存堆的地址*/
static u8_t *ram;
/* 指向对齐后的内存堆的最后一个内存块 */
static struct mem *ram_end; 
/* 指向已被释放的索引号最小的内存块(内存堆最前面的已被释放的)*/
static struct mem * LWIP_MEM_LFREE_VOLATILE lfree;
​

lwIP 内核使用三个指针:ram 指针、ram_end 指针和 lfree 指针。

ram 指针指向ram_heap数组对齐后的内存堆总空间首地址

ram_end 指针指向ram_heap数组内存堆总空间尾地址(接近总空间的尾 地址)

lfree 指针指向ram_heap数组最低内存地址的空闲内存块。

在内存分配当中lwip根据lfree指针指向的空闲内存进行分配,从而快速找到可用的内存块,并有效分配内存给需要的任务。ram_end指针用于检测总内存堆空间中是否存在空闲的内存,以便进行进一步的内存分配。

(6)内存初始化mem_init():

下图是对堆空间进行初始化,初始化后的lfree指针指向第一个内存块,该内存块由控制块可用内存所组成,ram_end指针指向堆空间的尾部,用于判断堆空间是否存在可用内存,若lfree指针指向ram_end指针,则表示该堆空间没有可用内存进行分配

控制块:标记内存是否可用

可用内存:实际可分配的内存区域

void mem_init(void)
{
  struct mem *mem;
​
  LWIP_ASSERT("Sanity check alignment",
    (SIZEOF_STRUCT_MEM & (MEM_ALIGNMENT-1)) == 0);
​
  /* 对内存堆的地址(全局变量的名)进行对齐指向 ram_heap*/
  ram = (u8_t *)LWIP_MEM_ALIGN(LWIP_RAM_HEAP_POINTER);
  /* 建立第一个内存块,内存块由内存块头+空间组成。 */
  mem = (struct mem *)(void *)ram;//附加在每个内存块前面的结构体
  /* 下一个内存块不存在,因此指向内存堆的结束 */
  mem->next = MEM_SIZE_ALIGNED;
  /* 前一个内存块就是它自己,因为这是第一个内存块 */ 
  mem->prev = 0;
  /* 第一个内存块没有被使用 */ 
  mem->used = 0;
  /* 初始化堆的末端(指向 MEM_SIZE_ALIGNED 底部位置)*/
  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;
​
  /* 已释放的索引最小的内存块就是上面建立的第一个内存块。*/
  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);
  }
}

在内存分配过程中,lfree指针从低地址开始不短查找和划分内存,直到ram_end指针所指向的地址,这意味着lfree指针从堆空间的起始位置开始,逐个遍历内存块,直到找到可用的内存或到达堆空间的末尾才能结束分配。

(7)mem_malloc ():
void *mem_malloc(mem_size_t size_in){
 mem_size_t ptr, ptr2, size;
 struct mem *mem, *mem2;
 /*******第一:检测用户申请的内存块释放满足 LWIP 的规则*******/
 /*******第二:从内存堆中划分用户的内存块******/
 /* 寻找足够大的空闲块,从最低的空闲块开始.*/
 for (ptr = mem_to_ptr(lfree); ptr < MEM_SIZE_ALIGNED - size;ptr = ((struct mem *)(void*)&ram[ptr])->next)
 {
    mem = ptr_to_mem(ptr); /* 取它的地址 */ 
    /* 空间大小必须排除内存块头大小 */
    if ((!mem->used) &&
    (mem->next - (ptr + SIZEOF_STRUCT_MEM)) >= size) {
     /* 这个地方需要判断 剩余的内存块是否可以申请 size 内存块 */
     if (mem->next - (ptr + SIZEOF_STRUCT_MEM) >= (size +SIZEOF_STRUCT_MEM + MIN_SIZE_ALIGNED)) 
     {
            /* 上面注释一大堆,主要就是说,剩余内存可能连一个内存块的头都放不下了,这个时候就没法新建空内存块。其索引也就不能移动 */
            /* 指向申请后的位置,即:建立下一个未使用的内存块的头部。即:插入一个新空内存块 */
        ptr2 = (mem_size_t)(ptr + SIZEOF_STRUCT_MEM + size);
            /*从 Ptr2 地址开始创建 mem2 的结构体 */
         mem2 = ptr_to_mem (ptr2);/* 调用(struct mem *)(void *)&ram[ptr]; */
         mem2->used = 0;
         /* 这个根据下面的 if(mem2->next != MEM_SIZE_ALIGNED)判定 */
        mem2->next = mem->next;
        mem2->prev = ptr; /* 空闲内存块的前一个指向上面分配的内存块 */
        /* 前一个内存块指向上面建立的空闲内存块 */
         mem->next = ptr2;
         mem->used = 1;/* 将当前分配的内存块标记为 已使用 */
         /* 如果 mem2 内存块的下一个内存块不是链表中最后一个内存块 (结束地址),那就将它下一个的内存块的 prve 指向 mem2 */
        if (mem2->next != MEM_SIZE_ALIGNED) 
        {
             ((struct mem *)(void *)&ram[mem2->next])->prev = ptr2;
        }
     }
     else {
         /* 内存块太小了会产生的碎片 */
         mem->used = 1;
         }
 /* 这里处理:当分配出去的内存正好是 lfree 时,因为该内存块已经被分配出去了, 必须修改 lfree 的指向下一个最其前面的已释放的内存块*/
    if (mem == lfree) 
    {
        struct mem *cur = lfree;
        /* 只要内存块已使用且没到结尾,则继续往后找 */
        while (cur->used && cur != ram_end) 
        {
            cur = ptr_to_mem(cur->next);/* 下一个内存块 */
        }
        /* 指向找到的 第一个已释放的内存块。如果上面没有找到,则 lfree = lfree 不变 */
         lfree = cur;
     }
     /* 这里返回 内存块的空间的地址,排除内存块的头 */
    return (u8_t *)mem + SIZEOF_STRUCT_MEM + MEM_SANITY_OFFSET; 
         }
     }
 return NULL;
    }
}

4.LWIP内存池简介

(1)什么是内存池

lwip内存池是把连续的内存池分成多个大小相同的内存空间,并通过单链表的方式连接起来。当用户申请内存时,系统会从单链表的头 部取出一个内存块进行分配,释放内存时只需将内存块放回链表的头部。

lwip内存池优点:

分配速度快,防止内存碎片,回收便捷

lwip内存池缺点:

资源浪费,申请大型内存时,可能申请失败

lwip内存池应用场景:

在 lwIP 中,存在多种固定大小的数据结构,这些数据结构的特点是预先知道其大小并且在整个生命周期中保持不变。比如说,在建立 TCP 连接时,需要使用 TCP 控制块这种数据结构,其大小是固定的。为了满足这些数据结构的内存分配需求,lwIP 在内存初始化时创建了动态内存池 POOL预先分配一定数量的内存块。这种内存管理方式有助于提高内存分配的效率和性能。

(2)实现LWIP内存池的文件

对于内存堆中的动态内存池,运用了很多复杂的宏定义的运用。所以我们将重点探究这四个关键文件:memp.c、memp.h、memp_std.h 和 memp_prive.h。

i.memp_priv.h 文件

定义了memp(链接内存块)memp_desc(管理链接的内存块)结构体

memp 结构体将同一类型的内存池链表的形式连接起来

memp_desc 结构体则用于管理描述各种类型的内存池, 包括数量、大小、内存池的起始地址以及指向空闲内存池的指针。

/* 管理内存块 */
struct memp {
 struct memp *next;
};
​
/* 管理和描述各类型的内存池 */
struct memp_desc {
 /** 每个内存块的大小 */
 u16_t size;
 /** 内存块的数量 */
 u16_t num;
 /** 指向内存的基地址 */
 u8_t *base;
 /** 每个池的第一个空闲元素。元素形成一个链表 */
 struct memp **tab;
};

memp结构体和memp_desc结构体的关系:

由图可知:每一个 memp_desc 结构体都是用于管理同一类型的内存池。 这些内存池,也就是内存块,是通过链表的形式相互连接起来的。

ii.memp_std.h 文件

该文件是 lwIP 内存池的核心定义,申请了所需的内存池。使用了宏定义来确定是否启用特定类型的内存池,例如 TCP、UDP、DHCP、ICMP 等协议

#if LWIP_RAW
LWIP_MEMPOOL(RAW_PCB, MEMP_NUM_RAW_PCB, sizeof(struct raw_pcb),"RAW_PCB")
#endif /* LWIP_RAW */
​
#if LWIP_UDP
LWIP_MEMPOOL(UDP_PCB, MEMP_NUM_UDP_PCB, sizeof(struct udp_pcb), "UDP_PCB")
#endif /* LWIP_UDP */
​
#if LWIP_TCP
LWIP_MEMPOOL(TCP_PCB, MEMP_NUM_TCP_PCB, sizeof(struct tcp_pcb), "TCP_PCB")
LWIP_MEMPOOL(TCP_PCB_LISTEN, MEMP_NUM_TCP_PCB_LISTEN, 
sizeof(struct tcp_pcb_listen), "TCP_PCB_LISTEN")
LWIP_MEMPOOL(TCP_SEG, MEMP_NUM_TCP_SEG, sizeof(struct tcp_seg), "TCP_SEG")
#endif /* LWIP_TCP */

通过上面代码,可发现:不同类型的内存池是通过相应的宏定义声明启用的。LWIP_MEMPOOL 这个宏定义用于初始化各种类型的内存池。

iii. memp.h 文件

文件主要定义了 memp_t 枚举类型,该类型用于获取各类内存池的数量声明了宏定义和函数以提供外部文件使用,这里定义里前面所提到了LWIP_MEMPOOL 宏定义,

typedef enum {
​
/* ##为 C 语言的连接符,例如 MEMP_##A,A = NAME ,所以等于 MEMP_NAME */
#define LWIP_MEMPOOL(name,num,size,desc) MEMP_##name,
​
#include "lwip/priv/memp_std.h"
 MEMP_MAX
 
} memp_t;
​
#include "lwip/priv/memp_priv.h" /* 该文件需要使用上面的枚举 */
#include "lwip/stats.h"

根据 memp_std.h 文件中启用的内存池类型来计算 MEMP_MAX,即各类内存池的最大数量。计算方法如下:

1,LWIP_MEMPOOL 宏定义指向 MEMP_##name(##为 C 语言中的连接符)

2,通过#include "lwip/priv/memp_std.h"文件来启用所需的内存池类型

iv.memp.c 文件

在memp.h当中,我们有提到:LWIP_MEMPOOL 指向 LWIP_MEMPOOL_DECLARE 宏定义,而在memp.c 文件当中的const memp_pools[MEMP _MAX]数组则用于管理各类内存池的描述符

#define LWIP_MEMPOOL(name,num,size,desc) \
 
LWIP_MEMPOOL_DECLARE(name,num,size,desc)
#include "lwip/priv/memp_std.h"

当 memp_std.h 文件只启用 LWIP_RAW 和 LWIP_UDP 类型的内存池时,展开后的枚举类型如下:

u8_t memp_memory_RAW_PCB_base[((((((num) * (MEMP_SIZE +
(((size) + MEM_ALIGNMENT - 1U) & ~(MEM_ALIGNMENT-1U))))) +
MEM_ALIGNMENT - 1U)))];
​
static struct memp *memp_tab_RAW_PCB;
const struct memp_desc memp_RAW_PCB= { \
 LWIP_MEM_ALIGN_SIZE(size), \
 (num), \
 memp_memory_TCPIP_MSG_API_base, \
 &memp_tab_TCPIP_MSG_API \
 };
 
u8_t memp_memory_UDP_PCB_base[((((((num) * (MEMP_SIZE +
(((size) + MEM_ALIGNMENT - 1U) & ~(MEM_ALIGNMENT-1U))))) +
MEM_ALIGNMENT - 1U)))];
​
static struct memp *memp_tab_UDP_PCB;
 
const struct memp_desc memp_UDP_PCB= { \
 LWIP_MEM_ALIGN_SIZE(size), \
 (num), \
 memp_memory_UDP_PCB_base, \
 &memp_tab_UDP_PCB \
 };

这段代码通过使用 LWIP_MEMPOOL_DECLARE 宏定义,声明了各类内存池的描述和管理信息。具体来说,它定义了memp_desc 结构体,比如 :

memp_RAW_PCB:用于描述该类型的内存池的数量、大小、分配内存地址以及指向空闲内存池的指针。

const struct memp_desc* const memp_pools[MEMP_MAX] = {
 &memp_memp_RAW_PCB,
 &memp_memp_UDP_PCB,
};

memp_pools[MEMP_RAW_PCB],它取自 memp_RAW_PCB 变 量的地址。这是我们在前面展开LWIP_MEMPOOL_DECLARE 宏定义时定义的变量。

v.memp_init 函数和 memp_init_pool 函数

该函数是内存池的初始化

void memp_init(void)
{
 u16_t i;
 /* 遍历,需要多少个内存池 */
 for (i = 0; i < LWIP_ARRAYSIZE(memp_pools); i++) {
 memp_init_pool(memp_pools[i]);
 }
}
​
void memp_init_pool(const struct memp_desc *desc)
{
 int i;
 struct memp *memp;
 *desc->tab = NULL;
 /* 内存对齐 */
 memp = (struct memp*)LWIP_MEM_ALIGN(desc->base);
 /* 将内存块链接成链表形式 */
 for (i = 0; i < desc->num; ++i) {
 memp->next = *desc->tab;
 *desc->tab = memp;
 /* 地址偏移*/ 
 memp = (struct memp *)(void *)((u8_t *)memp +
MEMP_SIZE + desc->size);
 }
}
​

每个类型的描述符都是用于管理和描述该类型的内存池。这些同一类型的内存池内部通过指向下一个节点的指针链接起来,形成一个链表。通过第二个 for 循 环语句,我们可以遍历这些同一类型的内存池,并将其以链表的形式进行链接。

memp_pool 数组包含了不同类型的内存池描述符,每个描述符负责管理同类型的内存池。这些内存池通过指针链接成一个单向链表,方便管理和访问。同一类型的内存池都 在同一个数组中分配,通过 base 指针可以找到该数组的首地址。tab 指针指向第一个空闲的内 存池,当用户申请内存池时,将从 tab 指针指向的内存池进行分配。分配完成后,tab 指针将偏移到下一个空闲内存池的地址,以便下次分配。

vi.memp_malloc 函数和 memp_malloc_pool 函数

内存池有多种类型,因此用户在申请内存池时需要**明确申请的类型lwIP 内存池的申请函数是 memp_malloc**

void *
memp_malloc(memp_t type)
{
 void *memp;
 memp = do_memp_malloc_pool(memp_pools[type]);
 return memp;
}
​
static void*
do_memp_malloc_pool(const struct memp_desc *desc)
{
     struct memp *memp;
    memp = *desc->tab;
    if (memp != NULL)
     {
        *desc->tab = memp->next;
        return ((u8_t*)memp + MEMP_SIZE);
     }
    else
    {
    }
    return NULL;
}
​

memp_malloc 函数根据用户传入的内存池类型,如 UDP_PCB 等,在 memp_pool 数组中查找对应的内存池描述符。一旦找到对应的描述符,该函数会根据描述符中的 tab 指针来分配内存给用户,并将 tab 指针偏移至下一个空闲内存池。

vii.memp_free 函数与 memp_free_pool 函数

内存池的释放函数相对简单,它需要传入两个参数:内存池的类型和要释放的内存池的地址。通过这两个参数,lwIP 内核可以确定该类型内存池描述符的位置,以及需要释放的内存池的具体位置。

void memp_free(memp_t type, void *mem)
{
    if (mem == NULL) /* 判断内存块的起始地址释放为空 */
    {
        return;
    }
    do_memp_free_pool(memp_pools[type], mem);
}
​
static void do_memp_free_pool(const struct memp_desc* desc, void *mem)
{
    struct memp *memp;
    /* 据内存块的地址偏移得到内存块的起始地址 */
     memp = (struct memp *)(void *)((u8_t*)mem - MEMP_SIZE);
    /* 内存块的下一个就是链表中的第一个空闲内存块 */
     memp->next = *desc->tab;
     /* *desc->tab 指向 memp 内存块中 */
    *desc->tab = memp;
}

释放函数很简单,只需要将内存池描述符的 tab 指针偏移至要释放的内存池。这样,释放的内存块就会被返回到相应的内存池中,以供后续使用。

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

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

相关文章

前端规范工程-5:Git提交信息规范(commitlint + czg)

前面讲的都是在git提交之前的一些检查流程&#xff0c;然而我们git提交信息的时候&#xff0c;也应该是需要规范的。直接进入主题&#xff1a; 目录 需安装插件清单commitlint 介绍安装配置配置commit-msg钩子提交填写commit信息czg后续方式一&#xff1a;push触动build并上传…

行为设计模式 -观察者模式- JAVA

观察者模式 一.简介二. 案例2.1 抽象主题&#xff08;Subject&#xff09;2.2 具体主题&#xff08;Concrete Subject&#xff09;2.3 抽象观察者&#xff08;Observer&#xff09;2.4 具体观察者&#xff08;Concrete Observer&#xff09;2.5 测试 三. 结论3.1 优缺点3.2 使用…

从零开始讲PCIe(0)——外设与外设总线

一、外设 计算机外设&#xff08;Peripheral&#xff09;是指连接到计算机主机以扩展其功能的外部设备。这些设备可以是输入设备、输出设备、存储设备或通信设备等&#xff0c;外设&#xff08;外围设备&#xff09;通过输入、输出、存储和通信等方式帮助计算机与用户和其他设备…

用Arduino单片机读取PCF8591模数转换器的模拟量并转化为数字输出

PCF8591是一款单芯片&#xff0c;单电源和低功耗8位CMOS数据采集设备。博文[1]对该产品已有介绍&#xff0c;此处不再赘述。但该博文是使用NVIDIA Jetson nano运行python读取输入PCF8591的模拟量的&#xff0c;读取的结果显示在屏幕上&#xff0c;或输出模拟量点亮灯。NVIDIA J…

可解释聚类又“炸出圈”啦!把准3个切入点一路开挂!创新思路一学就会~

可解释聚类是机器学习领域一个非常重要的研究方向&#xff0c;它通过引入解释性强的特征和模型&#xff0c;让我们更直观地理解聚类结果&#xff0c;从而提升聚类分析的准确性和可靠性。 这种方法在一些敏感领域如医疗、金融等非常适用&#xff0c;因为它与传统方法不同&#…

工具模块及项目整体模块框架

文章目录 工具模块logger.hpphelper.hppthreadpool.hpp 核心概念核心API交换机类型持久化⽹络通信消息应答持久化数据管理中心模块虚拟机管理模块交换路由模块消费者管理模块信道管理模块连接管理模块Broker服务器模块消费者管理信道请求模块通信连接模块项⽬模块关系图 工具模…

Oracle SQL语句没有过滤条件,究竟是否会走索引??

答案是&#xff1a;可能走索引也可能不走索引&#xff0c;具体要看列的值可不可为null&#xff0c;Oracle不会为所有列的nullable属性都为Y的sql语句走索引。 例子&#xff1a; create table t as select * from dba_objects; CREATE INDEX ix_t_name ON t(object_id, objec…

MySQL 中的 GTID 复制详解

MySQL 中的 GTID 复制详解 在 MySQL 的复制架构中&#xff0c;GTID&#xff08;Global Transaction Identifier&#xff09;复制是一种重要的技术&#xff0c;它为数据库的复制提供了更强大的功能和更高的可靠性。本文将深入探讨 MySQL 中的 GTID 复制是什么&#xff0c;以及它…

OpenCV计算机视觉库

计算机视觉和图像处理 Tensorflow入门深度神经网络图像分类目标检测图像分割OpenCVPytorchNLP自然语言处理 OpenCV 一、OpenCV简介1.1 简介1.2 OpenCV部署1.3 OpenCV模块 二、OpenCV基本操作2.1 图像的基本操作2.1.1 图像的IO操作2.1.2 绘制几何图像2.1.3 获取并修改图像的像素…

时间相关数据的统计分析(笔记更新中)

对事件相关数据的统计思路做一个笔记 可以用作肿瘤生长曲线&#xff08;Tumor Growth Curve&#xff09;/某一个药物处理后不同时间点表型的获取类型的数据。 总体来说合适的有两类&#xff0c;一类是以ANOVA为基础的方差分析&#xff0c;重复测量资料的方差分析&#xff1b;…

D - Connect the Dots Codeforces Round 976 (Div. 2)

原题 D - Connect the Dots 思路 直接去做的话会超时, 因此用差分去优化 代码 #include <bits/stdc.h> using namespace std;int f[200020]; int z; int b[11][200020];// 并查集的 find 函数 int find(int x) {return f[x] ! x ? f[x] find(f[x]) : x; }// 检查是…

食品饮料小程序搭建私域会员管理

食品饮料是商超主要经营类目之一&#xff0c;多样化的品牌/厂商/渠道/经销商&#xff0c;客户在消费方面购物渠道和选择范围广&#xff0c;无论厂商还是线下门店/线上电商都需要围绕流量/会员开展生意获得更多营收。 小程序开店基于微信平台生态分享宣传、用户店铺方便购物及提…

Flutter与原生代码通信

文章目录 1. 知识回顾2. 示例代码3. 经验总结我们在上一章回中介绍了通道相关的内容,本章回中将介绍其中的一种通道:MethodChannnel.闲话休提,让我们一起Talk Flutter吧。 1. 知识回顾 我们在上一章回中介绍了通道的概念和作用,并且提到了通道有不同的类型,本章回将其中一…

【C++】类与对象基础概念解析

恭喜你学习完C语言与数据结构的有关内容&#xff0c;现在让我们开始进行对C的学习吧~ &#x1f49d;&#x1f49d;&#x1f49d;如果你对C语言或数据结构还存在疑惑&#xff0c;欢迎观看我之前的作品 &#x1f449;【数据结构】 &#x1f449;【C语言】 目录 一、引言 二、类…

【2024年最新】基于springboot+mysql就业信息管理系统

技术摘要 技术框架&#xff1a;以springboot作为框架&#xff0c;业务模式&#xff1a;B/S模式数据库&#xff1a;MySql作为后台运行的数据库服务器&#xff1a;使用Tomcat用为系统的服务器 系统展示 系统实现功能 本次实现一个就业信息管理系统&#xff0c;通过这个系统能够…

vscode安装及c++配置编译

1、VScode下载 VS Code官网下载地址&#xff1a;Visual Studio Code - Code Editing. Redefined。 2、安装中文插件 搜索chinese&#xff0c;点击install下载安装中文插件。 3、VS Code配置C/C开发环境 3.1、MinGW-w64下载 VS Code是一个高级的编辑器&#xff0c;只能用来写代…

嵌入式系统中qt开发 Qdebug输出中文的时候变成了问号 ??? bulideroot制作的根文件系统

嵌入式系统中qt开发 Qdebug输出&#xff1f;&#xff1f;&#xff1f; bulideroot制作的根文件系统 这个问题我找了三四天了&#xff0c;因为的字符也配置了 /etc/profile中qt的环境变量我也配置了 我的/usr/share/fonts也是有字库的&#xff0c;但是qt输出的中文全是&#…

windows 11 LTSC 26100.1742 官方简体中文版

系统简介 Windows 11 LTSC&#xff08;长期服务通道&#xff09;是一个专为长期稳定性和可靠性设计的Windows 11变体&#xff0c;适合于需要最小更新和更改的关键任务系统和设备。与常规版本相比&#xff0c;LTSC版本的特点是更新频率较低&#xff0c;目的是为了保持系统的稳定…

从零开始掌握YOLOv11:揭秘三大损失函数的理想值(源码+实战)

相关文章&#xff1a; YOLOv1–v11: 版本演进及其关键技术解析-CSDN博客 YOLOv11&#xff1a;重新定义实时目标检测的未来-CSDN博客 Yolo v11目标检测实战1&#xff1a;对象分割和人流跟踪&#xff08;附源码&#xff09;-CSDN博客 YOLOv11目标检测实战2&#xff1a;人流统计…

win10下cuda12.1 +troch2.4.1+vs2022环境下编译安装flash-attn

步骤一 下载项目 先下载 https://github.com/Dao-AILab/flash-attention&#xff0c;然后在conda环境中进入项目目录 步骤二 安装依赖项 执行以下命令&#xff0c;安装cutlass库&#xff0c;该库为编译flash-attn的必须依赖 conda update --force conda conda install conda…