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标志。

时间: 2024-11-01 16:11:07

TCP的输入的相关文章

《TCP/IP详解卷2:实现》笔记--TCP的输入

当收到的数据报的协议字段指明这是一个TCP报文段时,ipintr(通过协议协议转换表中的pr_input函数)会调用tcp_input 进行处理,tcp_inut在软件中断一级执行. 函数非常长,我们将分两张讨论,下图列出了tcp_input中的处理框架.本章将结束对RST报文段处理的讲解,下一章开始 介绍ACK报文段的处理. 头几个步骤是非常典型的:对输入报文段做有效性验证(检验和.长度等),以及寻找连接的PCB.尽管后面还有大量的 代码,但通过"首部预测",算法却有可能完全跳过后续

《TCP/IP详解卷2:实现》笔记--TCP的输入(续)

本文处理ACK标志,更新窗口信息,处理URG标志及报文段中携带的所有数据,最后处理FIN标志,如果需要,则调用 tcp_output. 1.完成被动打开和同时打开 本节介绍如何处理SYN_RCVDz状态下收到的ACK报文段.这也将完成被动打开,或者是同时打开及自连接的连接建立 过程. 1.验证收到的ACK. 2.查看窗口大小选项. 3.向应用进程提交队列中的数据. 2.快速重传和快速恢复的算法 快速重传算法用于连续出现几次(一般是3次)重复ACK时,TCP认为某个报文已丢失并且从中推断出丢失报文

[JAVA] Tcp客户端和服务器简单代码

服务器: 1 import java.io.DataInputStream; 2 import java.io.DataOutputStream; 3 import java.io.IOException; 4 import java.net.ServerSocket; 5 import java.net.Socket; 6 7 public class TcpServer { 8 public static void main(String [] args) throws IOExceptio

[安卓] 9、线程、VIEW、消息实现从TCP服务器获取数据动态加载显示

一.前言: 一般情况下从TCP服务器读取数据是放在一个线程里读的,但是刷新界面又不得不放在线程外面,所以需要用消息传递把线程里从TCP里获得的数据传送出来,然后根据数据对页面进行相应的刷新. 二.业务逻辑:   这里包含2个layout,第一个用于登陆的(即输入服务器对应的IP和端口号),点击确定进行跳转到相应的监控界面,监控界面包括加热.关闭.和显示温度3个按钮,以及一个用于绘制温度的SurfaceView. 三.详细介绍: 3-1.2个activity介绍: 登陆页面对应的activity,

TCP的定时器

TCP为每条连接建立七个定时器,依次为:连接建立定时器.重传定时器.延时ACK定时器.持续定时器.保活定时器.FIN_WAIT_2定时器和TIME_WAIT定时器.实际上,为了提高效率,内核中只使用了四个定时器来完成七个定时器的功能. TCP定时器的实现涉及以下文件: net/ipv4/tcp_timer.c TCP的定时器 net/ipv4/inet_connection_sock.c 基于连接的传输控制块实现 net/ipv4/tcp_output.c TCP的输出 net/ipv4/tcp

TCP的输出

TCP段是封装在IP数据报中传输的,而IP数据报的传输是不可靠的.因此,不能将TCP段发送出去后就不再管它们了,相反必须跟踪它们,直到出现三种情况为止:一是在规定时间内接收方确认已收到该段:二是发送超时,即规定时间内未收到接收方的确认:三是确定数据包已丢失,在后两种情况下需从未接收的位置开始重新发送该数据报. 从图中可以看出TCP传输控制块中sk_write_queue字段存储的是发送队列双向链表的表头.而另外一个成员sk_send_head指向发送队列中下一个要发送的数据包,该字段是用来跟踪哪

七LWIP学习笔记之传输控制协议(TCP)

一.协议简介 1.TCP的必要性 2.TCP的特性 3.连接的定义 4.数据流编号 5.滑动窗口 二.TCP报文 1.报文格式 2.TCP选项 3.紧急数据 4.强迫数据交互 5.报文首部数据结构 三.TCP连接 1.建立连接 2.断开连接 3.复位连接 4.TCP状态转换图 5.特殊的状态转换 四.TCP控制块 1.控制块数据结构 2.控制块链表 3.接收窗口 4.发送窗口 五.TCP编程函数 1.控制块新建 2.控制块绑定 3.控制块监听 4.控制块连接 5.发送数据 6.关闭连接 7.其他

Java Socket20140620

7.2 面向套接字编程 我们已经通过了解Socket的接口,知其所以然,下面我们就将通过具体的案例,来熟悉Socket的具体工作方式 7.2.1使用套接字实现基于TCP协议的服务器和客户机程序    依据TCP协议,在C/S架构的通讯过程中,客户端和服务器的Socket动作如下: 客户端: 1.用服务器的IP地址和端口号实例化Socket对象. 2.调用connect方法,连接到服务器上. 3.将发送到服务器的IO流填充到IO对象里,比如BufferedReader/PrintWriter. 4

atitit.http原理与概论attilax总结

1. 图解HTTP 作者:[日]上野宣 著1 2. HTTP权威指南(国内首本HTTP及其相关核心Web技术权威著作)1 3. TCP/IP详解(中文版) (共3册), 这套丛书还有 <TCP/IP详解 卷2:实现>,<TCP/IP详解 卷3:TCP事务协议.HTTP.NNTP和UNIX域协议>2 4. TCP/IP详解 卷2:实现3 5. <TCP_IP详解 卷3:TCP事务协议.HTTP.NNTP和UNIX域协议——计算机科学丛书>([美]史蒂文斯(Stevens,