文章目录

  • 一,数据在tcp/ip层中的传递
  • 二,认识pbuf结构体
  • 二,创建一个pbuf
  • 三,释放pbuf
  • 四,给pbuf链表减肥
  • 五,移动payload
  • 六,小结

lwip使用pbuf对数据进行发送与接收,灵活的pbuf结构体使得数据在不同网络层之间传输时可以减少内存的开销,内存复制所占用的时间,一切都是为了节约内存,增加数据在不同层之间传递的速度。

一,数据在tcp/ip层中的传递

数据如何从tcp层一层层传递到最底层的物理层并发送出去呢?

应用层需要发送的数据传递到tcp层时,tcp层给数据添加首部数据,tcp层传递给ip层时,ip层将tcp层的所有数据(payload和首部数据)当成发送的数据,并给这份数据添加首部,这样一层层传递下去,如图:

二,认识pbuf结构体

为了更好的描述以上的数据传递过程,pbuf他来了。

struct pbuf {struct pbuf *next;  //指向下一个pbufvoid *payload;  //指向buff中的真实数据u16_t tot_len;  //该len与其后所有pbuf的lenu16_t len;  //payload中数据长度,不包括首部u8_t type;u8_t flags;u16_t ref;  //buffer被引用次数,包括next};

1,pbuf结构体很简单,他支持单向链表,其核心是payload指针,该指针指向真实的数据起始地址,而payload前面的有一段长度为offset的偏移内存,这个内存是用于存放数据的首部的;这点与第一节所述一样。

PBUF_RAM 示意图:

对于不同网络层的pbuf,其首部的数据也是不同的,所以其对应的pbuf中offset的长度也是不一样。

例如:tcp报文中,固定首部通常是20个字节,还有4*n的选项字段和填充字段,所以tcp层的pbuf中,offset的最小值是20,其后payload指向tcp的数据。

2,len表示的是payload数据的长度,也就是不包括首部。

3,tot_len:表示当前pbuf和后面所有pbuf的len之和。

3,flag是记录pbuf的一些标志。其值如下:

/** indicates this packet's data should be immediately passed to the application */
#define PBUF_FLAG_PUSH      0x01U //立即发送
/** indicates this is a custom pbuf: pbuf_free calls pbuf_custom->custom_free_function()when the last reference is released (plus custom PBUF_RAM cannot be trimmed) */
#define PBUF_FLAG_IS_CUSTOM 0x02U
/** indicates this pbuf is UDP multicast to be looped back */
#define PBUF_FLAG_MCASTLOOP 0x04U //udp多播返回
/** indicates this pbuf was received as link-level broadcast */
#define PBUF_FLAG_LLBCAST   0x08U //链路层的广播
/** indicates this pbuf was received as link-level multicast */
#define PBUF_FLAG_LLMCAST   0x10U //链路层的多播
/** indicates this pbuf includes a TCP FIN flag */
#define PBUF_FLAG_TCP_FIN   0x20U //tcp挥手标志

4,ref:表示该pbuf被外部引用的次数,也包括被上一个pbuf的next引用的情况,该变量用于防止在释放pbuf后导致内存读取错误。

5,由于payload指向的内存的性质不同,导致了pbuf的类型不同,pbuf的类型可用分为四种:在理解pbuf时,将pbuf结构体与payload指向的内存分开思考。

PBUF_RAM, 用于发送,pbuf和payload在连续的内存上
PBUF_ROM, pbuf在内存中,payload在外存
PBUF_REF, pbuf来自内存池,payload是其他程序段分配的内存,所以payload可能会被修改,发送时要复制payload;
PBUF_POOL, 用于接收,使用内存池分配。pbuf和payload在同一内存,pool内存大小是固定的,所以实际情况可能是多个pbuf连在一起。

PBUF_POOL示意图:

PBUF_REF与PBUF_ROM 示意图

二,创建一个pbuf

通过pbuf_alloc()创建一个pbuf,要创建一个pbuf,需要知道三个参数:

1,这个pbuf所在的网络层,以此来确定offset的值,上层的offset不仅要为自己的首部留出空间,而且还需要为下层的首部留出空间,所以层级越高,offset越大。

2,存放数据的大小
3,pbuf的类型

代码如下,参考注释和第二节的各种类型的示意图,理解代码逻辑

//创建一个pbuf
//layer:网络层
//length:数据长度
//type:buffer类型
pbuf_alloc(pbuf_layer layer, u16_t length, pbuf_type type)
{struct pbuf *p, *q, *r;u16_t offset; //payload在buffer的偏移s32_t rem_len; /* remaining length */LWIP_DEBUGF(PBUF_DEBUG | LWIP_DBG_TRACE, ("pbuf_alloc(length=%"U16_F")\n", length));/* determine header offset *///根据层级不同,计算不同的偏移空间,越高的层偏移越大switch (layer) {case PBUF_TRANSPORT:/* add room for transport (often TCP) layer header */offset = PBUF_LINK_ENCAPSULATION_HLEN + PBUF_LINK_HLEN + PBUF_IP_HLEN + PBUF_TRANSPORT_HLEN;  break;case PBUF_IP:/* add room for IP layer header */offset = PBUF_LINK_ENCAPSULATION_HLEN + PBUF_LINK_HLEN + PBUF_IP_HLEN;break;case PBUF_LINK:/* add room for link layer header */offset = PBUF_LINK_ENCAPSULATION_HLEN + PBUF_LINK_HLEN;break;case PBUF_RAW_TX:/* add room for encapsulating link layer headers (e.g. 802.11) */offset = PBUF_LINK_ENCAPSULATION_HLEN;break;case PBUF_RAW:/* no offset (e.g. RX buffers or chain successors) */offset = 0;break;default:LWIP_ASSERT("pbuf_alloc: bad pbuf layer", 0);return NULL;}//使用不同类型的内存,内存分配代码不同switch (type) {case PBUF_POOL: //通过内存池分配,可能需要若干个pool/* allocate head of pbuf chain into p */p = (struct pbuf *)memp_malloc(MEMP_PBUF_POOL); //先分配第一个poolLWIP_DEBUGF(PBUF_DEBUG | LWIP_DBG_TRACE, ("pbuf_alloc: allocated pbuf %p\n", (void *)p));if (p == NULL) {PBUF_POOL_IS_EMPTY();return NULL;}//设置相关成员的值p->type = type;p->next = NULL;/* make the payload pointer point 'offset' bytes into pbuf data memory *///移动payload在offset之后p->payload = LWIP_MEM_ALIGN((void *)((u8_t *)p + (SIZEOF_STRUCT_PBUF + offset)));LWIP_ASSERT("pbuf_alloc: pbuf p->payload properly aligned",((mem_ptr_t)p->payload % MEM_ALIGNMENT) == 0);/* the total length of the pbuf chain is the requested size */p->tot_len = length;/* set the length of the first pbuf in the chain *///计算第一个pbuf的payload长度:若length小于一个pool,则就是length,否则就是(pool长度-偏移)p->len = LWIP_MIN(length, PBUF_POOL_BUFSIZE_ALIGNED - LWIP_MEM_ALIGN_SIZE(offset));LWIP_ASSERT("check p->payload + p->len does not overflow pbuf",((u8_t*)p->payload + p->len <=(u8_t*)p + SIZEOF_STRUCT_PBUF + PBUF_POOL_BUFSIZE_ALIGNED));LWIP_ASSERT("PBUF_POOL_BUFSIZE must be bigger than MEM_ALIGNMENT",(PBUF_POOL_BUFSIZE_ALIGNED - LWIP_MEM_ALIGN_SIZE(offset)) > 0 );/* set reference count (needed here in case we fail) */p->ref = 1; //引用次数+1/* now allocate the tail of the pbuf chain *//* remember first pbuf for linkage in next iteration */r = p;    //保存第一个pbuf指针/* remaining length to be allocated */rem_len = length - p->len;  //还需要分配的长度/* any remaining pbufs to be allocated? *///还要再分配pool,直到满足所需内存,除第一个pbuf外,其他pbuf不需要offset预留空间给首部\以下pbuf以链表组织在第一个pbuf后while (rem_len > 0) { q = (struct pbuf *)memp_malloc(MEMP_PBUF_POOL);if (q == NULL) {PBUF_POOL_IS_EMPTY();/* free chain so far allocated */pbuf_free(p);/* bail out unsuccessfully */return NULL;}//设置pbuf的字段q->type = type;q->flags = 0;q->next = NULL;/* make previous pbuf point to this pbuf */r->next = q; //将pbuf与前面的pbuf连接 /* set total length of this pbuf and next in chain */LWIP_ASSERT("rem_len < max_u16_t", rem_len < 0xffff);q->tot_len = (u16_t)rem_len;/* this pbuf length is pool size, unless smaller sized tail */q->len = LWIP_MIN((u16_t)rem_len, PBUF_POOL_BUFSIZE_ALIGNED); //!不是第一个pbuf,不需要偏移q->payload = (void *)((u8_t *)q + SIZEOF_STRUCT_PBUF);  //payload只需要移动固定SIZEOF_STRUCT_PBUF个字节LWIP_ASSERT("pbuf_alloc: pbuf q->payload properly aligned",((mem_ptr_t)q->payload % MEM_ALIGNMENT) == 0);LWIP_ASSERT("check p->payload + p->len does not overflow pbuf",((u8_t*)p->payload + p->len <=(u8_t*)p + SIZEOF_STRUCT_PBUF + PBUF_POOL_BUFSIZE_ALIGNED));q->ref = 1; //被next引用/* calculate remaining length to be allocated */rem_len -= q->len; /* remember this pbuf for linkage in next iteration */r = q;}/* end of chain *//*r->next = NULL;*/break;case PBUF_RAM:{//分配的内存=pbuf结构体大小+偏移大小+真实数据大小mem_size_t alloc_len = LWIP_MEM_ALIGN_SIZE(SIZEOF_STRUCT_PBUF + offset) + LWIP_MEM_ALIGN_SIZE(length);/* bug #50040: Check for integer overflow when calculating alloc_len *///检验alloc_len(u16)是否溢出if (alloc_len < LWIP_MEM_ALIGN_SIZE(length)) {return NULL;}/* If pbuf is to be allocated in RAM, allocate memory for it. */p = (struct pbuf*)mem_malloc(alloc_len);}if (p == NULL) {return NULL;}/* Set up internal structure of the pbuf. *///初始化pbuf成员p->payload = LWIP_MEM_ALIGN((void *)((u8_t *)p + SIZEOF_STRUCT_PBUF + offset)); p->len = p->tot_len = length;p->next = NULL;p->type = type;LWIP_ASSERT("pbuf_alloc: pbuf->payload properly aligned",((mem_ptr_t)p->payload % MEM_ALIGNMENT) == 0);break;/* pbuf references existing (non-volatile static constant) ROM payload? */case PBUF_ROM:/* pbuf references existing (externally allocated) RAM payload? */case PBUF_REF:/* only allocate memory for the pbuf structure */p = (struct pbuf *)memp_malloc(MEMP_PBUF);if (p == NULL) {LWIP_DEBUGF(PBUF_DEBUG | LWIP_DBG_LEVEL_SERIOUS,("pbuf_alloc: Could not allocate MEMP_PBUF for PBUF_%s.\n",(type == PBUF_ROM) ? "ROM" : "REF"));return NULL;}/* caller must set this field properly, afterwards */p->payload = NULL;p->len = p->tot_len = length;p->next = NULL;p->type = type;break;default:LWIP_ASSERT("pbuf_alloc: erroneous type", 0);return NULL;}/* set reference count */p->ref = 1;/* set flags */p->flags = 0;LWIP_DEBUGF(PBUF_DEBUG | LWIP_DBG_TRACE, ("pbuf_alloc(length=%"U16_F") == %p\n", length, (void *)p));return p;
}

三,释放pbuf

释放pbuf需要注意pbuf被引用的次数。

当pbuf的ref成员为0时,则可以被释放,其后的pbuf会被判断是否需要被释放。若ref>0,则将ref-1并退出;具体的释放方式是通过调用内存释放函数进行释放,代码及注释如下:

u8_t
pbuf_free(struct pbuf *p)
{u16_t type;struct pbuf *q;u8_t count;if (p == NULL) {LWIP_ASSERT("p != NULL", p != NULL);LWIP_DEBUGF(PBUF_DEBUG | LWIP_DBG_LEVEL_SERIOUS,("pbuf_free(p == NULL) was called.\n"));return 0;}LWIP_DEBUGF(PBUF_DEBUG | LWIP_DBG_TRACE, ("pbuf_free(%p)\n", (void *)p));PERF_START;LWIP_ASSERT("pbuf_free: sane type",p->type == PBUF_RAM || p->type == PBUF_ROM ||p->type == PBUF_REF || p->type == PBUF_POOL);count = 0;  //记录被释放的pbuf数量while (p != NULL) {u16_t ref;SYS_ARCH_DECL_PROTECT(old_level); //申请临界保护变量SYS_ARCH_PROTECT(old_level);  //进入临界区LWIP_ASSERT("pbuf_free: p->ref > 0", p->ref > 0);ref = --(p->ref); //该pbuf引用次数-1SYS_ARCH_UNPROTECT(old_level);//若引用次数为0,根据pbuf不同类型释放if (ref == 0) { q = p->next;    //保存下一个pbufLWIP_DEBUGF( PBUF_DEBUG | LWIP_DBG_TRACE, ("pbuf_free: deallocating %p\n", (void *)p));type = p->type;
#if LWIP_SUPPORT_CUSTOM_PBUFif ((p->flags & PBUF_FLAG_IS_CUSTOM) != 0) {struct pbuf_custom *pc = (struct pbuf_custom*)p;LWIP_ASSERT("pc->custom_free_function != NULL", pc->custom_free_function != NULL);pc->custom_free_function(p);} else
#endif {//pool类型释放MEMP_PBUF_POOLif (type == PBUF_POOL) {memp_free(MEMP_PBUF_POOL, p);} else if (type == PBUF_ROM || type == PBUF_REF) {#if ESP_LWIPif (p->l2_owner != NULL&& p->l2_buf != NULL&& p->l2_owner->l2_buffer_free_notify != NULL) {p->l2_owner->l2_buffer_free_notify(p->l2_buf);}
#endifmemp_free(MEMP_PBUF, p);/* type == PBUF_RAM */} else {mem_free(p);}}count++;//检查下一个是否也需要释放p = q;} else {  //不为0,退出释放LWIP_DEBUGF( PBUF_DEBUG | LWIP_DBG_TRACE, ("pbuf_free: %p has ref %"U16_F", ending here.\n", (void *)p, ref));p = NULL;}}PERF_STOP("pbuf_free");return count;
}

四,给pbuf链表减肥

pbuf在使用中,可能原先分配的内存过大,需要调整为小点的内存,使用函数pbuf_realloc(struct pbuf *p, u16_t new_len);可以为pbuf链表重新分配内存:

其重点是对pbuf链表的操作,即找到链表中new_len所在的那个pbuf,然后给他重新分配内存,并释放它后面的pbuf。

代码及注释如下:

//给pbuf减肥,新的长度为new——len
void
pbuf_realloc(struct pbuf *p, u16_t new_len)
{struct pbuf *q;u16_t rem_len; /* remaining length */s32_t grow; //需要增加的长度,其实这个值是负数,也就是长度实际上是减少LWIP_ASSERT("pbuf_realloc: p != NULL", p != NULL);LWIP_ASSERT("pbuf_realloc: sane p->type", p->type == PBUF_POOL ||p->type == PBUF_ROM ||p->type == PBUF_RAM ||p->type == PBUF_REF);//新长度不能大于pbuf链的总长if (new_len >= p->tot_len) {/* enlarging not yet supported */return;}grow = new_len - p->tot_len;  //需要减少的长度rem_len = new_len;  //剩余长度=全新的长度q = p;//从pbuf链开始往下找,找到满足pbuf链表中长度为new_len时的pbufwhile (rem_len > q->len) {rem_len -= q->len;  //每经过一个pbuf剩余长度就减少p->lenLWIP_ASSERT("grow < max_u16_t", grow < 0xffff);q->tot_len += (u16_t)grow;  //该pbuf的tot_len减少q = q->next;  //下一个LWIP_ASSERT("pbuf_realloc: q != NULL", q != NULL);}//找到最后一个pbuf,PBUF_RAM类型且rem_len小于pbuf原来的大小,则重新分配pbuf的大小if ((q->type == PBUF_RAM) && (rem_len != q->len)
#if LWIP_SUPPORT_CUSTOM_PBUF&& ((q->flags & PBUF_FLAG_IS_CUSTOM) == 0)
#endif ) {//新的pbuf=首部大小(payload-q)+rem_lenq = (struct pbuf *)mem_trim(q, (u16_t)((u8_t *)q->payload - (u8_t *)q) + rem_len);LWIP_ASSERT("mem_trim returned q == NULL", q != NULL);}//调节最后一个pbuf的长度q->len = rem_len;q->tot_len = q->len;//q后面的pbuf不会被使用了,释放掉if (q->next != NULL) {pbuf_free(q->next);}q->next = NULL;}

五,移动payload

在第一节我们可以看到,payload指针在数据传递过程中需要频繁的移动,这个过程是由函数pbuf_header();实现的。

//header_size_increment>0,payload前移,数据传递下层
//header_size_increment<0,payload后移,数据传递上层
pbuf_header(struct pbuf *p, s16_t header_size_increment)
{return pbuf_header_impl(p, header_size_increment, 0);
}

函数很简单,header_size_increment决定了payload移动的方向和距离。通过调用pbuf_header_impl();实现;
其中,需要判断pbuf结构体与payload指向的地址是否连续,如果连续的情况(如RAM,POOL类型)则需要注意payload指针不能超出边界。代码注释如下:

//移动pbuf首部地址
//header_size_increment>0 首部在payload外,需要放到payload中
pbuf_header_impl(struct pbuf *p, s16_t header_size_increment, u8_t force)
{u16_t type;void *payload;u16_t increment_magnitude;  //位移LWIP_ASSERT("p != NULL", p != NULL);if ((header_size_increment == 0) || (p == NULL)) {return 0;}//计算header_size_increment的绝对值if (header_size_increment < 0) {increment_magnitude = (u16_t)-header_size_increment;/* Check that we aren't going to move off the end of the pbuf */LWIP_ERROR("increment_magnitude <= p->len", (increment_magnitude <= p->len), return 1;);} else {increment_magnitude = (u16_t)header_size_increment;}type = p->type;/* remember current payload pointer */payload = p->payload; //暂存原payload/* pbuf types containing payloads? *///如果payload与pbuf结构体是在连续内存,则直接移动payloadif (type == PBUF_RAM || type == PBUF_POOL) {/* set new payload pointer */p->payload = (u8_t *)p->payload - header_size_increment;/* boundary check fails? *///如果payload超过buffer头部内存边界,则复原payload,退出if ((u8_t *)p->payload < (u8_t *)p + SIZEOF_STRUCT_PBUF) {LWIP_DEBUGF( PBUF_DEBUG | LWIP_DBG_TRACE,("pbuf_header: failed as %p < %p (not enough space for new header size)\n",(void *)p->payload, (void *)((u8_t *)p + SIZEOF_STRUCT_PBUF)));/* restore old payload pointer */p->payload = payload;/* bail out unsuccessfully */return 1;}//如果pbuf与payload内存不连续,则无需检查是否超出边界} else if (type == PBUF_REF || type == PBUF_ROM) {/* hide a header in the payload? *///header_size_increment < 0说明首部在payload中,将payload指针后移if ((header_size_increment < 0) && (increment_magnitude <= p->len)) {/* increase payload pointer */p->payload = (u8_t *)p->payload - header_size_increment;} else if ((header_size_increment > 0) && force) {p->payload = (u8_t *)p->payload - header_size_increment;  //首部在payload外,payload需要前移} else {/* cannot expand payload to front (yet!)* bail out unsuccessfully */return 1;}} else {/* Unknown type */LWIP_ASSERT("bad pbuf type", 0);return 1;}/* modify pbuf length fields *///更新pbuf的成员p->len += header_size_increment;p->tot_len += header_size_increment;LWIP_DEBUGF(PBUF_DEBUG | LWIP_DBG_TRACE, ("pbuf_header: old %p new %p (%"S16_F")\n",(void *)payload, (void *)p->payload, header_size_increment));return 0;
}

六,小结

学习好pbuf,重点是理解pbuf结构体的payload成员,以及对各个类型的pbuf有一个抽象的认识。最好搭配示意图理解pbuf在数据传递过程中的灵活性。
pbuf是lwip协议各层数据传递的基础,掌握得好,后面事半功倍。
pbuf其他函数比较少用,读者可以自己去看,加深对pbuf的理解。

lwip协议栈源码分析之pbuf相关推荐

  1. lwip路由实现_TCP超时与重传《LwIP协议栈源码详解——TCP/IP协议的实现》

    在TCP两端交互过程中,数据和确认都有可能丢失.TCP通过在发送时设置一个定时器来解决这种问题.如果当定时器溢出时还没有收到确认,它就重传该数据.对任何TCP协议实现而言,怎样决定超时间隔和如何确定重 ...

  2. linux网络协议栈源码分析 - 传输层(TCP连接的建立)

    1.bind系统调用 1.1.地址端口及状态检查(inet_bind) 通过路由表查找绑定地址的路由类型,对于非本地IP检查是否允许绑定非本地IP地址:检查公认端口绑定权限,是否允许绑定0~1024端 ...

  3. linux网络协议栈源码分析 - 邻居子系统邻居状态转移

    1.邻居项状态转移图 邻居项主要的状态转移如下(省略邻居项垃圾回收及转移原因,更权威详细的状态转移图参看<深入理解LINUX网络技术内幕>P648 "图26-13: NUD状态间 ...

  4. SD 协议与协议栈源码分析(SD 内存卡)

    本文结合 SD Spec v2.0 和 Spec v3.0,分析了以下几个协议栈中的一些重要实现部分,由于大部分协议栈都没有实现完全,后面挑选了典型的实现进行分析, esp32 SD 协议栈 rt-t ...

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

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

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

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

  7. tcp/ip 协议栈Linux内核源码分析15 udp套接字接收流程二

    内核版本:3.4.39 上篇我们分析了UDP套接字如何接收数据的流程,最终它是在内核套接字的接收队列里取出报文,剩下的问题就是谁会去写入这个队列,当然,这部分工作由内核来完成,本篇剩下的文章主要分析内 ...

  8. LwIP 之一 源码目录文件详解及移植说明

       lwIP 是 TCP/IP 协议套件的一个小型独立实现.lwIP TCP/IP 实现的重点是减少 RAM 使用同时仍然有一个完整的 TCP. 这使得 lwIP 适合使用在具有数 10 千字节的可 ...

  9. JDK源码分析 NIO实现

    总列表:http://hg.openjdk.java.net/ 小版本:http://hg.openjdk.java.net/jdk8u jdk:http://hg.openjdk.java.net/ ...

  10. Nmap源码分析(基本框架)

    Nmap是一款非常强大的开源扫描工具.自己在使用过程中忍不住想仔细阅读一下它的源码.源码里面汇集了众多安全专家的精巧设计与优雅写法,读起来令人心旷神怡而又受益匪浅. 这里我们以阅读nmap6.0的代码 ...

最新文章

  1. 创建响应式布局的优秀网格工具集锦《系列五》
  2. 用鼠标拖动图片的JS代码
  3. 玩“剪刀石头布“的脑机!密歇根大学开发由大脑意识精密控制的假肢
  4. vue xlsx 导入导出_只需三步vue实现excel文件数据提取并存为json数据
  5. mysql 数值型注入_SQL注入之PHP-MySQL实现手工注入-数字型
  6. mysql addslashes_PHP函数addslashes和mysql_real_escape_string的区别
  7. 快速集成iOS基于RTMP的视频推流
  8. 君正4750开发板使用日记2-Linux环境搭建与内核编译
  9. RUBY常用类库文档翻译以及使用示例
  10. saas模式的外贸建站比较
  11. 立体几何相关公式推导理解(球体、台体体积)
  12. 注册表看计算机配置命令行,regedit-注册表编辑器及其命令行使用
  13. Siebel 数学运算
  14. ⭐️Python实用小工具之制作酷炫二维码(有界面、附源码)⭐️
  15. 在带有触控 ID 的妙控键盘上无法正常使用触控 ID的解决方法
  16. 两进两出热电阻信号隔离变送器
  17. kX 3552插件===优化注册表小程序
  18. 如何修改程序标题, 菜单的字体
  19. 计算机病毒中的宏病毒,意外:我该如何处理计算机中的宏病毒?
  20. 电路中输入电阻,网孔电流法,节点电压法,戴维宁定理知识点复习总结

热门文章

  1. java基于JSP+Servlet的员工绩效考核系统
  2. lisp 角平分线_清华同方mds软件下载安装 清华英泰cad mds2002
  3. 饥荒专用服务器全图显示代码,《饥荒》代码大全 控制台代码使用方法及寻找代码方法...
  4. Flutter Image图片显示
  5. office 2010 projectn visio 下载
  6. tcp/ip协议详解
  7. 三思笔记,涂抹ORACLE~~
  8. cakephp index.php,CakePHP - 中文手册
  9. 计算机的3d软件家庭版,3DOne家庭版 64位
  10. LaTex使用的一些技巧记录