LwIP源码分析(3):内存堆和内存池代码详解
文章目录
- 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库中的 malloc
和 free
来申请和释放内存,但是这种分配方式可能会产生很多堆碎片,最后造成没有内存可以分配,而且运行地越久寻找新内存的时间可能也越长,这里不建议使用这种方式。下面通过代码来详细地看一下内存堆和内存池的实现
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_malloc
在SYS_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_PCB
和MEMP_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
就根据参数声明了一个内存堆数组,还有memp
和memp_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库中的malloc
和free
管理内存
(2)MEMP_MEM_MALLOC
:使用内存堆方式来分配内存池的内存。上面内存池的分配中我把相关代码注释掉了,如果此宏打开,在分配和释放内存时会直接使用mem_malloc
和mem_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):内存堆和内存池代码详解相关推荐
- Spring5源码分析系列(九)Spring事务原理详解
终于等到了B站的薪资沟通电话,美滋滋,本节开始进入Spring数据访问篇,讲解spring事务,文章参考自Tom老师视频. 事务基本概念 事务(Transaction)是访问并可能更新数据库中各种数据 ...
- LwIP源码分析(2):tcpip_init和tcpip_thread函数分析
环境:FreeRTOS & LwIP 2.2.0 文章中的所有参数检测的断言代码都删除以使代码更清晰 LwIP通过调用tcpip_init来初始化TCPIP协议栈,函数如下所示,函数中代码的含 ...
- LWIP源码分析——ip4.c
LWIP源码分析--ip4.c ipv4是IP栈中重要的一部分,实现功能使用了上千行代码,分析起来可能会稍显复杂,这部分采用的分析的思路是,重点思想总结部分放在前面,剩下的结合代码穿插分析 1.ipv ...
- Linux brk(),mmap()系统调用源码分析3:brk()的内存申请流程
Linux brk(),mmap()系统调用源码分析 brk()的内存申请流程 荣涛 2021年4月30日 内核版本:linux-5.10.13 注释版代码:https://github.com/Rt ...
- Linux内存管理 brk(),mmap()系统调用源码分析2:brk()的内存释放流程
Linux brk(),mmap()系统调用源码分析 brk()的内存释放流程 荣涛 2021年4月30日 内核版本:linux-5.10.13 注释版代码:https://github.com/Rt ...
- ceph bluestore源码分析:admin_socket实时获取内存池数据
环境: 版本:ceph 12.2.1 部署完cephfs 使用ceph-fuse挂载,并写入数据 关键参数: debug_mempool = true 将该参数置为true即可查看详细的blustor ...
- python生成中文词云的代码_[python] 基于词云的关键词提取:wordcloud的使用、源码分析、中文词云生成和代码重写...
1. 词云简介 词云,又称文字云.标签云,是对文本数据中出现频率较高的"关键词"在视觉上的突出呈现,形成关键词的渲染形成类似云一样的彩色图片,从而一眼就可以领略文本数据的主要表达意 ...
- 源码分析:Java对象的内存分配
Java对象的分配,根据其过程,将其分为快速分配和慢速分配两种形式,其中快速分配使用无锁的指针碰撞技术在新生代的Eden区上进行分配,而慢速分配根据堆的实现方式.GC的实现方式.代的实现方式不同而具有 ...
- lwip源码分析 之 DHCP协议实现(一)
文章目录 一,dhcp协议简介 二,源码解析 2.1 dhcp结构体 2.2 开始广播 2.3 回调接口 a,发送请求 b,等待ack 2.4 其他情况 一,dhcp协议简介 dhcp协议是动态主机配 ...
最新文章
- 如何在您的笔记本上搭建View 演示环境 -5.配置View Connection Server
- Spring启动流程(原理)详解--结合web.xml加载配置分析 转
- 腾讯助力贵阳市智慧升级 共同探索创新型城市发展
- 《JavaScript入门经典(第6版)》——2.7 问答
- Bug之本地可以发送邮件 测试服不行
- 【Libevent】Libevent学习笔记(三):事件循环
- MySQL探秘(三):InnoDB的内存结构和特性(可靠性和持久性)
- 解析word template返回使用的webservice WSDL和Operation
- go 修改结构体方法_「GCTT 出品」Go 系列教程——26. 结构体取代类
- 算法(2)-二叉树的遍历(递归/迭代)python实现
- 直播APP源码在ftp服务器搭建教程
- lingo与matlab转换,请教lingo与matlab
- St. Luke’s University Health Network是世界首批试用远程患者管理解决方案Masimo SafetyNet™来协助COVID-19住院患者的机构之一
- 收到offer不想去,如何优雅拒绝?
- word怎么删除参考文献的横线_教大家word2016怎么去掉引用参考文献中的横线
- 计算机专业被check后拒签,签证被Check是怎么一回事?解读美签的几种情况
- 智慧灯杆网关+云平台,助力智慧城市智能照明
- 车牌识别github资源
- 路漫漫........
- 一阶电路中的时间常数_一阶RC电路的时间常数为 ;一阶RL电路的时间常数为
热门文章
- Blender 制作柱体骨架
- PF_RING 6.0.2发布
- 基于stm32F4的项目总结:控制层设计(四)直流有刷电机驱动基础知识
- HT-IDE3000显示This application has requested the Runtime to terminate it in an unusual way
- Perl实现逆波兰式与递归计算
- 踩雷1:Android Studio:3dmap 隐私合规校验失败: errorcode 555570 确保调用SDK任何接口前先调用更新隐私合规updatePrivacyShow、updateP
- PHP_基础学习(10)
- 神舟八号利用计算机进行飞行状态调整属于,“神舟八号”飞船利用计算机进行飞行状态调整属于____。...
- 诊所管理系统方案/案列/APP/软件/小程序/网站
- 瑞友云电脑与政府行业