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

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

将收到的分片插入到分片队列是由函数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;
			else
				qp->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;
	else
		qp->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;
			else
				qp->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;
	else
		qp->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表的逻辑图其实就是整个重组子系统的缩影,多看看那个。 

猜你喜欢

转载自blog.csdn.net/fuyuande/article/details/86666680