TCP数据接收之入口

本文详细介绍了TCP数据接收的过程,重点分析了接收入口函数tcp_v4_rcv(),以及数据如何进入prequeue、backlog和receive队列。探讨了TCP接收涉及的三个队列的作用,强调理解其设计意图对于TCP协议理解的重要性。
摘要由CSDN通过智能技术生成

IP层组合出一包数据后,如果数据包首部的协议字段表明上层协议为TCP,则调用TCP的tcp_v4_rcv()函数将数据传递给传输层继续处理,传输层的整体处理过程是非常复杂的,这篇笔记就先来看看传输层的入口处是如何处理的。

1. 三个队列

TCP对输入数据包的整体处理流程可以简单的用下图表达:
在这里插入图片描述

从上图中可以看到TCP的接收过程会涉及到三个队列:prequeue队列、receive队列以及backlog队列,这里首先介绍下这三个队列的作用,然后再跟踪源代码实现。
从数据接收的角度考虑,可以将TCP的传输控制块的状态分为如下三种:

  1. 用户进程正在读写数据,此时TCB是被锁定的
  2. 用户进程正在读写数据,但是因为没有数据可用而进入了休眠态,等待数据可用,这时TCB是不会被用户进程锁定的
  3. 用户进程根本就没有在读写数据,当然这时TCB也不会被用户进程锁定

再考虑一点,由于协议栈对输入数据包的处理实际上都是软中断中进行的,出于性能的考虑,我们总是期望软中断能够快速的结束。

这样,再来理解上图:

  • 如果被用户进程锁定,那么处于情形一,此时由于互斥,没得选,为了能快速结束软中断处理,将数据包放入到backlog队列中,这类数据包的真正处理是在用户进程释放TCB时进行的;
  • 如果没有被进程锁定,那么首先尝试将数据包放入prequeue队列,原因还是为了尽快让软中断结束,这种数据包的处理是在用户进程读数据过程中处理的;
  • 如果没有被进程锁定,prequeue队列也没有接受该数据包(出于性能考虑,比如prequeue队列不能无限制增大),那么没有更好的办法了,必须在软中断中对数据包进行处理,处理完毕后将数据包加入到receive队列中。

综上,可以总结如下:

  • 放入receive队列的数据包都是已经被TCP处理过的数据包,比如校验、回ACK等动作都已经完成了,这些数据包等待用户空间程序读即可;相反,放入backlog队列和prequeue队列的数据包都还需要TCP处理,实际上,这些数据包也都是在合适的时机通过tcp_v4_do_rcv()处理的;
  • 设计三个队列是有其特殊目的的,理解背后的设计意图非常重要。

2. 接收入口:tcp_v4_rcv()

tcp_v4_rcv()是TCP协议的接收入口函数。

int tcp_v4_rcv(struct sk_buff *skb)
{
	const struct iphdr *iph;
	struct tcphdr *th;
	struct sock *sk;
	int ret;
	//非本机数据包扔掉
	if (skb->pkt_type != PACKET_HOST)
		goto discard_it;

	/* Count it even if it's bad */
	TCP_INC_STATS_BH(TCP_MIB_INSEGS);

	//下面主要是对TCP段的长度进行校验。注意pskb_may_pull()除了校验,还有一个额外的功能,
	//如果一个TCP段在传输过程中被网络层分片,那么在目的端的网络层会重新组包,这会导致传给
	//TCP的skb的分片结构中包含多个skb,这种情况下,该函数会将分片结构重组到线性数据区

	//保证skb的线性区域至少有20个字节数据
	if (!pskb_may_pull(skb, sizeof(struct tcphdr)))
		goto discard_it;

	th = tcp_hdr(skb);

	if (th->doff < sizeof(struct tcphdr) / 4)
		goto bad_packet;
	//保证skb的线性区域至少包括实际的TCP首部
	if (!pskb_may_pull(skb, th->doff * 4))
		goto discard_it;

	//数据包校验相关,校验失败,则悄悄丢弃,不产生任何的差错报文
	/* An explanation is required here, I think.
	 * Packet length and doff are validated by header prediction,
	 * provided case of th->doff==0 is eliminated.
	 * So, we defer the checks. */
	if (!skb_csum_unnecessary(skb) && tcp_v4_checksum_init(skb))
		goto bad_packet;

	//初始化skb中的控制块
	th = tcp_hdr(skb);
	iph = ip_hdr(skb);
	TCP_SKB_CB(skb)->seq = ntohl(th->seq);
	TCP_SKB_CB(skb)->end_seq = (TCP_SKB_CB(skb)->seq + th->syn + th->fin +
				    skb->len - th->doff * 4);
	TCP_SKB_CB(skb)->ack_seq = ntohl(th->ack_seq);
	TCP_SKB_CB(skb)->when	 = 0;
	TCP_SKB_CB(skb)->flags	 = iph->tos;
	TCP_SKB_CB(skb)->sacked	 = 0;

	//根据传入段的源和目的地址信息从ehash或者bhash中查询对应的TCB,这一步决定了
	//输入数据包应该由哪个套接字处理,获取到TCB时,还会持有一个引用计数
	sk = __inet_lookup(skb->dev->nd_net, &tcp_hashinfo, iph->saddr,
			th->source, iph->daddr, th->dest, inet_iif(skb));
	if (!sk)
		goto no_tcp_socket;

process:
	//TCP_TIME_WAIT需要做特殊处理,这里先不关注
	if (sk->sk_state == TCP_TIME_WAIT)
		goto do_time_wait;
	//IPSec相关
	if (!xfrm4_policy_check(sk, XFRM_POLICY_IN, skb))
		goto discard_and_relse;
	nf_reset(skb);
	//TCP套接字过滤器,如果数据包被过滤掉了,结束处理过程
	if (sk_filter(sk, skb))
		goto discard_and_relse;
	//到了传输层,该字段已经没有意义,将其置为空
	skb->dev = NULL;

	//先持锁,这样进程上下文和其它软中断则无法操作该TCB
	bh_lock_sock_nested(sk);
	ret = 0;
    //如果当前TCB没有被进程上下文锁定,首先尝试将数据包放入prequeue队列,
	//如果prequeue队列没有处理,再将其处理后放入receive队列。如果TCB已
	//经被进程上下文锁定,那么直接将数据包放入backlog队列
	if (!sock_owned_by_user(sk)) {
    //DMA部分,忽略
#ifdef CONFIG_NET_DMA
		struct tcp_sock *tp = tcp_sk(sk);
		if (!tp->ucopy.dma_chan && tp->ucopy.pinned_list)
			tp->ucopy.dma_chan = get_softnet_dma();
		if (tp->ucopy.dma_chan)
			ret = tcp_v4_do_rcv(sk, skb);
		else
#endif
		{
        	//prequeue没有接收该数据包时返回0,那么交由tcp_v4_do_rcv()处理
			if (!tcp_prequeue(sk, skb))
				ret = tcp_v4_do_rcv(sk, skb);
		}
	} else {
    	//TCB被用户进程锁定,直接将数据包放入backlog队列
		sk_add_backlog(sk, skb);
    }
	//释放锁
	bh_unlock_sock(sk);
	//释放TCB引用计数
	sock_put(sk);
	//返回处理结果
	return ret;

no_tcp_socket:
	if (!xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb))
		goto discard_it;

	if (skb->len < (th->doff << 2) || tcp_checksum_complete(skb)) {
bad_packet:
		TCP_INC_STATS_BH(TCP_MIB_INERRS);
	} else {
		tcp_v4_send_reset(NULL, skb);
	}

discard_it:
	/* Discard frame. */
	kfree_skb(skb);
	return 0;

discard_and_relse:
	sock_put(sk);
	goto discard_it;

do_time_wait:
...
}

2.1 接收到prequeue队列

/* Packet is added to VJ-style prequeue for processing in process
 * context, if a reader task is waiting. Apparently, this exciting
 * idea (VJ's mail "Re: query about TCP header on tcp-ip" of 07 Sep 93)
 * failed somewhere. Latency? Burstiness? Well, at least now we will
 * see, why it failed. 8)8)				  --ANK
 *
 * NOTE: is this not too big to inline?
 */
static inline int tcp_prequeue(struct sock *sk, struct sk_buff *skb)
{
	struct tcp_sock *tp = tcp_sk(sk);
	//sysctl_tcp_low_latency(/proc/net/ipv4/tcp_low_latency)系统参数的含义是
	//“是否启动tcp低时延”,如果启用则为1,否则为0(默认)

	//tp->ucopy.task不为空,表示有进程正阻塞到该套接字上等待数据可用,所以,下面这两
	//个条件表示没有启动TCP低时延并且当前有进程在等待数据时,则把数据包放入prequeue队列

    //为什么放入prequeue队列就增加了tcp时延也非常好理解,因为放入prequeue队列的数据
	//包实际上会被延迟处理,也就会延迟给对端回复ACK,所以增加了时延
	if (!sysctl_tcp_low_latency && tp->ucopy.task) {
		__skb_queue_tail(&tp->ucopy.prequeue, skb);
		tp->ucopy.memory += skb->truesize;
        //为了防止prequeue队列无线增大,这里设置了门限,超过了该门限,
		//则直接在这里处理prequeue队列中的数据包
		if (tp->ucopy.memory > sk->sk_rcvbuf) {
			struct sk_buff *skb1;
			BUG_ON(sock_owned_by_user(sk));
			while ((skb1 = __skb_dequeue(&tp->ucopy.prequeue)) != NULL) {
				sk->sk_backlog_rcv(sk, skb1);
				NET_INC_STATS_BH(LINUX_MIB_TCPPREQUEUEDROPPED);
			}
			tp->ucopy.memory = 0;
		} else if (skb_queue_len(&tp->ucopy.prequeue) == 1) {
        	//这里是另外一种情况,当prequeue队列由空变为不空时,唤醒等待进程,
			//让等待进程有机会快速处理prequeue队列
			wake_up_interruptible(sk->sk_sleep);
			//延迟确认相关
			if (!inet_csk_ack_scheduled(sk))
				inet_csk_reset_xmit_timer(sk, ICSK_TIME_DACK,
						          (3 * TCP_RTO_MIN) / 4,
							  TCP_RTO_MAX);
		}
		return 1;
	}
	return 0;
}

2.1.2 接收到backlog队列

就是直接将数据包加入到传输控制块的后备队列中,是简单的双向循环链表插入操作。

/* The per-socket spinlock must be held here. */
static inline void sk_add_backlog(struct sock *sk, struct sk_buff *skb)
{
	if (!sk->sk_backlog.tail) {
		sk->sk_backlog.head = sk->sk_backlog.tail = skb;
	} else {
		sk->sk_backlog.tail->next = skb;
		sk->sk_backlog.tail = skb;
	}
	skb->next = NULL;
}

2.1.3 接收到receive队列

该函数完成TCP对一个数据包的接收处理,然后将处理后的数据包放入receive队列(如果有数据的话)。实际上,prequeue和backlog队列中skb的处理最终也都是调用的该函数,这在tcp_recvmsg()的处理过程中可以清晰看到。

该函数还只是简单地根据TCB的状态做简单的区分处理,相关内容在其它笔记中会单独介绍。

/* The socket must have it's spinlock held when we get
 * here.
 *
 * We have a potential double-lock case here, so even when
 * doing backlog processing we use the BH locking scheme.
 * This is because we cannot sleep with the original spinlock
 * held.
 */
int tcp_v4_do_rcv(struct sock *sk, struct sk_buff *skb)
{
	struct sock *rsk;
#ifdef CONFIG_TCP_MD5SIG
	/*
	 * We really want to reject the packet as early as possible
	 * if:
	 *  o We're expecting an MD5'd packet and this is no MD5 tcp option
	 *  o There is an MD5 option and we're not expecting one
	 */
	if (tcp_v4_inbound_md5_hash(sk, skb))
		goto discard;
#endif
	//连接态的数据包由tcp_rcv_established()处理
	if (sk->sk_state == TCP_ESTABLISHED) { /* Fast path */
		TCP_CHECK_TIMER(sk);
		if (tcp_rcv_established(sk, skb, tcp_hdr(skb), skb->len)) {
			rsk = sk;
			goto reset;
		}
		TCP_CHECK_TIMER(sk);
		return 0;
	}

	//再次检查头部长度,并完成校验
	if (skb->len < tcp_hdrlen(skb) || tcp_checksum_complete(skb))
		goto csum_err;

	//LISTEN状态数据包处理过程,见连接建立过程分析
	if (sk->sk_state == TCP_LISTEN) {
		struct sock *nsk = tcp_v4_hnd_req(sk, skb);
		if (!nsk)
			goto discard;

		if (nsk != sk) {
			if (tcp_child_process(sk, nsk, skb)) {
				rsk = nsk;
				goto reset;
			}
			return 0;
		}
	}

	//其它TCP状态到达的数据包都由tcp_rcv_state_process处理
	TCP_CHECK_TIMER(sk);
	if (tcp_rcv_state_process(sk, skb, tcp_hdr(skb), skb->len)) {
		rsk = sk;
		goto reset;
	}
	TCP_CHECK_TIMER(sk);
	return 0;

reset:
	tcp_v4_send_reset(rsk, skb);
discard:
	kfree_skb(skb);
	/* Be careful here. If this function gets more complicated and
	 * gcc suffers from register pressure on the x86, sk (in %ebx)
	 * might be destroyed here. This current version compiles correctly,
	 * but you have been warned.
	 */
	return 0;

csum_err:
	TCP_INC_STATS_BH(TCP_MIB_INERRS);
	goto discard;
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值