linux内核协议栈 TCP层数据发送系统调用 tcp_sendmsg

目录

1 发送队列

2 tcp_sendmsg()

2.1 数据内存分配大小 select_size()

2.2 数据推送接口 tcp_push / tcp_push_one /__tcp_push_pending_frames


TCP协议发送相关的系统调用接口由很多,例如 send()、sendto()、sendmsg()和sendmmsg()。但是到了TCP协议层,都统一由内核的 tcp_sendmsg() 处理。

1 发送队列

在看tcp_sendmsg()代码之前,有必要先看下发送队列的组织和使用方式。
在这里插入图片描述
注:要特别注意的是,sk_send_head跟踪的是那些尚未发送过的数据,不包括重传数据

2 tcp_sendmsg()

该函数要完成的工作就是将应用程序要发送的数据组织成skb,然后尽可能的发出去。核心操作如下:

  1. 确定当前的MSS(PMTU的存在,该值可能是动态变化的),以及TCP可以往一个skb中填充的最大数据量size_goal(字节为单位)。这两个参数在支持TSO的情形下是不相等的,这时size_goal将会是MSS的整数倍;
  2. 数据拷贝过程分为内外两层循环,外层循环负责遍历数组(应用程序指定的数据可能不在一个连续空间中,如writev()),内层循环负责将一个数组元素内的数据拷贝完;
  3. 接下来要寻找一个skb,分两种情况:1)如果当前发送队列中最后一个skb还有空间可以继续填充数据,那么就先往该skb中填充数据;2)如果没有现成的skb可用,那么就新分配一个。判断一个skb是否还可以容纳数据的依据就是看其当前已经保存的数据是否已经超过了size_goal;
  4. 找到skb后,下一步就决定往skb的哪个区域拷贝数据,优先线性缓冲区,如果线性缓冲区没有空间了,则会往frag_list[]中放(前提是设备支持SG IO,如果设备不支持,只能重新分配skb,然后将数据拷贝到其线性缓冲区中);
  5. 拷贝过程中,如果需要会调用不同接口进行数据发送。
@msg:要发送的数据;
@size:本次要发送的数据量
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);

	//计算超时时间,如果设置了MSG_DONTWAIT标记,则超时时间为0
	flags = msg->msg_flags;
	timeo = sock_sndtimeo(sk, flags & MSG_DONTWAIT);

	//只有ESTABLISHED和CLOSE_WAIT两个状态可以发送数据,其它状态需要等待连接完成;
	//CLOSE_WAIT是收到对端FIN但是本端还没有发送FIN时所处状态,所以也可以发送数据
	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值,保存到mss_now中
	mss_now = tcp_current_mss(sk, !(flags&MSG_OOB));
	//获取一个skb可以容纳的数据量。如果不支持TSO,那么该值就是MSS,否则是MSS的整数倍
	size_goal = tp->xmit_size_goal;

	//应用要发送的数据被保存在msg中,以数组方式组织,msg_iovlen为数组大小,msg_iov为数组第一个元素
	iovlen = msg->msg_iovlen;
	iov = msg->msg_iov;
	//copied将记录本次能够写入TCP的字节数,如果成功,最终会返回给应用,初始化为0
	copied = 0;

	//检查之前TCP连接是否发生过异常
	err = -EPIPE;
	if (sk->sk_err || (sk->sk_shutdown & SEND_SHUTDOWN))
		goto do_error;

	//外层循环用来遍历msg_iov数组
	while (--iovlen >= 0) {
		//msg_iov数组中每个元素包含的数据量都可以不同,每个元素自己有多少数据量记录在自己的iov_len字段中
		int seglen = iov->iov_len;
		//from指向要拷贝的数据起点
		unsigned char __user *from = iov->iov_base;

		//iov指向下一个数组元素
		iov++;
		//内层循环用于拷贝一个数组元素
		while (seglen > 0) {
			//copy保存本轮循环要拷贝的数据量,下面会根据不同的情况计算该值
			int copy;
			//获取发送队列中最后一个数据块,因为该数据块当前已保存数据可能还没有超过
			//size_goal,所以可以继续往该数据块中填充数据
			skb = tcp_write_queue_tail(sk);

			//cond1:tcp_send_head()返回NULL表示待发送的新数为空(可能有待确认数据)
			//cond2:copy <= 0说明发送队列最后一个skb数据量也达到了size_goal,不能
			//  继续填充数据了。当两次发送之间MSS发生变化会出现小于0的情况
			
			//这两种情况中的任意一种发生都只能选择分配新的skb
			if (!tcp_send_head(sk) ||
			    (copy = size_goal - skb->len) <= 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,select_size()的返回值决定了skb的线性区域大小,见下文
				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.
				 */
				//根据硬件能力确定TCP是否需要执行校验工作
				if (sk->sk_route_caps & NETIF_F_ALL_CSUM)
					skb->ip_summed = CHECKSUM_PARTIAL;

				//将新分配的skb加入到TCB的发送队列中,并且更新相关内存记账信息
				skb_entail(sk, skb);
				//设置本轮要拷贝的数据量为size_goal,因为该skb是新分配的,所以
				//一定可以容纳这么多,但是具体能不能拷贝这么多,还需要看有没有这么
				//多的数据要发送,见下方
				copy = size_goal;
			}
			//如果skb可以容纳的数据量超过了当前数组元素中已有数据量,那么本轮只拷贝数组元素中已有的数据量
			if (copy > seglen)
				copy = seglen;

			/* Where to copy to? */
			if (skb_tailroom(skb) > 0) {
				//如果skb的线性部分还有空间,先填充这部分

				//如果线性空间部分小于当前要拷贝的数据量,则调整本轮要拷贝的数据量
				/* 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 {
				//merge用于指示是否可以将新拷贝的数据和当前skb的最后一个片段合并。如果
				//它们在页面内刚好是连续的,那么就可以合并为一个片段
				int merge = 0;
				//i为当前skb中已经存在的分片个数
				int i = skb_shinfo(skb)->nr_frags;
				//page指向上一次分配的页面,off指向该页面中的偏移量
				struct page *page = TCP_PAGE(sk);
				int off = TCP_OFF(sk);
				//该函数用于判断该skb最后一个片段是否就是当前页面的最后一部分,如果是,那么新拷贝的
				//数据和该片段就可以合并,所以设置merge为1,这样可以节省一个frag_list[]位置
				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))) {
					//如果skb中已经容纳的分片已经达到了限定值(条件1),或者网卡不支持SG IO
					//那么就不能往skb中添加分片,设置PUSH标志位,然后跳转到new_segment处,
					//然后重新分配一个skb,继续拷贝数据
					/* 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) {
					//如果上一次分配的页面已经使用完了,设定sk_sndpage为NULL
					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;
				//检查拷贝copy字节数据后是否会导致发送内存超标,如果超标需要等待内存可用
				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;
				}
				//拷贝copy字节数据到页面中
				err = skb_copy_to_page(sk, from, skb, page, off, copy);
				//拷贝失败处理
				if (err) {
					//虽然本次拷贝失败了,但是如果页面是新分配的,也不会收回了,
					//而是将其继续指派给当前TCB,这样下次发送就可以直接使用了
					if (!TCP_PAGE(sk)) {
						TCP_PAGE(sk) = page;
						TCP_OFF(sk) = 0;
					}
					goto do_error;
				}

				//更新skb中相关指针、计数信息
				if (merge) {
					//因为可以和最后一个分片合并,所以只需要更新该分片的大小即可
					skb_shinfo(skb)->frags[i - 1].size += copy;
				} else {
					//占用一个新的frag_list[]元素
					skb_fill_page_desc(skb, i, page, off, copy);
					if (TCP_PAGE(sk)) {
						//如果是旧页面,但是因为新分配了片段,所以累加对页面的引用计数
						//从这里可以看出,skb中的每个片段都会持有一个对页面的引用计数
						get_page(page);
					} else if (off + copy < PAGE_SIZE) {
						//页面是新分配的,并且本次拷贝没有将页面用完,所以持有页面的
						//引用计数,然后将页面指定给sk_sndmsg_page字段,下次可以继续使用
						get_page(page);
						TCP_PAGE(sk) = page;
					}
				}
				//设置sk_sndmsg_off的偏移量
				TCP_OFF(sk) = off + copy;
			}//end of 'else'

			//如果本轮是第一次拷贝,清除PUSH标记
			if (!copied)
				TCP_SKB_CB(skb)->flags &= ~TCPCB_FLAG_PSH;
			//write_seq记录的是发送队列中下一个要分配的序号,所以这里需要更新它
			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;
			//如果该skb没有填满,继续下一轮拷贝
			if (skb->len < size_goal || (flags & MSG_OOB))
				continue;
			//如果需要设置PUSH标志位,那么设置PUSH,然后发送数据包,可将PUSH可以让TCP尽快的发送数据
			if (forced_push(tp)) {
				tcp_mark_push(tp, skb);
				//尽可能的将发送队列中的skb发送出去,禁用nalge
				__tcp_push_pending_frames(sk, mss_now, TCP_NAGLE_PUSH);
			} else if (skb == tcp_send_head(sk))
				//当前只有这一个skb,也发送出去。因为只有一个,所以肯定也不存在拥塞,可以发送
				tcp_push_one(sk, mss_now);

			//继续拷贝数据
			continue;

wait_for_sndbuf:
			//设置套接字结构中发送缓存不足的标志
			set_bit(SOCK_NOSPACE, &sk->sk_socket->flags);
wait_for_memory:
			//如果已经有数据拷贝到了发送缓存中,那么调用tcp_push()立即发送,这样可能可以
			//让发送缓存快速的有剩余空间可用
			if (copied)
				tcp_push(sk, flags & ~MSG_MORE, mss_now, TCP_NAGLE_PUSH);
			//等待有空余内存可以使用,如果timeo不为0,那么这一步会休眠
			if ((err = sk_stream_wait_memory(sk, &timeo)) != 0)
				goto do_error;
			//睡眠后MSS可能发生了变化,所以重新计算
			mss_now = tcp_current_mss(sk, !(flags&MSG_OOB));
			size_goal = tp->xmit_size_goal;
		}//end of 'while (seglen > 0)',内层循环
	}//end of 'while (--iovlen >= 0)',外层循环

out:
	//如果拷贝了数据到发送缓存区,尝试进行一次发送
	if (copied)
		tcp_push(sk, flags, mss_now, tp->nonagle);
	TCP_CHECK_TIMER(sk);
	release_sock(sk);
	//返回本次写入的数据量
	return copied;

do_fault:
	//发生了错误,并且当前skb尚未包含任何数据,那么需要释放该skb
	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;
}

2.1 数据内存分配大小 select_size()

该函数的返回值决定了分配的skb的线性区域大小。下面重点理解TSO场景,因为目前软件可以实现GSO,所以基本上都是走这条分支。

#define MAX_TCP_HEADER	(128 + MAX_HEADER)

static inline int select_size(struct sock *sk)
{
	struct tcp_sock *tp = tcp_sk(sk);
	//不考虑任何特殊情况,返回值就是MSS值
	int tmp = tp->mss_cache;
	//如果底层支持SG IO
	if (sk->sk_route_caps & NETIF_F_SG) {
		//如果支持TSO,那么返回值就是0,这表示TSO场景,分配数据时,skb的线性区域大小将为0
		if (sk_can_gso(sk))
			tmp = 0;
		else {
			//这一部分的原理没看明白
			int pgbreak = SKB_MAX_HEAD(MAX_TCP_HEADER);

			if (tmp >= pgbreak &&
			    tmp <= pgbreak + (MAX_SKB_FRAGS - 1) * PAGE_SIZE)
				tmp = pgbreak;
		}
	}

	return tmp;
}

2.2 数据推送接口 tcp_push / tcp_push_one /__tcp_push_pending_frames

《linux内核协议栈 TCP层数据发送之发送新数》

猜你喜欢

转载自blog.csdn.net/wangquan1992/article/details/109017786
今日推荐