首页 > 代码库 > TCP的输出

TCP的输出

    TCP段是封装在IP数据报中传输的,而IP数据报的传输是不可靠的。因此,不能将TCP段发送出去后就不再管它们了,相反必须跟踪它们,直到出现三种情况为止:一是在规定时间内接收方确认已收到该段;二是发送超时,即规定时间内未收到接收方的确认;三是确定数据包已丢失,在后两种情况下需从未接收的位置开始重新发送该数据报。


从图中可以看出TCP传输控制块中sk_write_queue字段存储的是发送队列双向链表的表头。而另外一个成员sk_send_head指向发送队列中下一个要发送的数据包,该字段是用来跟踪哪些包还未发送的,而不是用来进行发送的,如果为空,则意味着发送队列上的所有数据包都已发送过了。

在发送方从接收方接收到ACK段后,可扩大发送窗口,从sk_send_head开始遍历发送队列发送更多的段。

在TCP输出引擎中,无论是首次发送TCP段,还是重传,或是建立TCP连接时发送SYN段,都会调用tcp_transmit_skb()

1、在最上层的tcp_sendmsg()和tcp_sendpage()都是用来获取数据到SKB中的,无论数据是来自用户层还是页面缓存,最后将套接口缓存加入到传输控制块的发送队列sk_write_queue中,并在适当的时候调用tcp_write_xmit()或tcp_push_one()尽力将这些数据报发送出去。

2、在TCP接收处理ACK段的过程中,会调用tcp_data_snd_check()来检测发送队列中是否还有数据包要发送,如果有,则同样调用tcp_write_xmit()来处理发送

3、当要重传数据时,无论是超时重传还是回应收到的SACK信息,都会调用tcp_retransmit_skb()来处理重传,而该函数最终还是调用tcp_transmit_skb()来重传数据报。

当接收者发送回与发送队列sk_write_queue上的SKB项对应的ACK段,此时才能从发送队列上删除释放SKB。

TCP的输出涉及以下文件:

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

net/ipv4/tcp_ipv4.c 传输控制块与网络层之间的接口实现

net/ipv4/tcp_input.c TCP的输入

net/ipv4/tcp_output.c TCP的输出

最大段长度(MSS)

TCP提供的是一种面向连接的、可靠的字节流服务。TCP提供可靠性的一种重要的措施就是MSS。通过MSS,数据被分割成TCP认为适合发送的数据块,称为段(segment)。段不包括协议首部,只包含数据。与MSS最为相关的一个参数就是网络设备接口的MTU,以太网的MTU是1500B,其中扣除不带选项的基本IP首部和基本TCP首部长度各20B,因此MSS值可达1460B。

TCP三次握手过程中可以看到,双方都通过TCP选项通告本端能接收的MSS值,该值来源于tcp_sock结构的成员advmss,而advmss又来自路由项中的MSS度量值metrics[RTAX_MAX](参见tcp_connect_init())。路由项中的MSS度量值直接由网络设备接口的MTU减去IP首部和TCP首部计算得到的。

/* Do all connect socket setups that can be done AF independent. */
static void tcp_connect_init(struct sock *sk)
{
	struct dst_entry *dst = __sk_dst_get(sk);
	struct tcp_sock *tp = tcp_sk(sk);
	__u8 rcv_wscale;

	/* We'll fix this up when we get a response from the other end.
	 * See tcp_input.c:tcp_rcv_state_process case TCP_SYN_SENT.
	 */
	tp->tcp_header_len = sizeof(struct tcphdr) +
		(sysctl_tcp_timestamps ? TCPOLEN_TSTAMP_ALIGNED : 0);

#ifdef CONFIG_TCP_MD5SIG
	if (tp->af_specific->md5_lookup(sk, sk) != NULL)
		tp->tcp_header_len += TCPOLEN_MD5SIG_ALIGNED;
#endif

	/* If user gave his TCP_MAXSEG, record it to clamp */
	if (tp->rx_opt.user_mss)
		tp->rx_opt.mss_clamp = tp->rx_opt.user_mss;
	tp->max_window = 0;
	tcp_mtup_init(sk);
	tcp_sync_mss(sk, dst_mtu(dst));

	if (!tp->window_clamp)
		tp->window_clamp = dst_metric(dst, RTAX_WINDOW);
	tp->advmss = dst_metric(dst, RTAX_ADVMSS);
	if (tp->rx_opt.user_mss && tp->rx_opt.user_mss < tp->advmss)
		tp->advmss = tp->rx_opt.user_mss;

	tcp_initialize_rcv_mss(sk);

	tcp_select_initial_window(tcp_full_space(sk),
				  tp->advmss - (tp->rx_opt.ts_recent_stamp ? tp->tcp_header_len - sizeof(struct tcphdr) : 0),
				  &tp->rcv_wnd,
				  &tp->window_clamp,
				  sysctl_tcp_window_scaling,
				  &rcv_wscale);

	tp->rx_opt.rcv_wscale = rcv_wscale;
	tp->rcv_ssthresh = tp->rcv_wnd;

	sk->sk_err = 0;
	sock_reset_flag(sk, SOCK_DONE);
	tp->snd_wnd = 0;
	tcp_init_wl(tp, 0);
	tp->snd_una = tp->write_seq;
	tp->snd_sml = tp->write_seq;
	tp->snd_up = tp->write_seq;
	tp->rcv_nxt = 0;
	tp->rcv_wup = 0;
	tp->copied_seq = 0;

	inet_csk(sk)->icsk_rto = TCP_TIMEOUT_INIT;
	inet_csk(sk)->icsk_retransmits = 0;
	tcp_clear_retrans(tp);
}
tcp_sock成员rx_opt,为tcp_options_received结构类型,记录来自对端的TCP选项通告,其中user_mss是用户通告TCP_MAXSEG选项设置的MSS上限,它和建立连接时对端SYN段中的MSS通告(RFC1122明确说明通告MSS不包含TCP和IP选项)两者中取最小值作为该连接的MSS上限,存储在mss_clamp中。表示对端的MSS。如果没有收到来自对端通告的MSS且也没有设置user_mss,则将对端的MSS设置为默认值536B(加上首部,允许576B的IP数据报协议)。事实上,表示对端MSS的mss_clamp其初始值就定位536(tcp_v4_connect()),收到来自对端的MSS通告后,才对其进行修正。

与最大段长度有关的一个函数是tcp_current_mss(),用来计算当前有效的MSS,需要考虑TCP首部中的SACK选项和IP选项,以及PMTU。

/* Compute the current effective MSS, taking SACKs and IP options,
 * and even PMTU discovery events into account.
 */
unsigned int tcp_current_mss(struct sock *sk)
{
	struct tcp_sock *tp = tcp_sk(sk);
	struct dst_entry *dst = __sk_dst_get(sk);
	u32 mss_now;
	unsigned header_len;
	struct tcp_out_options opts;
	struct tcp_md5sig_key *md5;

	mss_now = tp->mss_cache;

	if (dst) {
		u32 mtu = dst_mtu(dst);
		if (mtu != inet_csk(sk)->icsk_pmtu_cookie)
			mss_now = tcp_sync_mss(sk, mtu);
	}

	header_len = tcp_established_options(sk, NULL, &opts, &md5) +
		     sizeof(struct tcphdr);
	/* The mss_cache is sized based on tp->tcp_header_len, which assumes
	 * some common options. If this is an odd packet (because we have SACK
	 * blocks etc) then our calculated header_len will be different, and
	 * we have to adjust mss_now correspondingly */
	if (header_len != tp->tcp_header_len) {
		int delta = (int) header_len - tp->tcp_header_len;
		mss_now -= delta;
	}

	return mss_now;
}
sendmsg系统调用在TCP中实现

sendmsg系统调用在TCP中实现共分为两层---套接口层和传输接口层,而主要的实现在传输接口层中。TCP的发送工作大部分是在传输层接口中完成的,因此整个实现过程比较复杂,涉及从用户空间复制数据到内核空间、分割TCP段等。

int tcp_sendmsg(struct kiocb *iocb, struct socket *sock, struct msghdr *msg,
		size_t size)
{
	struct sock *sk = sock->sk;
	struct iovec *iov;
	struct tcp_sock *tp = tcp_sk(sk);
	struct sk_buff *skb;
	int iovlen, flags;
	int mss_now, size_goal;
	int err, copied;
	long timeo;

	lock_sock(sk);
	TCP_CHECK_TIMER(sk);

	flags = msg->msg_flags;
	timeo = sock_sndtimeo(sk, flags & MSG_DONTWAIT);

	/* Wait for a connection to finish. */
	if ((1 << sk->sk_state) & ~(TCPF_ESTABLISHED | TCPF_CLOSE_WAIT))
		if ((err = sk_stream_wait_connect(sk, &timeo)) != 0)
			goto out_err;

	/* This should be in poll */
	clear_bit(SOCK_ASYNC_NOSPACE, &sk->sk_socket->flags);

	mss_now = tcp_send_mss(sk, &size_goal, flags);

	/* Ok commence sending. */
	iovlen = msg->msg_iovlen;
	iov = msg->msg_iov;
	copied = 0;

	err = -EPIPE;
	if (sk->sk_err || (sk->sk_shutdown & SEND_SHUTDOWN))
		goto out_err;

	while (--iovlen >= 0) {
		size_t seglen = iov->iov_len;
		unsigned char __user *from = iov->iov_base;

		iov++;

		while (seglen > 0) {
			int copy = 0;
			int max = size_goal;

			skb = tcp_write_queue_tail(sk);
			if (tcp_send_head(sk)) {
				if (skb->ip_summed == CHECKSUM_NONE)
					max = mss_now;
				copy = max - skb->len;
			}

			if (copy <= 0) {
new_segment:
				/* Allocate new segment. If the interface is SG,
				 * allocate skb fitting to single page.
				 */
				if (!sk_stream_memory_free(sk))
					goto wait_for_sndbuf;

				skb = sk_stream_alloc_skb(sk, select_size(sk),
						sk->sk_allocation);
				if (!skb)
					goto wait_for_memory;

				/*
				 * Check whether we can use HW checksum.
				 */
				if (sk->sk_route_caps & NETIF_F_ALL_CSUM)
					skb->ip_summed = CHECKSUM_PARTIAL;

				skb_entail(sk, skb);
				copy = size_goal;
				max = size_goal;
			}

			/* Try to append data to the end of skb. */
			if (copy > seglen)
				copy = seglen;

			/* Where to copy to? */
			if (skb_tailroom(skb) > 0) {
				/* We have some space in skb head. Superb! */
				if (copy > skb_tailroom(skb))
					copy = skb_tailroom(skb);
				if ((err = skb_add_data(skb, from, copy)) != 0)
					goto do_fault;
			} else {
				int merge = 0;
				int i = skb_shinfo(skb)->nr_frags;
				struct page *page = TCP_PAGE(sk);
				int off = TCP_OFF(sk);

				if (skb_can_coalesce(skb, i, page, off) &&
				    off != PAGE_SIZE) {
					/* We can extend the last page
					 * fragment. */
					merge = 1;
				} else if (i == MAX_SKB_FRAGS ||
					   (!i &&
					   !(sk->sk_route_caps & NETIF_F_SG))) {
					/* Need to add new fragment and cannot
					 * do this because interface is non-SG,
					 * or because all the page slots are
					 * busy. */
					tcp_mark_push(tp, skb);
					goto new_segment;
				} else if (page) {
					if (off == PAGE_SIZE) {
						put_page(page);
						TCP_PAGE(sk) = page = NULL;
						off = 0;
					}
				} else
					off = 0;

				if (copy > PAGE_SIZE - off)
					copy = PAGE_SIZE - off;

				if (!sk_wmem_schedule(sk, copy))
					goto wait_for_memory;

				if (!page) {
					/* Allocate new cache page. */
					if (!(page = sk_stream_alloc_page(sk)))
						goto wait_for_memory;
				}

				/* Time to copy data. We are close to
				 * the end! */
				err = skb_copy_to_page(sk, from, skb, page,
						       off, copy);
				if (err) {
					/* If this page was new, give it to the
					 * socket so it does not get leaked.
					 */
					if (!TCP_PAGE(sk)) {
						TCP_PAGE(sk) = page;
						TCP_OFF(sk) = 0;
					}
					goto do_error;
				}

				/* Update the skb. */
				if (merge) {
					skb_shinfo(skb)->frags[i - 1].size +=
									copy;
				} else {
					skb_fill_page_desc(skb, i, page, off, copy);
					if (TCP_PAGE(sk)) {
						get_page(page);
					} else if (off + copy < PAGE_SIZE) {
						get_page(page);
						TCP_PAGE(sk) = page;
					}
				}

				TCP_OFF(sk) = off + copy;
			}

			if (!copied)
				TCP_SKB_CB(skb)->flags &= ~TCPCB_FLAG_PSH;

			tp->write_seq += copy;
			TCP_SKB_CB(skb)->end_seq += copy;
			skb_shinfo(skb)->gso_segs = 0;

			from += copy;
			copied += copy;
			if ((seglen -= copy) == 0 && iovlen == 0)
				goto out;

			if (skb->len < max || (flags & MSG_OOB))
				continue;

			if (forced_push(tp)) {
				tcp_mark_push(tp, skb);
				__tcp_push_pending_frames(sk, mss_now, TCP_NAGLE_PUSH);
			} else if (skb == tcp_send_head(sk))
				tcp_push_one(sk, mss_now);
			continue;

wait_for_sndbuf:
			set_bit(SOCK_NOSPACE, &sk->sk_socket->flags);
wait_for_memory:
			if (copied)
				tcp_push(sk, flags & ~MSG_MORE, mss_now, TCP_NAGLE_PUSH);

			if ((err = sk_stream_wait_memory(sk, &timeo)) != 0)
				goto do_error;

			mss_now = tcp_send_mss(sk, &size_goal, flags);
		}
	}

out:
	if (copied)
		tcp_push(sk, flags, mss_now, tp->nonagle);
	TCP_CHECK_TIMER(sk);
	release_sock(sk);
	return copied;

do_fault:
	if (!skb->len) {
		tcp_unlink_write_queue(skb, sk);
		/* It is the one place in all of TCP, except connection
		 * reset, where we can be unlinking the send_head.
		 */
		tcp_check_send_head(sk, skb);
		sk_wmem_free_skb(sk, skb);
	}

do_error:
	if (copied)
		goto out;
out_err:
	err = sk_stream_error(sk, flags, err);
	TCP_CHECK_TIMER(sk);
	release_sock(sk);
	return err;
}
Nagle算法

为减少网络通信的开销,提升性能及吞吐速度,系统默认采用Nagle算法。若应用程序请求发送一批数据(量较大),那么系统在接收了那些数据之后,可能会延迟一段时间,等待数据积累到一定程度后一起发送出去。当然如果在规定的时间内没有新数据加入,那么原先的数据也会被发送出去。这样会使得在单个TCP段内数据量增大。与之相反的则是使用多个TCP段,使每个段负载的数据量都比较少。如果是后一种情况,那么必然会涉及一项开销,即每个段的TCP首部都要占用20B。如果假定只发送2B,那么20B的首部就显得有点多。采用Nagle算法之后,能有效地利用数据包的可用空间。该算法的另一个功能是确认消息的延迟发送。系统收到TCP数据之后,必须向对方反馈一个ACK,采用该算法后,主机会暂时等待一段时间,看是否有数据发送给对方,以便能随发送数据一起反馈ACK,从而节省一个数据包的通信量。

但在某些情况下,这样反而会产生不利影响。例如网络应用通常只需发送很少量的数据,同时要求能得到极其迅速的响应,那么再使用这种算法,反而会影响性能。Telnet便是这样的一个典型例子。Telnet的本质是一种交互式的应用,用户可通过它登录一台远程机器,然后向其传送命令。通常,用户每秒中用户只会进行少量的键击,若再使用Nagle算法,便会造成响应迟钝,甚至产生对方主机不予应答的感觉。

往返时间测量

TCP传输往返时间是指从发送方TCP段开始,到发送方接收到该段立即响应所耗费的传输时间。当接收方和发送方同时支持TCP时间戳选项时,发送记录在TCP首部选项内的时间戳会被接收方随响应反射回来,发送方就可以利用响应段反射的时间戳计算出发送段的即时往返传输时间。在接收方应答不反射时间戳的情况下,发送方利用重发队列中非重传响应所确认的最先数据片段的时间戳来取样RTT。

发送方每接收一次新的确认,都会产生一个新的RTT样本。为了避免RTT样本的随机抖动,系统利用加权平均算法对样本进行平滑。为了回避浮点运算,RTT的平滑值SRTT是实际RTT均值的8倍,迭代过程中SRTT收敛于8倍的RTT。

路径MTU发现

当网络上一台IP主机有数据要发送给另外一台IP主机时,数据最终被封装成IP数据报来传输。最理想的情况是数据报的大小是在源主机到目的主机的路径上无需分片的最大尺寸。这种数据报的尺寸称作路径MTU(PMTU),等于路径上每一条MTU中的最小值。

PMTU实现的技术简单,在IP首部中使用不分片位DF动态发现一条路径的PMTU。基本思想就是源主机一开始假定一条路径的PMTU是其已知的该路径的第一跳的MTU,在这条路径上发送的数据报都设置DF位。如果有数据报太大,不被路径中的某个路由器分片就不能转发,那么该路由器将丢弃这个数据报,然后返回一个“需要分片,但设置了DF位”的ICMP目的不可达报文。在收到这样一条报文后,源主机将减小其假定的该路径PMTU。当主机对PMTU的估计值小到其发送的数据报无需分片也能支持转发的时候,PMTU发现过程结束。

TCP的输出