【STM32】TCP/IP通信协议--LWIP内存管理

news2024/11/18 13:27:43

五、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/2178924.html

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

相关文章

只申请一块sizeofimage的内存能否实现PE文件的拉伸

不能,别试了,浪费时间. 从最后一个节复制,也会被覆盖 BOOL StrechFileBuffer(__in char* m_fileName, __inout char** LPImageBuffer) {FILE* file (fopen(m_fileName, "rb"));if (file NULL){printf("error :%d", GetLastError());return FALSE;}// 从文…

【HyperWorks入门教程】HyperWorks的shrink warp meshing

在HyperWorks中&#xff0c;针对某些具有复杂几何特征的零部件的网格剖分&#xff0c;Altair HyperMesh 向用户提供了一种名为 Shrink Warp Meshing 的技术&#xff0c;快捷高效地完成有限元模型前处理工作。例如在车辆碰撞分析研究中&#xff0c;用户可以使用 Shrink Warp Mes…

3GPP链路级仿真-Link-Level Simulator for 5G Localization

文章目录 II. SYSTEM ARCHITECTURE AND CAPABILITIESA. System Architecture III. KEY COMPONENTSA. Transmission Models of the Positioning SignalsB. Dedicated Wireless Channel Model IV. APPLICATION CASESA. Two-Dimensional Mobile Terminal Localization仿真工作流程…

Spring+Mybatis IOC + AOP + 开启事务 模板

萌新小白刚入行Java 框架的可以试着自己拿着这个代码改一改,看看能不能运行成功 第一步:创建maven项目,在pom.xml文件中引入以下需要用到的jar包 <project xmlns"http://maven.apache.org/POM/4.0.0" xmlns:xsi"http://www.w3.org/2001/XMLSchema-instance&…

matlab2019b-2024b knnclassify无法识别的问题(亲测,已解决)

matlab2019a-2024b 已经移除了knnclassify分类&#xff0c;修改了名称和功能&#xff0c;如果你还想使用它&#xff0c;就必须在2018版本以前的旧版本中找相关的工具箱&#xff08;这是免费的哦&#xff0c;如果官网下载 需要付费&#xff09;。 这里本人从2014a中分离出的工具…

Docker启动 Redis提示:Can‘t initialize Background Jobg

问题说明: 在使用docker启动redis失败&#xff0c;但是查看容器日志&#xff0c;除了提示 Fatal:Cant initialize Background Jobg&#xff0c;没有其他错误信息。经过长时间查找资料及试错&#xff0c;现记录下可能的产生原因及解决方案&#xff0c;以便以后参考。 产生原因&…

【数据结构与算法】Z算法(扩展KMP)(C++和Python写法)

Z算法&#xff08;扩展KMP&#xff09; 文章目录 Z算法&#xff08;扩展KMP&#xff09;朴素求法线性求法力扣类型题变种题&#xff1a;[3303. 第一个几乎相等子字符串的下标](https://leetcode.cn/problems/find-the-occurrence-of-first-almost-equal-substring/) 所谓Z算法&…

[题解] Codeforces Round 976 (Div. 2) A ~ E

A. Find Minimum Operations 签到. void solve() {int n, k;cin >> n >> k;if (k 1) {cout << n << endl;return;}int ans 0;while (n) {ans n % k;n / k;}cout << ans << endl; }B. Brightness Begins 打表发现, 翻转完后的序列为: 0…

基于SSM+小程序的流浪动物领养管理系统(救助1)(源码+sql脚本+视频导入教程+文档)

&#x1f449;文末查看项目功能视频演示获取源码sql脚本视频导入教程视频 1、项目介绍 本系统功能为信息发布管理、领养记录管理、动物小圈管理、求助日报管理等。本系统的使用角色为管理员和用户&#xff0c;用户可以发布自己捡到的流浪动物、求领养信息、申请领养&#xff…

从零开发操作系统

没有操作系统 要考虑放到什么位置 org 07c00h 我用nasm&#xff08;汇编编译&#xff09; 放到7c00处 ibm兼容机 AX发生变化 -寄存器 不可能做存储 内存- 代码段数据段 if else --指令 代码 int a -数据段 必须告诉计算机代码段从哪里开始 改变cs寄存器里面的值可以改变推进寄…

【MYSQL】MYSQL约束

约束是在创建表的时候用的 1、概念 约束英文&#xff1a;constraint 约束实际上就是表中数据的限制条件 2、作用 表在设计的时候加入约束的目的就是为了保证表中的记录完整性和有效性&#xff0c;比如&#xff1a;用户表有些列的值&#xff08;手机号&#xff09;不能为空…

STM32自动下载电路分享及注意事项

文章目录 简介ISP下载启动配置 USB转串口芯片CH340C手动isp下载自动isp下载RTS、DTR电平变化分析注意事项 简介 在嵌入式开发中&#xff0c;使用STM32下载程序&#xff0c;可以通过仿真器下载&#xff0c;也可以通过串口下载。在stm32串口下载时&#xff0c;我们需要手动配置启…

gradle的入门及kotlin的了解

gradle项目创建方式 1.idea springboot initalizer 2.命令行 gradle目录结构 gradle命令 gradle wrapper 一个解决不同项目需要不同版本gradle的问题 比如&#xff0c;对方电脑没用安装gradle 对方电脑安装了gradle&#xff0c;但是版本太旧了 于是&#xff0c;在项目根目…

Qt 学习第十一天:QTableWidget 的使用

一、创建QTableWidget对象&#xff0c;设置大小&#xff0c;在窗口的位置 //创建tablewidgetQTableWidget *table new QTableWidget(this);table->resize(550, 300);table->move(100, 100); //移动 二、设置表头 //设置表头QStringList headerList; //定义headerList…

Webpack 特性探讨:CDN、分包、Tree Shaking 与热更新

文章目录 前言包准备CDN 集成代码分包Tree Shaking原理实现条件&#xff1a;解决 treeShaking 无效方案&#xff1a;示例代码&#xff1a; 热更新&#xff08;HMR&#xff09; 前言 Webpack 作为现代前端开发中的核心构建工具&#xff0c;提供了丰富的特性来帮助开发者优化和打…

63.5 注意力提示_by《李沐:动手学深度学习v2》pytorch版

系列文章目录 文章目录 系列文章目录注意力提示生物学中的注意力提示查询、键和值注意力的可视化使用 show_heatmaps 显示注意力权重代码示例 代码解析结果 小结练习 注意力提示 &#x1f3f7;sec_attention-cues 感谢读者对本书的关注&#xff0c;因为读者的注意力是一种稀缺…

Python 读取与处理出入库 Excel 数据实战案例(HTML 网页展示)

有如下数据&#xff0c;需要对数据合并处理&#xff0c;输出到数据库。 数据样例&#xff1a;&#x1f447; excel内容&#xff1a; 出入库统计表河北库.xlsx: 出入库统计表天津库.xlsx: 01实现过程 1、创建test.py文件&#xff0c;然后将下面代码复制到里面&#xff0c;最后…

original多因子图绘制

成品参考 首先导入数据 设置过程 设置X轴 设置图 双击空白部分设置图层宽度&#xff08;也需要设置高度&#xff09; 颜色配置 1.删除边框 合适的参数与颜色&#xff08;设置为单色&#xff09;

PDF转换为TIF,JPG的一个简易工具(含下载链接)

目录 0.前言&#xff1a; 1.工具目录 2.工具功能&#xff08;效果&#xff09;&#xff0c;如何运行 效果 PDF转换为JPG&#xff08;带颜色&#xff09; PDF转换为TIF&#xff08;LZW形式压缩&#xff0c;可以显示子的深浅&#xff09; PDF转换为TIF&#xff08;CCITT形…

Squaretest单元测试辅助工具使用

1、idea安装插件 Squaretest 然后关掉idea 2、安装字节码软件&#xff08;jclasslib&#xff09; 3、找到idea里面的Squaretest安装目录 找到包含TestStarter的jar包 4、打开 com.squaretest.c.f 打开后选择常量池 5、找到第16个修改 Long value值&#xff0c;修改的数字即为使…