首页 > 代码库 > TCP的输入

TCP的输入

    TCP发送方将段发送出去之后,会跟踪它们,直到得到接收方的确认为止。因此,当接收方收到一个段后,会根据情况将其添加到sk_receive_queue或prequeue,又或者sk_backlog后备队列中。

    在启用tcp_low_latency时,TCP传输控制块在软中断中接收并处理TCP段,然后将其插入到sk_receive_queue队列中,等待用户进程从接收队列中获取TCP段后复制到用户空间中,最终删除并释放。

    不启用tcp_low_latency时,能够提高TCP/IP协议栈的吞吐量及反应速度,TCP传输控制块在软中断中将TCP段添加到prequeue队列中,然后立即处理prequeue队列中的段,如果用户进程正在读取数据,则可以直接复制数据到用户空间的缓冲区中,否则添加到sk_receive_queue队列中,然后从软中断中返回。在多数情况下有机会处理prequeue队列中的段,但只有当用户进程在进行recv类系统调用返回前,才在软中断中复制数据到用户空间的缓冲区中。

   在用户进程因操作传输控制块而将其锁定时,无论是否启用tcp_low_latency,都会将未处理的TCP段添加到后备队列中,一旦用户进程解锁传输控制块,就会立即处理后备队列,将TCP段处理之后添加到sk_receive_queue队列中。

TCP接收过程中各函数调用关系如下图


TCP输入涉及以下文件:

include/linux/filter.h 定义套接口过滤相关结构和宏

include/linux/tcp.h 定义TCP段的格式、TCP传输控制块等结构、宏和函数原型

include/linux/sock.h 定义基本传输控制块结构、宏和函数原型

net/ipv4/tcp_input.c TCP的输入

net/ipv4/tcp.c 传输控制块与应用层之间的接口实现

net/core/sock.c 实现传输层通用的函数

net/core/filter.c 套接口过滤的实现

TCP接收的总入口

当IP层接收到报文,或由多个分片组装成一个完整的IP数据报之后,会调用该报文对应的传输层接收函数,传递给传输层处理。tcp_v4_rcv()是TCP接收数据的总入口。首先对TCP段进行简单的校验,如TCP首部长度、校验和等,此时还不清楚该TCP段的宿主,即还不清楚向哪个TCP传输控制块传递该段。然后根据源地址、源端口、目的地址、目的端口查找到所属的传输控制块。最后调用tcp_v4_do_rcv()将该TCP段接收到所属的传输控制块中。

/*
 *	From tcp_input.c
 */

int tcp_v4_rcv(struct sk_buff *skb)
{
	const struct iphdr *iph;
	struct tcphdr *th;
	struct sock *sk;
	int ret;
	struct net *net = dev_net(skb->dev);

	if (skb->pkt_type != PACKET_HOST)
		goto discard_it;

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

	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;
	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;

	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;

	sk = __inet_lookup_skb(&tcp_hashinfo, skb, th->source, th->dest);
	if (!sk)
		goto no_tcp_socket;

process:
	if (sk->sk_state == TCP_TIME_WAIT)
		goto do_time_wait;

	if (!xfrm4_policy_check(sk, XFRM_POLICY_IN, skb))
		goto discard_and_relse;
	nf_reset(skb);

	if (sk_filter(sk, skb))
		goto discard_and_relse;

	skb->dev = NULL;

	bh_lock_sock_nested(sk);
	ret = 0;
	if (!sock_owned_by_user(sk)) {
#ifdef CONFIG_NET_DMA
		struct tcp_sock *tp = tcp_sk(sk);
		if (!tp->ucopy.dma_chan && tp->ucopy.pinned_list)
			tp->ucopy.dma_chan = dma_find_channel(DMA_MEMCPY);
		if (tp->ucopy.dma_chan)
			ret = tcp_v4_do_rcv(sk, skb);
		else
#endif
		{
			if (!tcp_prequeue(sk, skb))
				ret = tcp_v4_do_rcv(sk, skb);
		}
	} else
		sk_add_backlog(sk, skb);
	bh_unlock_sock(sk);

	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(net, 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:
	if (!xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb)) {
		inet_twsk_put(inet_twsk(sk));
		goto discard_it;
	}

	if (skb->len < (th->doff << 2) || tcp_checksum_complete(skb)) {
		TCP_INC_STATS_BH(net, TCP_MIB_INERRS);
		inet_twsk_put(inet_twsk(sk));
		goto discard_it;
	}
	switch (tcp_timewait_state_process(inet_twsk(sk), skb, th)) {
	case TCP_TW_SYN: {
		struct sock *sk2 = inet_lookup_listener(dev_net(skb->dev),
							&tcp_hashinfo,
							iph->daddr, th->dest,
							inet_iif(skb));
		if (sk2) {
			inet_twsk_deschedule(inet_twsk(sk), &tcp_death_row);
			inet_twsk_put(inet_twsk(sk));
			sk = sk2;
			goto process;
		}
		/* Fall through to ACK */
	}
	case TCP_TW_ACK:
		tcp_v4_timewait_ack(sk, skb);
		break;
	case TCP_TW_RST:
		goto no_tcp_socket;
	case TCP_TW_SUCCESS:;
	}
	goto discard_it;
}
报文的过滤

现在,无论是PF_PACKET类型的套接口还是PF_INET类型的套接口,Linux都支持内核过滤。内核允许把过滤器直接挂接到PF_PACKET或PF_INET类型套接口的处理例程中。当确定了接收到的包所属的传输层套接口后,即调用过滤函数,TCP和UDP中过滤函数为sk_filter()

过滤器的数据结构

设置BPF过滤器是通过setsockopt调用来完成的,格式如下

setsockopt(s, SOL_SOCKET, SO_ATTACH_FILTER, &filter, sizeof(filter));

struct sock_filter	/* Filter block */
{
	__u16	code;   /* Actual filter code */
	__u8	jt;	/* Jump true */
	__u8	jf;	/* Jump false */
	__u32	k;      /* Generic multiuse field */
};

struct sock_fprog	/* Required for SO_ATTACH_FILTER. */
{
	unsigned short		len;	/* Number of filter blocks */
	struct sock_filter __user *filter;
};
实际上,可以把sock_filter结构数组看作一系列的指令集,和汇编指令很相似,原理也差不多。内核在过滤过程中,会根据一条一条执行对被过滤的包做出相应的动作。

#ifdef __KERNEL__
struct sk_filter
{
	atomic_t		refcnt;
	unsigned int         	len;	/* Number of filter blocks */
	struct rcu_head		rcu;
	struct sock_filter     	insns[0];
};
sk_filter可以说是与sock_fprog结构相对应。安装过滤器时,过滤规则会从用户空间的sk_filter复制到内核空间的sk_filter结构中。只是sk_filter结构只能在内核中使用。

/**
 *	sk_filter - run a packet through a socket filter
 *	@sk: sock associated with &sk_buff
 *	@skb: buffer to filter
 *
 * Run the filter code and then cut skb->data to correct size returned by
 * sk_run_filter. If pkt_len is 0 we toss packet. If skb->len is smaller
 * than pkt_len we keep whole skb->data. This is the socket level
 * wrapper to sk_run_filter. It returns 0 if the packet should
 * be accepted or -EPERM if the packet should be tossed.
 *
 */
int sk_filter(struct sock *sk, struct sk_buff *skb)
{
	int err;
	struct sk_filter *filter;

	err = security_sock_rcv_skb(sk, skb);
	if (err)
		return err;

	rcu_read_lock_bh();
	filter = rcu_dereference(sk->sk_filter);
	if (filter) {
		unsigned int pkt_len = sk_run_filter(skb, filter->insns,
				filter->len);
		err = pkt_len ? pskb_trim(skb, pkt_len) : -EPERM;
	}
	rcu_read_unlock_bh();

	return err;
}
ESTABLISHED状态的接收

tcp_rcv_established()是ESTABLISHED状态下的输入处理函数。为了能高效低处理接收到的段,对TCP端处理提供了两种路径:

快速路径:用于处理预期理想情形下的输入段。在正常情况下,TCP连接最常见的情形应该被尽可能地检测并最优化处理,而无需去检测一下边缘的情形。

慢速路径:用于所有和预期理想不对应的且需要进一步处理的段。如接收到的段存在除时间戳选项之外选项的段。
通过研究表明,在局域网内通过TCP连接输入的所有数据包中95%以上都执行了快速路径,而在广域网上达到了80%,因此两种路径区分处理时很有必要的。

数据从内核空间复制到用户空间

大多数的数据传输最终是要传递给用户进程的,因此最后还要涉及数据从内核空间复制到用户空间的过程。用户空间缓存由tcp_sock结构的ucopy成员描述,其类型也是由多个成员组成的结构,其中的iov是复制过程中最重要的成员,它指向描述用户进程提供的缓存区信息数组。

struct iovec
{
	void __user *iov_base;	/* BSD uses caddr_t (1003.1g requires void *) */
	__kernel_size_t iov_len; /* Must be size_t (1003.1g) */
};
/* Data for direct copy to user */
	struct {
		struct sk_buff_head	prequeue;
		struct task_struct	*task;
		struct iovec		*iov;
		int			memory;
		int			len;
#ifdef CONFIG_NET_DMA
		/* members for async copy */
		struct dma_chan		*dma_chan;
		int			wakeup;
		struct dma_pinned_list	*pinned_list;
		dma_cookie_t		dma_cookie;
#endif
	} ucopy;
iov_base指向用户空间缓冲区,iov_len是用户空间缓冲区的长度

TCP接收到数据后,如果数据经检查正常,就要把数据复制给用户进程。在多数情况下,用户进程会调用recvmsg系统调用接收数据。除此之外,还有一种情况会将接收到的数据主动复制到用户空间,那就是,TCP正在接收的段的序号与尚未从内核空间复制到用户空间的段的最前序号相等,TCP段中的用户数据长度小于用户空间缓存剩余的可使用量,且用户进程正在调用recv等系统调用从内核空间读取数据(用户进程正在睡眠),传输层被用户锁定,在这种情况下就可以直接复制数据到用户空间。

tcp_copy_to_iovec()在接收到的数据主动复制到用户空间时被调用。实际上该函数也只是对skb_copy_datagram_iovec()及skb_copy_and_csum_datagram_iovec()做了封装调用。

static int tcp_copy_to_iovec(struct sock *sk, struct sk_buff *skb, int hlen)
{
	struct tcp_sock *tp = tcp_sk(sk);
	int chunk = skb->len - hlen;
	int err;

	local_bh_enable();
	if (skb_csum_unnecessary(skb))
		err = skb_copy_datagram_iovec(skb, hlen, tp->ucopy.iov, chunk);
	else
		err = skb_copy_and_csum_datagram_iovec(skb, hlen,
						       tp->ucopy.iov);

	if (!err) {
		tp->ucopy.len -= chunk;
		tp->copied_seq += chunk;
		tcp_rcv_space_adjust(sk);
	}

	local_bh_disable();
	return err;
}
SACK信息

通过TCP通信时,如果发生序列中某个数据包丢失,TCP则会重传从最后确认的包开始的后续包,这样原先已经正确传输的包也可能重复发送,急剧降低TCP性能。为改善这种情况,发展出了SACK(selective acknowledgment,选择性确认)技术,使TCP只重新发送丢失的包,不用发送后续的所有包,并且提供相应的机制使接收方告诉发送方哪些数据丢失,哪些数据重发了,哪些数据已经提前收到等。

SACK信息是通过TCP头的选项部分提供的,信息分两种,一种标识是否支持SACK,在TCP握手时发送;另一种是具体的SACK信息。

SACK的产生

SACK通常都是由TCP接收方产生的,如果在TCP握手时接收到对方SACK允许选项,且本端也支持SACK的话,接收异常时就可以发送SACK包通知发送方。

TCP接收方接收到非期待序号的数据块时,如果该块的序号小于期待的序号,说明时网络复制或是重发的包,可以丢弃;如果收到的数据块序号大于期待的序号,说明中间有包被丢弃或延迟,这时会发送SACK通知发送方出现了网络丢包。

为反映接收方接收缓存和网络传输情况,SACK中的第一个块必须描述是哪个数据块激发了SACK选项,接收方应在SACK选项中填写尽可能多的块信息,如果空间有限不能全部写入,则报告最近接收的不连续数据块,让发送方能了解当前网络传输情况的最新信息。

发送方对SACK的响应

TCP发送方都应该维护一个未确认的重发数据队列,数据在未被确认前是不能释放的。重发队列中的每个数据块都有一个标志位SACKed,标识该数据块是否被SACK过。对于已经被SACK过的块,在重新发送数据时将被跳过。发送方接收到接收方SACK信息后,根据SACK中数据标识重发队列中相应数据块的SACKed标志。

TCP的输入