文章目录

  • 1 内存堆
    • 1.1 mem_init
    • 1.2 mem_malloc
    • 1.3 mem_free
  • 2 内存池
    • 2.1 memp_init
    • 2.2 memp_malloc
    • 2.3 memp_free
  • 3 内存管理宏定义

在嵌入式系统中,内存池有助于快速有效地分配内存。LwIP提供了两个灵活的方式来管理和组织内存池的大小:内存堆和内存池。当然它还支持C库中的 mallocfree来申请和释放内存,但是这种分配方式可能会产生很多堆碎片,最后造成没有内存可以分配,而且运行地越久寻找新内存的时间可能也越长,这里不建议使用这种方式。下面通过代码来详细地看一下内存堆和内存池的实现

1 内存堆

来看看LwIP中实现的堆内存管理的实现:

1.1 mem_init

先来看看mem_init函数,相关结构体和宏定义写在前面:

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;
#if MEM_OVERFLOW_CHECK/** this keeps track of the user allocation size for guard checks */mem_size_t user_size;
#endif
};#define MEM_ALIGNMENT 4
#define LWIP_MEM_ALIGN_SIZE(size) (((size) + MEM_ALIGNMENT - 1U) & ~(MEM_ALIGNMENT-1U))
#define SIZEOF_STRUCT_MEM    LWIP_MEM_ALIGN_SIZE(sizeof(struct mem))/* 用户自定义的堆内存大小 */
#define MEM_SIZE (32 * 1024)
#define MEM_SIZE_ALIGNED     LWIP_MEM_ALIGN_SIZE(MEM_SIZE)
#define LWIP_DECLARE_MEMORY_ALIGNED(variable_name, size) u8_t variable_name[LWIP_MEM_ALIGN_BUFFER(size)]
LWIP_DECLARE_MEMORY_ALIGNED(ram_heap, MEM_SIZE_ALIGNED + (2U * SIZEOF_STRUCT_MEM));
#define LWIP_RAM_HEAP_POINTER ram_heapstatic struct mem * ptr_to_mem(mem_size_t ptr)
{return (struct mem *)(void *)&ram[ptr];
}static struct mem * LWIP_MEM_LFREE_VOLATILE lfree;
static u8_t *ram;
---------------------------------------------------------------
void mem_init(void)
{struct mem *mem;/* 检查struct mem是否是四字节对齐 */LWIP_ASSERT("Sanity check alignment",(SIZEOF_STRUCT_MEM & (MEM_ALIGNMENT - 1)) == 0);/* 将ram变量的指针指向上面声明的ram_heap数组,其大小为(MEM_SIZE+2*mem结构体大小) */ram = (u8_t *)LWIP_MEM_ALIGN(LWIP_RAM_HEAP_POINTER);/* 上面ram_heap数组的大小多了两个mem结构体,分别填充在ram_heap的起始和结尾 *//* 初始化堆的起始 */mem = (struct mem *)(void *)ram;/* 下一个内存的偏移量为MEM_SIZE_ALIGNED,即结束 */mem->next = MEM_SIZE_ALIGNED;/* 前一个内存的偏移量 */mem->prev = 0;/* 当前内存没有被使用 */mem->used = 0;/* 初始化堆的结束:指向上一个内存的结束位置 */ram_end = ptr_to_mem(MEM_SIZE_ALIGNED);/* 由于当前结构体为标记堆内存的结束,无实际大小,标记使用 */ram_end->used = 1;/* 标记下一个内存的偏移量:下一个还是自己表示到头了 */ram_end->next = MEM_SIZE_ALIGNED;/* 标记前一个内存的偏移量 */ram_end->prev = MEM_SIZE_ALIGNED;/* 合理性检验:一般在每次mem_free()后调用,确保堆链表结构体内内存的合法性和是否对齐 */MEM_SANITY();/* 将堆内存中第一块可用内存的指针赋值给lfree */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);}
}

初始化后ram_heap数组的内存分配如下所示:

1.2 mem_malloc

首先来看看内存分配的函数,它的原理是First Fit,即遍历空闲内存,找到第一个满足大小的堆内存进行分配,并将剩下的返回内存堆。

对于mem_malloc有三种方式,第一种为C库的malloc,一种为使用内存池(memory pool)方式,还有一种就是内存堆方式,这里对内存堆分配方式进行介绍(具体实现见代码注释):

  • 该函数中有一个宏定义:LWIP_ALLOW_MEM_FREE_FROM_OTHER_CONTEXT,这是允许用户可以在中断或不允许等待信号量的上下文中释放pbuf,它的原理就是使用信号量和SYS_ARCH_PROTECT保护mem_malloc函数,使用SYS_ARCH_PROTECT保护mem_free函数。只有mem_mallocSYS_ARCH_UNPROTECTs后,mem_free才可以允许。

    • 很明显,这种方式要经常开关中断,如果在内存很少的嵌入式系统中,mem_malloc的速度会非常慢,一般都不会打开,所以这里不对相关宏定义的代码进行解析。
  • 函数中对于overflow检查的相关代码也将省略

void* mem_malloc(mem_size_t size_in)
{mem_size_t ptr, ptr2, size;struct mem *mem, *mem2;/* 声明一个unsigned long的变量lev用于同步 */LWIP_MEM_ALLOC_DECL_PROTECT();/* 分配的内存大小为0则直接返回 */if (size_in == 0){return NULL;}/* 保证分配的内存是字节对齐的 */size = (mem_size_t)LWIP_MEM_ALIGN_SIZE(size_in);/* 分配的内存的大小必须比MIN_SIZE_ALIGNED(12)大:防止分配过多小内存造成内存碎片 */if (size < MIN_SIZE_ALIGNED){/* every data block must be at least MIN_SIZE_ALIGNED long */size = MIN_SIZE_ALIGNED;}/* 如果size大于总的堆内存的大小或者小于待分配内存的大小,则返回NULL */if ((size > MEM_SIZE_ALIGNED) || (size < size_in)){return NULL;}/* 获得在mem_init中创建的互斥量,以并发地使用内存管理函数 */sys_mutex_lock(&mem_mutex);/* 在有OS的情况下:如果当前函数在中断中运行,则lev=basepri并设置优先级高于lev的中断才能打断;否则进入临界区 */LWIP_MEM_ALLOC_PROTECT();/* 从lfree指针开始用first fit方式寻找第一个足够大的内存 */for (ptr = mem_to_ptr(lfree); ptr < MEM_SIZE_ALIGNED - size;ptr = ptr_to_mem(ptr)->next){mem = ptr_to_mem(ptr);/* 如果该内存没有used且大小满足(size+内存头mem结构体)的大小 */if ((!mem->used) && (mem->next - (ptr + SIZEOF_STRUCT_MEM)) >= size){/* 如果该内存除去我们申请的内存后,剩余的大小还比(struct mem + 最小申请内存MIN_SIZE_ALIGNED)大则将剩下的内存返回内存堆中 */if (mem->next - (ptr + SIZEOF_STRUCT_MEM) >= (size + SIZEOF_STRUCT_MEM + MIN_SIZE_ALIGNED)){ptr2 = (mem_size_t)(ptr + SIZEOF_STRUCT_MEM + size);LWIP_ASSERT("invalid next ptr",ptr2 != MEM_SIZE_ALIGNED);/* create mem2 struct */mem2 = ptr_to_mem(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){ptr_to_mem(mem2->next)->prev = ptr2;}}else{/* 剩下的内存不足以分配一个新的内存堆,只好浪费,标记整块内存used */mem->used = 1;}/* 如果分配的内存为第一块,则需要更新lfree指针(指向第一块空闲的堆的指针) */if (mem == lfree){struct mem *cur = lfree;/* Find next free block after mem and update lowest free pointer */while (cur->used && cur != ram_end){cur = ptr_to_mem(cur->next);}lfree = cur;LWIP_ASSERT("mem_malloc: !lfree->used", ((lfree == ram_end) || (!lfree->used)));}/* 对应前面的protect,若在中断中则使能中断并清空basepri寄存器;若不在中断中,则退出临界区 */LWIP_MEM_ALLOC_UNPROTECT();/* 释放互斥锁 */sys_mutex_unlock(&mem_mutex);/* 返回申请的内存的地址,这里不打开完整性检查,即MEM_SANITY_OFFSET为0 */return (u8_t *)mem + SIZEOF_STRUCT_MEM + MEM_SANITY_OFFSET;}}/* 找不到足够大的内存,释放锁 */LWIP_MEM_ALLOC_UNPROTECT();sys_mutex_unlock(&mem_mutex);return NULL;
}

1.3 mem_free

mem_free函数也很好理解,具体见注释:

void mem_free(void *rmem)
{struct mem *mem;/* 和mem_malloc一样,声明一个lev变量 */LWIP_MEM_FREE_DECL_PROTECT();if (rmem == NULL) {return;}/* 由于分配的内存都是四字节对齐的,若不对齐则传入的参数错误 */if ((((mem_ptr_t)rmem) & (MEM_ALIGNMENT - 1)) != 0) {LWIP_MEM_ILLEGAL_FREE("mem_free: sanity check alignment");return;}/* 获取该段内存前面的结构体的内容 */mem = (struct mem *)(void *)((u8_t *)rmem - (SIZEOF_STRUCT_MEM + MEM_SANITY_OFFSET));/* 如果结构体的首地址不在整个内存堆的范围内则直接返回 */if ((u8_t *)mem < ram || (u8_t *)rmem + MIN_SIZE_ALIGNED > (u8_t *)ram_end) {return;}/* 痛mem_malloc中的LWIP_MEM_ALLOC_PROTECT() */LWIP_MEM_FREE_PROTECT();/* 如果mem->used为0,说明参数出错,直接返回 */if (!mem->used) {return;}/* 该函数检查mem->prev和mem->next的参数合法性 */if (!mem_link_valid(mem)) {LWIP_MEM_FREE_UNPROTECT();return;}/* 标记该mem为not used */mem->used = 0;/* 如果释放的mem为第一块内存,则更新lfree */if (mem < lfree) {/* the newly freed struct is now the lowest */lfree = mem;}/* 最后,如果释放的内存的prev和next有not used的话,则合并 */plug_holes(mem);LWIP_MEM_FREE_UNPROTECT();
}

2 内存池

在LwIP的数据段中保留了一个固定大小的静态内存,这段内存被分为多个内存池(pools),用来描述各种不同的数据结构,比如TCP、UDP、IP等报文的首部,它们的长度是固定的,这样每次分配和释放都不会产生内存碎片。比如有一个内存池用于TCP控制结构体struct tcp_pcb,另一个内存池用于UDP控制结构体struct udp_pcb。每个内存池都可以配置它可以容量的最大数量的数据结构,比如上面的TCP和UDP控制块的个数可以通过MEMP_NUM_TCP_PCBMEMP_NUM_UDP_PCB进行修改。

下面来对内存池的代码进行分析,代码中去掉了统计分析和overflow检查的相关代码。

2.1 memp_init

首先是内存池的初始化函数:

void memp_init(void)
{u16_t i;/* for every pool: */for (i = 0; i < LWIP_ARRAYSIZE(memp_pools); i++) {memp_init_pool(memp_pools[i]);}
}

可以看到内存池的初始化函数非常简单,就是对每一个定义的内存池进行初始化。现在来看看memp_pools是什么:

const struct memp_desc *const memp_pools[MEMP_MAX] = {
#define LWIP_MEMPOOL(name,num,size,desc) &memp_ ## name,
#include "lwip/priv/memp_std.h"
};

其中MEMP_MAX的定义如下:

typedef enum {
#define LWIP_MEMPOOL(name,num,size,desc)  MEMP_##name,
#include "lwip/priv/memp_std.h"MEMP_MAX
} memp_t;

可以看出来,在memp_std.h中定义了多个内存池,每个内存池的参数包括:名字(name)、个数(num)、大小(size)和描述(desc),下面来看一下这个文件:

#define LWIP_DECLARE_MEMORY_ALIGNED(variable_name, size) u8_t variable_name[LWIP_MEM_ALIGN_BUFFER(size)]#define LWIP_MEMPOOL_DECLARE(name,num,size,desc) \LWIP_DECLARE_MEMORY_ALIGNED(memp_memory_ ## name ## _base, ((num) * (MEMP_SIZE + MEMP_ALIGN_SIZE(size)))); \\LWIP_MEMPOOL_DECLARE_STATS_INSTANCE(memp_stats_ ## name) \\static struct memp *memp_tab_ ## name; \\const struct memp_desc memp_ ## name = { \DECLARE_LWIP_MEMPOOL_DESC(desc) \LWIP_MEMPOOL_DECLARE_STATS_REFERENCE(memp_stats_ ## name) \LWIP_MEM_ALIGN_SIZE(size), \(num), \memp_memory_ ## name ## _base, \&memp_tab_ ## name \};#define LWIP_MEMPOOL(name,num,size,desc) LWIP_MEMPOOL_DECLARE(name,num,size,desc)
------------
#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 */#if LWIP_ALTCP && LWIP_TCP
LWIP_MEMPOOL(ALTCP_PCB,      MEMP_NUM_ALTCP_PCB,       sizeof(struct altcp_pcb),      "ALTCP_PCB")
#endif /* LWIP_ALTCP && LWIP_TCP */#if LWIP_IPV4 && IP_REASSEMBLY
LWIP_MEMPOOL(REASSDATA,      MEMP_NUM_REASSDATA,       sizeof(struct ip_reassdata),   "REASSDATA")
#endif /* LWIP_IPV4 && IP_REASSEMBLY */
#if (IP_FRAG && !LWIP_NETIF_TX_SINGLE_PBUF) || (LWIP_IPV6 && LWIP_IPV6_FRAG)
LWIP_MEMPOOL(FRAG_PBUF,      MEMP_NUM_FRAG_PBUF,       sizeof(struct pbuf_custom_ref),"FRAG_PBUF")
#endif /* IP_FRAG && !LWIP_NETIF_TX_SINGLE_PBUF || (LWIP_IPV6 && LWIP_IPV6_FRAG) */#if LWIP_NETCONN || LWIP_SOCKET
LWIP_MEMPOOL(NETBUF,         MEMP_NUM_NETBUF,          sizeof(struct netbuf),         "NETBUF")
LWIP_MEMPOOL(NETCONN,        MEMP_NUM_NETCONN,         sizeof(struct netconn),        "NETCONN")
#endif /* LWIP_NETCONN || LWIP_SOCKET */
  • 对于代码中#的使用参考文章:C语言 #和##的使用

可以看到一个宏定义LWIP_MEMPOOL就根据参数声明了一个内存堆数组,还有mempmemp_desc结构体。

现在回过头来看MEMP_MAX,前面的#define LWIP_MEMPOOL(name,num,size,desc) MEMP_##name就是暂时用一下文件中这个定义,将LWIP_MEMPOOL的宏定义再进行一层替换,作为enum的值的名称,所以MEMP_MAX就是内存堆的总个数。

memp_pools就是将在memp_std.h中用LWIP_MEMPOOL声明的各个内存池的memp_desc结构体的地址包含进来。

最后再回来看memp_init_pool函数:

struct memp {struct memp *next;
};struct memp_desc {/** Element size */u16_t size;/** Number of elements */u16_t num;/** Base address */u8_t *base;/** First free element of each pool. Elements form a linked list. */struct memp **tab;
};
--------
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);/* 初始化为0:如果MEMP_MEM_INIT宏定义为1的话 */memset(memp, 0, (size_t)desc->num * (MEMP_SIZE + desc->size));/* create a linked list of memp elements */for (i = 0; i < desc->num; ++i) {memp->next = *desc->tab;*desc->tab = memp;/* cast through void* to get rid of alignment warnings */memp = (struct memp *)(void *)((u8_t *)memp + MEMP_SIZE + desc->size);}
}

其中base就是通过LWIP_MEMPOOL_DECLARE声明的第一项大小为num*(MEMP_SIZE+MEMP_ALIGN_SIZE(size))的数组。除了每个内存池的大小是固定的,它的原理和内存堆类似,内存池的大小包括内存池的实际存数据大小和内存池结构体struct memp的大小。
tab就是通过LWIP_MEMPOOL_DECLARE声明的struct *memp指针变量的地址,它表示内存池中第一个没有使用的pool。

所以这个函数就是将每个声明的内存池通过链表连接起来,其中desc->tab为每个内存池的基地址。

最后,根据memp_init_pool函数,memp_pools中的每个memp_desc初始化后的示意图如下:

2.2 memp_malloc

来看一下内存池分配函数:

void *memp_malloc(memp_t type)
{void*memp;LWIP_ERROR("memp_malloc: type < MEMP_MAX", (type < MEMP_MAX), return NULL;);memp = do_memp_malloc_pool_fn(memp_pools[type], file, line);return memp;
}

参数type的定义如下:

typedef enum {
#define LWIP_MEMPOOL(name,num,size,desc)  MEMP_##name,
#include "lwip/priv/memp_std.h"MEMP_MAX
} memp_t;

memp_t就是根据LWIP_MEMPOOL定义的各个内存池的name而组成的枚举类型,我们就通过这个枚举类型来申请相应的内存池中的内存。

然后来看看do_memp_malloc_pool_fn函数:

static void* do_memp_malloc_pool(const struct memp_desc *desc)
{struct memp *memp;/* 与内存堆中的作用一样:为了执行关中断/进临界区等操作,这里不再解释 */SYS_ARCH_DECL_PROTECT(old_level);SYS_ARCH_PROTECT(old_level);/*  获取对于memp的tab */memp = *desc->tab;if (memp != NULL) {/* 指向下一个memp结构体 */*desc->tab = memp->next;SYS_ARCH_UNPROTECT(old_level);/* 将内存的首地址返回 */return ((u8_t *)memp + MEMP_SIZE);} else {SYS_ARCH_UNPROTECT(old_level);}return NULL;
}

2.3 memp_free

void memp_free(memp_t type, void *mem)
{if (mem == NULL) {return;}do_memp_free_pool(memp_pools[type], mem);
}

同样地来看看do_memp_free_pool函数:

static void do_memp_free_pool(const struct memp_desc *desc, void *mem)
{struct memp *memp;SYS_ARCH_DECL_PROTECT(old_level);/* 获得该内存前面的结构体的地址 */memp = (struct memp *)(void *)((u8_t *)mem - MEMP_SIZE);SYS_ARCH_PROTECT(old_level);/* 将该内存地址加入到desc->tab的最前面,表示空闲 */memp->next = *desc->tab;*desc->tab = memp;SYS_ARCH_UNPROTECT(old_level);
}

3 内存管理宏定义

(1)MEM_LIBC_MALLOC:使用C库中的mallocfree管理内存
(2)MEMP_MEM_MALLOC:使用内存堆方式来分配内存池的内存。上面内存池的分配中我把相关代码注释掉了,如果此宏打开,在分配和释放内存时会直接使用mem_mallocmem_free
(3)MEM_USE_POOLS:使用内存池的方式来分配内存堆的内存。同上。
若此宏打开,还需将宏MEMP_MEM_MALLOC关闭,并将MEMP_USE_CUSTOM_POOLS宏打开,表示创建一个lwippools.h文件,然后用LWIP_MALLOC_MEMPOOL来声明自己的内存。
下面给出一个lwippools.h的配置例子:

/* OPTIONAL: Pools to replace heap allocation* Optional: Pools can be used instead of the heap for mem_malloc. If* so, these should be defined here, in increasing order according to* the pool element size.** LWIP_MALLOC_MEMPOOL(number_elements, element_size)*/
#if MEM_USE_POOLS
LWIP_MALLOC_MEMPOOL_START
LWIP_MALLOC_MEMPOOL(100, 256)
LWIP_MALLOC_MEMPOOL(50, 512)
LWIP_MALLOC_MEMPOOL(20, 1024)
LWIP_MALLOC_MEMPOOL(20, 1536)
LWIP_MALLOC_MEMPOOL_END
#endif /* MEM_USE_POOLS *//* Optional: Your custom pools can go here if you would like to use* lwIP's memory pools for anything else.*/
LWIP_MEMPOOL(SYS_MBOX, 22, 100, "SYS_MBOX")
  • 除了声明内存池给内存堆分配外,可以在最后声明一个内存池供自己使用
  • 这种方式适合用在SRAM很大的情况下,以空间换取执行速度

LwIP源码分析(3):内存堆和内存池代码详解相关推荐

  1. Spring5源码分析系列(九)Spring事务原理详解

    终于等到了B站的薪资沟通电话,美滋滋,本节开始进入Spring数据访问篇,讲解spring事务,文章参考自Tom老师视频. 事务基本概念 事务(Transaction)是访问并可能更新数据库中各种数据 ...

  2. LwIP源码分析(2):tcpip_init和tcpip_thread函数分析

    环境:FreeRTOS & LwIP 2.2.0 文章中的所有参数检测的断言代码都删除以使代码更清晰 LwIP通过调用tcpip_init来初始化TCPIP协议栈,函数如下所示,函数中代码的含 ...

  3. LWIP源码分析——ip4.c

    LWIP源码分析--ip4.c ipv4是IP栈中重要的一部分,实现功能使用了上千行代码,分析起来可能会稍显复杂,这部分采用的分析的思路是,重点思想总结部分放在前面,剩下的结合代码穿插分析 1.ipv ...

  4. Linux brk(),mmap()系统调用源码分析3:brk()的内存申请流程

    Linux brk(),mmap()系统调用源码分析 brk()的内存申请流程 荣涛 2021年4月30日 内核版本:linux-5.10.13 注释版代码:https://github.com/Rt ...

  5. Linux内存管理 brk(),mmap()系统调用源码分析2:brk()的内存释放流程

    Linux brk(),mmap()系统调用源码分析 brk()的内存释放流程 荣涛 2021年4月30日 内核版本:linux-5.10.13 注释版代码:https://github.com/Rt ...

  6. ceph bluestore源码分析:admin_socket实时获取内存池数据

    环境: 版本:ceph 12.2.1 部署完cephfs 使用ceph-fuse挂载,并写入数据 关键参数: debug_mempool = true 将该参数置为true即可查看详细的blustor ...

  7. python生成中文词云的代码_[python] 基于词云的关键词提取:wordcloud的使用、源码分析、中文词云生成和代码重写...

    1. 词云简介 词云,又称文字云.标签云,是对文本数据中出现频率较高的"关键词"在视觉上的突出呈现,形成关键词的渲染形成类似云一样的彩色图片,从而一眼就可以领略文本数据的主要表达意 ...

  8. 源码分析:Java对象的内存分配

    Java对象的分配,根据其过程,将其分为快速分配和慢速分配两种形式,其中快速分配使用无锁的指针碰撞技术在新生代的Eden区上进行分配,而慢速分配根据堆的实现方式.GC的实现方式.代的实现方式不同而具有 ...

  9. lwip源码分析 之 DHCP协议实现(一)

    文章目录 一,dhcp协议简介 二,源码解析 2.1 dhcp结构体 2.2 开始广播 2.3 回调接口 a,发送请求 b,等待ack 2.4 其他情况 一,dhcp协议简介 dhcp协议是动态主机配 ...

最新文章

  1. 如何在您的笔记本上搭建View 演示环境 -5.配置View Connection Server
  2. Spring启动流程(原理)详解--结合web.xml加载配置分析 转
  3. 腾讯助力贵阳市智慧升级 共同探索创新型城市发展
  4. 《JavaScript入门经典(第6版)》——2.7 问答
  5. Bug之本地可以发送邮件 测试服不行
  6. 【Libevent】Libevent学习笔记(三):事件循环
  7. MySQL探秘(三):InnoDB的内存结构和特性(可靠性和持久性)
  8. 解析word template返回使用的webservice WSDL和Operation
  9. go 修改结构体方法_「GCTT 出品」Go 系列教程——26. 结构体取代类
  10. 算法(2)-二叉树的遍历(递归/迭代)python实现
  11. 直播APP源码在ftp服务器搭建教程
  12. lingo与matlab转换,请教lingo与matlab
  13. St. Luke’s University Health Network是世界首批试用远程患者管理解决方案Masimo SafetyNet™来协助COVID-19住院患者的机构之一
  14. 收到offer不想去,如何优雅拒绝?
  15. word怎么删除参考文献的横线_教大家word2016怎么去掉引用参考文献中的横线
  16. 计算机专业被check后拒签,签证被Check是怎么一回事?解读美签的几种情况
  17. 智慧灯杆网关+云平台,助力智慧城市智能照明
  18. 车牌识别github资源
  19. 路漫漫........
  20. 一阶电路中的时间常数_一阶RC电路的时间常数为 ;一阶RL电路的时间常数为

热门文章

  1. Blender 制作柱体骨架
  2. PF_RING 6.0.2发布
  3. 基于stm32F4的项目总结:控制层设计(四)直流有刷电机驱动基础知识
  4. HT-IDE3000显示This application has requested the Runtime to terminate it in an unusual way
  5. Perl实现逆波兰式与递归计算
  6. 踩雷1:Android Studio:3dmap 隐私合规校验失败: errorcode 555570 确保调用SDK任何接口前先调用更新隐私合规updatePrivacyShow、updateP
  7. PHP_基础学习(10)
  8. 神舟八号利用计算机进行飞行状态调整属于,“神舟八号”飞船利用计算机进行飞行状态调整属于____。...
  9. 诊所管理系统方案/案列/APP/软件/小程序/网站
  10. 瑞友云电脑与政府行业