继续上篇,上次讲到了分片队列的查找操作,剩下的就是分片队列插入和重组两个部分了,这个也是分片重组的关键部分。

将收到的分片插入到分片队列是由函数inet_frag_queue()函数完成,这个函数比较长,多看几遍就好了 :-)

/* Add new segment to existing queue. */
/* 添加一个新的片段到分片队列里面 */
static int ip_frag_queue(struct ipq *qp, struct sk_buff *skb)
{struct sk_buff *prev, *next;struct net_device *dev;int flags, offset;int ihl, end;int err = -ENOENT;u8 ecn;/* last_in标志位已经置位,这时候再收到报文就不用处理了,* 一种情况是重组已经完成,这时候又收到了报文,可能是重传* 当然,分片队列被垃圾回收定时器回收的时候也会设置这个标志位,* 表示已废弃。*/if (qp->q.last_in & INET_FRAG_COMPLETE)goto err;/* 下面这段描述摘自 http://blog.chinaunix.net/uid-23629988-id-3047513.html* 关于ip_frag_too_far这个函数我还没有分析清楚,日后搞明白了补上,:-)* 欢迎懂得大神讲一下*    1. IPCB(skb)->flags只有在本机发送IPv4分片时被置位,那么这里的检查应该是*    预防收到本机自己发出的IP分片。*    2. 关于ip_frag_too_far:该函数主要保证了来自同一个peer(相同的源地址)不*    会占用过多的IP分片队列。*    3. 前面两个条件为真时,调用ip_frag_reinit,重新初始化该队列。出错,那么只*       好kill掉这个队列了。* */if (!(IPCB(skb)->flags & IPSKB_FRAG_COMPLETE) &&unlikely(ip_frag_too_far(qp)) &&unlikely(err = ip_frag_reinit(qp))) {ipq_kill(qp);goto err;}/* 获取ip头里面的ecn标志位 */ecn = ip4_frag_ecn(ip_hdr(skb)->tos);offset = ntohs(ip_hdr(skb)->frag_off);/* 分片标志位 */flags = offset & ~IP_OFFSET;offset &= IP_OFFSET;/* 得到片偏移位置,相对于原始未分片报文,单位为8字节 */offset <<= 3;     /* offset is in 8-byte chunks */ihl = ip_hdrlen(skb);/* Determine the position of this fragment. *//* skb的长度减去IP头就剩下数据部分长度,这个长度加上片偏移的长度* 就得到了这段报文相对于原始报文的尾偏移*/end = offset + skb->len - ihl;err = -EINVAL;/* Is this the final fragment? *//* 如果是最后的一片 */if ((flags & IP_MF) == 0) {/* If we already have some bits beyond end* or have different end, the segment is corrupted.*//* * 既然是最后一片,尾偏移肯定要大于或者等于当前分片队列的长度,不是的话就错了* * 如果已经收到过最后分片(分片重传)并且长度和当前skb所指向的尾偏移不一致,* 出错了*/if (end < qp->q.len ||((qp->q.last_in & INET_FRAG_LAST_IN) && end != qp->q.len))goto err;/* 一切正常,设置last_in 标志位,同时将分片队列长度设置上 * 只有收到了最后一个分片报文才能够得知完整的报文长度*/qp->q.last_in |= INET_FRAG_LAST_IN;qp->q.len = end;} else {/* 如果不是最后一片,并且长度不是的倍数,就截取数据到的8倍数,* 因为数据被截取了,校验和也失效了,这里重置校验和*/if (end&7) {end &= ~7;if (skb->ip_summed != CHECKSUM_UNNECESSARY)skb->ip_summed = CHECKSUM_NONE;}if (end > qp->q.len) {/* 数据的尾部超出分片队列总长,如果已经收到了最后的分片,说明出错了,直接报错。不是的话更新队列长度就好。*//* Some bits beyond end -> corruption. */if (qp->q.last_in & INET_FRAG_LAST_IN)goto err;qp->q.len = end;}}/* 说明长度为空,丢弃 */if (end == offset)goto err;err = -ENOMEM;/* 去掉IP头部  */if (pskb_pull(skb, ihl) == NULL)goto err;/* 只保留数据部分 */err = pskb_trim_rcsum(skb, end - offset);if (err)goto err;/* Find out which fragments are in front and at the back of us* in the chain of fragments so far.  We must know where to put* this fragment, right?*//* 如果是第一个分片报文则直接插入 * 如果上一个报文的偏移值小于当前偏移值则放在该报文后面即可*/ prev = qp->q.fragments_tail;if (!prev || FRAG_CB(prev)->offset < offset) {next = NULL;goto found;}/* 乱序到达的话找到它下面一个报文即可* 这里是遍历分片列表,找到当前报文的后一个*/prev = NULL;for (next = qp->q.fragments; next != NULL; next = next->next) {if (FRAG_CB(next)->offset >= offset)break;   /* bingo! */prev = next;}found:/* We found where to put this one.  Check for overlap with* preceding fragment, and, if needed, align things so that* any overlaps are eliminated.* 这时候已经找到在分片队列中的位置,需要和前后报文检查看看是否有* 数据重叠。*/if (prev) {/* i等于与上个报文重叠部分数据长度,如果完全落在上个报文内部则报错 */int i = (FRAG_CB(prev)->offset + prev->len) - offset;if (i > 0) {/* 重叠的部分直接丢弃,end <= offset说明完全重叠 */offset += i;err = -EINVAL;if (end <= offset)goto err;err = -ENOMEM;/* 去掉重叠部分 */if (!pskb_pull(skb, i))goto err;/* 数据有变更,重置校验和 */if (skb->ip_summed != CHECKSUM_UNNECESSARY)skb->ip_summed = CHECKSUM_NONE;}}err = -ENOMEM;while (next && FRAG_CB(next)->offset < end) {/* 与后面紧邻的报文重叠部分数据长度 */int i = end - FRAG_CB(next)->offset; /* overlap is 'i' bytes *//* 如果重叠长度小于后面skb的长度,那么只需要将next skb* 的长度减去重叠部分即可,同时更新偏移值和校验和*/if (i < next->len) {/* Eat head of the next overlapped fragment* and leave the loop. The next ones cannot overlap.*/if (!pskb_pull(next, i))goto err;FRAG_CB(next)->offset += i;qp->q.meat -= i;if (next->ip_summed != CHECKSUM_UNNECESSARY)next->ip_summed = CHECKSUM_NONE;break;} else {/* 走到这说明重叠长度大于next的长度,这时候next可以直接从队列中* 摘掉了。*/struct sk_buff *free_it = next;/* Old fragment is completely overridden with* new one drop it.*/next = next->next;if (prev)prev->next = next;elseqp->q.fragments = next;qp->q.meat -= free_it->len;/* 从分片队列释放该skb */frag_kfree_skb(qp->q.net, free_it);}}/* 设置该skb的控制信息,即偏移值 */FRAG_CB(skb)->offset = offset;/* Insert this fragment in the chain of fragments. *//* 插入报文,如果是最后一片则设置fragments_tail指针指向最后一片 */skb->next = next;if (!next)qp->q.fragments_tail = skb;if (prev)prev->next = skb;elseqp->q.fragments = skb;dev = skb->dev;if (dev) {/* 记录设备的索引同时清空skb的dev指针 */    qp->iif = dev->ifindex;skb->dev = NULL;}/* 更新队列的接收时间戳 * 更新队列当前收到长度和,注意meat和len区别,前者保存当前已接受部分数据长度,* 后者表示目前已知分片最大长度,当收到最后一个分片MF=0,就能够得到原始报文长度*/qp->q.stamp = skb->tstamp;qp->q.meat += skb->len;qp->ecn |= ecn;/* 增加分片内存所占空间 */atomic_add(skb->truesize, &qp->q.net->mem);/* 设置标志位 */if (offset == 0)qp->q.last_in |= INET_FRAG_FIRST_IN;if (qp->q.last_in == (INET_FRAG_FIRST_IN | INET_FRAG_LAST_IN) &&qp->q.meat == qp->q.len)/* 如果报文已经收集齐,则调用ip_frag_reasm() 进行重组操作 */    return ip_frag_reasm(qp, prev, dev);write_lock(&ip4_frags.lock);/* 移到lru末尾 */list_move_tail(&qp->q.lru_list, &qp->q.net->lru_list);write_unlock(&ip4_frags.lock);/* 分片还在继续,返回EINPROGRESS */return -EINPROGRESS;err:kfree_skb(skb);return err;
}

如果分片报文的集齐了就会调用ip_frag_rasm来重组,来看下:

/* Add new segment to existing queue. */
/* 添加一个新的片段到分片队列里面 */
static int ip_frag_queue(struct ipq *qp, struct sk_buff *skb)
{struct sk_buff *prev, *next;struct net_device *dev;int flags, offset;int ihl, end;int err = -ENOENT;u8 ecn;/* last_in标志位已经置位,这时候再收到报文就不用处理了,* 一种情况是重组已经完成,这时候又收到了报文,可能是重传* 当然,分片队列被垃圾回收定时器回收的时候也会设置这个标志位,* 表示已废弃。*/if (qp->q.last_in & INET_FRAG_COMPLETE)goto err;/* 下面这段描述摘自 http://blog.chinaunix.net/uid-23629988-id-3047513.html* 关于ip_frag_too_far这个函数我还没有分析清楚,日后搞明白了补上,:-)* 欢迎懂得大神讲一下*    1. IPCB(skb)->flags只有在本机发送IPv4分片时被置位,那么这里的检查应该是*    预防收到本机自己发出的IP分片。*    2. 关于ip_frag_too_far:该函数主要保证了来自同一个peer(相同的源地址)不*    会占用过多的IP分片队列。*    3. 前面两个条件为真时,调用ip_frag_reinit,重新初始化该队列。出错,那么只*       好kill掉这个队列了。* */if (!(IPCB(skb)->flags & IPSKB_FRAG_COMPLETE) &&unlikely(ip_frag_too_far(qp)) &&unlikely(err = ip_frag_reinit(qp))) {ipq_kill(qp);goto err;}/* 获取ip头里面的ecn标志位 */ecn = ip4_frag_ecn(ip_hdr(skb)->tos);offset = ntohs(ip_hdr(skb)->frag_off);/* 分片标志位 */flags = offset & ~IP_OFFSET;offset &= IP_OFFSET;/* 得到片偏移位置,相对于原始未分片报文,单位为8字节 */offset <<= 3;     /* offset is in 8-byte chunks */ihl = ip_hdrlen(skb);/* Determine the position of this fragment. *//* skb的长度减去IP头就剩下数据部分长度,这个长度加上片偏移的长度* 就得到了这段报文相对于原始报文的尾偏移*/end = offset + skb->len - ihl;err = -EINVAL;/* Is this the final fragment? *//* 如果是最后的一片 */if ((flags & IP_MF) == 0) {/* If we already have some bits beyond end* or have different end, the segment is corrupted.*//* * 既然是最后一片,尾偏移肯定要大于或者等于当前分片队列的长度,不是的话就错了* * 如果已经收到过最后分片(分片重传)并且长度和当前skb所指向的尾偏移不一致,* 出错了*/if (end < qp->q.len ||((qp->q.last_in & INET_FRAG_LAST_IN) && end != qp->q.len))goto err;/* 一切正常,设置last_in 标志位,同时将分片队列长度设置上 * 只有收到了最后一个分片报文才能够得知完整的报文长度*/qp->q.last_in |= INET_FRAG_LAST_IN;qp->q.len = end;} else {/* 如果不是最后一片,并且长度不是的倍数,就截取数据到的8倍数,* 因为数据被截取了,校验和也失效了,这里重置校验和*/if (end&7) {end &= ~7;if (skb->ip_summed != CHECKSUM_UNNECESSARY)skb->ip_summed = CHECKSUM_NONE;}if (end > qp->q.len) {/* 数据的尾部超出分片队列总长,如果已经收到了最后的分片,说明出错了,直接报错。不是的话更新队列长度就好。*//* Some bits beyond end -> corruption. */if (qp->q.last_in & INET_FRAG_LAST_IN)goto err;qp->q.len = end;}}/* 说明长度为空,丢弃 */if (end == offset)goto err;err = -ENOMEM;/* 去掉IP头部  */if (pskb_pull(skb, ihl) == NULL)goto err;/* 只保留数据部分 */err = pskb_trim_rcsum(skb, end - offset);if (err)goto err;/* Find out which fragments are in front and at the back of us* in the chain of fragments so far.  We must know where to put* this fragment, right?*//* 如果是第一个分片报文则直接插入 * 如果上一个报文的偏移值小于当前偏移值则放在该报文后面即可*/ prev = qp->q.fragments_tail;if (!prev || FRAG_CB(prev)->offset < offset) {next = NULL;goto found;}/* 乱序到达的话找到它下面一个报文即可* 这里是遍历分片列表,找到当前报文的后一个*/prev = NULL;for (next = qp->q.fragments; next != NULL; next = next->next) {if (FRAG_CB(next)->offset >= offset)break;   /* bingo! */prev = next;}found:/* We found where to put this one.  Check for overlap with* preceding fragment, and, if needed, align things so that* any overlaps are eliminated.* 这时候已经找到在分片队列中的位置,需要和前后报文检查看看是否有* 数据重叠。*/if (prev) {/* i等于与上个报文重叠部分数据长度,如果完全落在上个报文内部则报错 */int i = (FRAG_CB(prev)->offset + prev->len) - offset;if (i > 0) {/* 重叠的部分直接丢弃,end <= offset说明完全重叠 */offset += i;err = -EINVAL;if (end <= offset)goto err;err = -ENOMEM;/* 去掉重叠部分 */if (!pskb_pull(skb, i))goto err;/* 数据有变更,重置校验和 */if (skb->ip_summed != CHECKSUM_UNNECESSARY)skb->ip_summed = CHECKSUM_NONE;}}err = -ENOMEM;while (next && FRAG_CB(next)->offset < end) {/* 与后面紧邻的报文重叠部分数据长度 */int i = end - FRAG_CB(next)->offset; /* overlap is 'i' bytes *//* 如果重叠长度小于后面skb的长度,那么只需要将next skb* 的长度减去重叠部分即可,同时更新偏移值和校验和*/if (i < next->len) {/* Eat head of the next overlapped fragment* and leave the loop. The next ones cannot overlap.*/if (!pskb_pull(next, i))goto err;FRAG_CB(next)->offset += i;qp->q.meat -= i;if (next->ip_summed != CHECKSUM_UNNECESSARY)next->ip_summed = CHECKSUM_NONE;break;} else {/* 走到这说明重叠长度大于next的长度,这时候next可以直接从队列中* 摘掉了。*/struct sk_buff *free_it = next;/* Old fragment is completely overridden with* new one drop it.*/next = next->next;if (prev)prev->next = next;elseqp->q.fragments = next;qp->q.meat -= free_it->len;/* 从分片队列释放该skb */frag_kfree_skb(qp->q.net, free_it);}}/* 设置该skb的控制信息,即偏移值 */FRAG_CB(skb)->offset = offset;/* Insert this fragment in the chain of fragments. *//* 插入报文,如果是最后一片则设置fragments_tail指针指向最后一片 */skb->next = next;if (!next)qp->q.fragments_tail = skb;if (prev)prev->next = skb;elseqp->q.fragments = skb;dev = skb->dev;if (dev) {/* 记录设备的索引同时清空skb的dev指针 */    qp->iif = dev->ifindex;skb->dev = NULL;}/* 更新队列的接收时间戳 * 更新队列当前收到长度和,注意meat和len区别,前者保存当前已接受部分数据长度,* 后者表示目前已知分片最大长度,当收到最后一个分片MF=0,就能够得到原始报文长度*/qp->q.stamp = skb->tstamp;qp->q.meat += skb->len;qp->ecn |= ecn;/* 增加分片内存所占空间 */atomic_add(skb->truesize, &qp->q.net->mem);/* 设置标志位 */if (offset == 0)qp->q.last_in |= INET_FRAG_FIRST_IN;if (qp->q.last_in == (INET_FRAG_FIRST_IN | INET_FRAG_LAST_IN) &&qp->q.meat == qp->q.len)/* 如果报文已经收集齐,则调用ip_frag_reasm() 进行重组操作 */    return ip_frag_reasm(qp, prev, dev);write_lock(&ip4_frags.lock);/* 移到lru末尾 */list_move_tail(&qp->q.lru_list, &qp->q.net->lru_list);write_unlock(&ip4_frags.lock);/* 分片还在继续,返回EINPROGRESS */return -EINPROGRESS;err:kfree_skb(skb);return err;
}/* Build a new IP datagram from all its fragments. */
/* 分片重组 */
static int ip_frag_reasm(struct ipq *qp, struct sk_buff *prev,struct net_device *dev)
{struct net *net = container_of(qp->q.net, struct net, ipv4.frags);struct iphdr *iph;struct sk_buff *fp, *head = qp->q.fragments;int len;int ihlen;int err;u8 ecn;/* 重组之前首先将分片队列从分片子系统隔离开 */ipq_kill(qp);/* 检查ecn标志位,0xff则丢弃该报文,之所以这么做是因为rfc文档建议这么做 */ecn = ip4_frag_ecn_table[qp->ecn];if (unlikely(ecn == 0xff)) {err = -EINVAL;goto out_fail;}/* Make the one we just received the head. *//* 这一步是将最后接收到的skb指针指向分片队列的首部接收完最后一片后重组完成* 是要将skb传递给上层处理的。这一段代码貌似复杂,多看几遍就懂了,下面放上一个* 简要的图。*/if (prev) {head = prev->next;fp = skb_clone(head, GFP_ATOMIC);if (!fp)goto out_nomem;fp->next = head->next;if (!fp->next)qp->q.fragments_tail = fp;prev->next = fp;/* skb_morph 作用基本和skb_clone一致,这里的作用是* 将刚收到的指针指向分片队列首部,fragments就是分片* 首部。*/skb_morph(head, qp->q.fragments);head->next = qp->q.fragments->next;kfree_skb(qp->q.fragments);qp->q.fragments = head;}WARN_ON(head == NULL);WARN_ON(FRAG_CB(head)->offset != 0);/* Allocate a new buffer for the datagram. */ihlen = ip_hdrlen(head);len = ihlen + qp->q.len;/* ip报文最大65535字节,超过这个长度就报错 */err = -E2BIG;if (len > 65535)goto out_oversize;/* Head of list must not be cloned. */if (skb_cloned(head) && pskb_expand_head(head, 0, 0, GFP_ATOMIC))goto out_nomem;/* If the first fragment is fragmented itself, we split* it to two chunks: the first with data and paged part* and the second, holding only fragments. *//**通常SKB数据区会由线性缓存和非线性缓存组成,超过MTU大小就要使用* 另外的skb来存储,这个部分放在skb_shinfo(head)->frag_list里。* 分片队列重组完成后也是把原来的一个个分片放到skb_shinfo(head)->frag_list里,* 所以这里为了避免和head原有的frag_list弄混(如果head存在frag_list),将head的数据分为* 两个部分,head存储线性和非线性数据区,clone指向head的原有frag_list,同时再将分片队列* 里的skb挂到clone后,这样后续的上层处理就非常简单。*/if (skb_has_frag_list(head)) {struct sk_buff *clone;int i, plen = 0;/* 创建一个线性数据区长度为0的skb */if ((clone = alloc_skb(0, GFP_ATOMIC)) == NULL)goto out_nomem;clone->next = head->next;head->next = clone;/* 继承head的frag_list */skb_shinfo(clone)->frag_list = skb_shinfo(head)->frag_list;/* 将head的frag_list指针 重置 */skb_frag_list_init(head);for (i = 0; i < skb_shinfo(head)->nr_frags; i++)plen += skb_frag_size(&skb_shinfo(head)->frags[i]);clone->len = clone->data_len = head->data_len - plen;head->data_len -= clone->len;head->len -= clone->len;clone->csum = 0;clone->ip_summed = head->ip_summed;atomic_add(clone->truesize, &qp->q.net->mem);}/* 再将所有的分片挂到frag_list队列上 */skb_shinfo(head)->frag_list = head->next;/* 指针指向传输层首部 */skb_push(head, head->data - skb_network_header(head));/* 处理校验和 */for (fp=head->next; fp; fp = fp->next) {head->data_len += fp->len;head->len += fp->len;if (head->ip_summed != fp->ip_summed)head->ip_summed = CHECKSUM_NONE;else if (head->ip_summed == CHECKSUM_COMPLETE)head->csum = csum_add(head->csum, fp->csum);head->truesize += fp->truesize;}/* 重组完成,从分片占用的系统内存中减去重组后大小 */atomic_sub(head->truesize, &qp->q.net->mem);head->next = NULL;head->dev = dev;head->tstamp = qp->q.stamp;/* 重置IP头 */iph = ip_hdr(head);iph->frag_off = 0;iph->tot_len = htons(len);iph->tos |= ecn;IP_INC_STATS_BH(net, IPSTATS_MIB_REASMOKS);qp->q.fragments = NULL;qp->q.fragments_tail = NULL;return 0;out_nomem:LIMIT_NETDEBUG(KERN_ERR pr_fmt("queue_glue: no memory for gluing queue %p\n"),qp);err = -ENOMEM;goto out_fail;
out_oversize:if (net_ratelimit())pr_info("Oversized IP packet from %pI4\n", &qp->saddr);
out_fail:IP_INC_STATS_BH(net, IPSTATS_MIB_REASMFAILS);return err;
}

下面贴一张图,主要处理是将收到的分片指针指向分片队列首部,因为重组完成后就会把重组好的报文还给协议栈继续处理,这时候分片skb指针将由原先的skb分片指向重组skb首部

IPv4分片重组就是以上这些内容,代码虽然很多但是逻辑不是很复杂,只要理解了分片队列、垃圾回收队列(lru队列)的组织结构再结合具体的代码分析就能够搞清了。第一篇博客给的那张关于分片队列、哈希表的、lru表的逻辑图其实就是整个重组子系统的缩影,多看看那个。

tcp/ip 协议栈Linux源码分析三 IPv4分片报文重组分析三相关推荐

  1. tcp/ip 协议栈Linux源码分析一 IPv4分片报文重组分析一

    内核版本:3.4.39 之前因工作原因接触到了IPv4 报文重组这个话题,一直以来对这个重组流程不是很清楚,所以很多功能的实现都避开了分片报文的处理,一方面是因为重组比较复杂,另一方面是经验不多无从下 ...

  2. tcp/ip 协议栈Linux源码分析二 IPv4分片报文重组分析二

    继续接着上篇讲,之前我们说过,收到分片报文后首先会检查分片报文所占内存是否过大,如果超过阈值的话就要调用ip_evictor函数去释放一些旧的分片队列,关于如何释放分片队列资源上一篇已经总结完成,接下 ...

  3. tcp/ip 协议栈Linux源码分析五 IPv6分片报文重组分析一

    做防火墙模块的时候遇到过IPv6分片报文处理的问题是,当时的问题是netfilter无法基于传输层的端口拦截IPv6分片报文,但是IPv4的分片报文可以.分析了内核源码得知是因为netfilter的连 ...

  4. tcp/ip 协议栈Linux源码分析四 IPv4分片 ip_fragment函数分析

    内核版本:3.4.39 很多项目涉及到IP分片的时候都是绕过去了,感觉分片挺难的.但是老这么做也不行啊,抽空分析了内核的分片处理函数ip_fragment,也不是特别复杂,感觉挺简单的,看来事情只有实 ...

  5. linux源码分析之cpu初始化 kernel/head.s,linux源码分析之cpu初始化

    linux源码分析之cpu初始化 kernel/head.s 收藏 来自:http://blog.csdn.net/BoySKung/archive/2008/12/09/3486026.aspx l ...

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

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

  7. TCP拥塞控制算法BBR源码分析

      BBR是谷歌与2016年提出的TCP拥塞控制算法,在Linux4.9的patch中正式加入.该算法一出,瞬间引起了极大的轰动.在CSDN上也有众多大佬对此进行分析讨论,褒贬不一.   本文首先对源 ...

  8. Spring源码分析系列——bean创建过程分析(三)——工厂方法创建bean

    前言 spring创建bean的方式 测试代码准备 createBeanInstance()方法分析 instantiateUsingFactoryMethod()方法分析 总结 spring创建be ...

  9. tcp/ip 协议栈Linux内核源码分析12 udp套接字发送流程一

    内核版本:3.4.39 因为过往的开发工作中既包括内核网络层模块的开发,又包括应用层程序的开发,所以对于网络数据的通信有那么一些了解.但是对于网络通信过程中,内核和应用层之间接口是如何运作的不是很清楚 ...

最新文章

  1. 利用Nginx实现简易负载均衡
  2. pytorch中lstm学习
  3. 编写测试用例的方法_适合小白的测试用例编写方法,一看就会!
  4. manual php,PHP - Manual: 介绍 (官方文档)
  5. Windows sever 2008 动态硬盘数据恢复
  6. spring-core
  7. 前端学习(1742):前端调试值之快速调试和修改
  8. 在Android上可视化TensorFlow Lite AI结果
  9. checkboxlist详细用法、checkboxlist用法、checkboxlist
  10. MySQL优化SQL性能问题
  11. 部署http+svn,yum安装svn 1.9版本
  12. 怎样自制微信gif动态表情包?
  13. 计算机的硬盘如何查看,怎样查看电脑硬盘信息 电脑中的硬盘信息
  14. excel保存快捷键_实例演示:Excel表格数据很少但文件很大,教你如何瘦身缩小...
  15. 计算机主板品牌排行榜,电脑主板十大品牌排行榜
  16. Ubuntu 中文字体美化方案大全 (1): 概述篇
  17. SpringBoot-RSA加密
  18. 国内linux内核代码贡献率,[图]AMD为Linux内核贡献27.5万行代码 确认Van Gogh APU支持DDR5和VCN3...
  19. navicat导出数据库数据
  20. URL must not be null

热门文章

  1. html 商品展示框
  2. 黑客发现了欺骗华尔街的新方法
  3. ”A page can have only one server-side Form tag“错误
  4. vba传值调用_小白关于VBA调用Sub传递参数之传值与传址的思考
  5. 模拟退火算法SA参数设置实验记录
  6. 广西计算机电缆线报价,广西壮族自治区耐火计算机电缆JYPVP32哪里便宜
  7. 关键词热度分析工具_干货分享丨关键词热度分析工具
  8. c语言十万以内超级素数,用C语言求素数的优化
  9. mysql使用释放资源_数据库--释放mysql数据库资源
  10. 【Paper】2014_基于自适应定位的传感器频率的对比研究