TCP中的send函数---tcp_sendmsg函数的实现

当应用层程序调用send发送数据之后,相应系统调用为sys_sendmsg,在socket文件系统中,该调用指向inet_sendmsg。

不同的传输层协议inet_sendmsg的proto指向的操作也不一样,而对于TCP协议,inet_sendmsg指向tcp_sendmsg函数,

所以tcp的发送函数接口是tcp_sendmsg。在介绍tcp_sendmsg之前需要介绍tcp的发送队列,在sock结构中有两个和发送队列

有关的变量,分别为sk_write_queue和sk_send_head。sk_write_queue指向整个发送队列,其中包括了已发送未被确认的数

据以及还未发送的数据;sk_send_head指向当前要发送的数据,即下一个要发送的SKB,结构示意图如下:


上图表示当前发送队列有三个SKB,已经发送了SKB1但是还未被确认,下一个要发送的是SKB2。TCP中的最主要到发送函数

是tcp_transmit_skb,所有的SKB都经过该函数进行发送。

了解了发送队列之后再来看看tcp_sendmsg,该函数的实现如下(linux-2.6.38):

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

	//首先对sock加锁防止下半段中断访问
	lock_sock(sk);
	TCP_CHECK_TIMER(sk);

	//对于阻塞的发送模式还需设置超时时间
	flags = msg->msg_flags;
	timeo = sock_sndtimeo(sk, flags & MSG_DONTWAIT);

	//只有在ESTABLISHED和CLOSE_WAIT状态下对方才能够接收数据,尝试等待连接的建立
	if ((1 << sk->sk_state) & ~(TCPF_ESTABLISHED | TCPF_CLOSE_WAIT))
		if ((err = sk_stream_wait_connect(sk, &timeo)) != 0)
			goto out_err;

	clear_bit(SOCK_ASYNC_NOSPACE, &sk->sk_socket->flags);

	//获取当前的MSS大小
	mss_now = tcp_send_mss(sk, &size_goal, flags);

	/*iov可以看做是一个数组,iovlen表示数组的长度。每一项都是一个数据段。*/
 /*struct msghdr和struct iovec在内核的消息通信机制里很常见*/
	iovlen = msg->msg_iovlen;
	iov = msg->msg_iov;//获取第一个iovec
	copied = 0;

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

	sg = sk->sk_route_caps & NETIF_F_SG;

	/*循环操作iov,TCP是面向字节流而不是面向数据报的,可以发生粘包现象*/
	/*如果前一个SKB的小于MSS,新的数据可以将部分数据填入旧的SKB中*/
	while (--iovlen >= 0) {
		size_t seglen = iov->iov_len;					//读取该段数据的长度
		unsigned char __user *from = iov->iov_base;<span style="white-space:pre">			</span>//指向数据区域

		iov++;

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

			//得到发送队列的尾部的skb,因为尾部才可能有剩余空间
			skb = tcp_write_queue_tail(sk);				
			if (tcp_send_head(sk)) {				//判断是否还有未发送的数据,即sk_send_head是否非空,
				if (skb->ip_summed == CHECKSUM_NONE)
					max = mss_now;
				copy = max - skb->len;
			}

			//不能够填充到sk_send_head指向的SKB的话,新建一个SKB
			if (copy <= 0) {
new_segment:
				//首先判断发送队列总长度是否超过发送缓冲区上限,即sk->sk_wmem_queued < sk->sk_sndbuf
				if (!sk_stream_memory_free(sk))
					goto wait_for_sndbuf;		//设置SOCK_NOSPACE标志位后等待空间


				//分配一个新的SKB封装新的数据,失败的话也要等待空间,select_size返回一个skb连续数据域的长度,在2.6.38中返回的是0,表示数据全部存储在skb尾部的frags中
				skb = sk_stream_alloc_skb(sk,select_size(sk, sg),sk->sk_allocation);
				if (!skb)
					goto wait_for_memory;

				if (sk->sk_route_caps & NETIF_F_ALL_CSUM)
					skb->ip_summed = CHECKSUM_PARTIAL;

				skb_entail(sk, skb);					//将新的SKB插入发送队列尾部
				copy = size_goal;
				max = size_goal;
			}

			if (copy > seglen)
				copy = seglen;

			if (skb_tailroom(skb) > 0) {				//判断skb的连续数据存储区是否还有空间
				if (copy > skb_tailroom(skb))<span style="white-space:pre">			</span>//不够长的话放入部分数据
					copy = skb_tailroom(skb);
				if ((err = skb_add_data(skb, from, copy)) != 0)
					goto do_fault;
			} else {
				//这一段是将数据复制到skb尾部的skb_shared_info中的frags,比较复杂,这样做的目的是支持物理上不连续的数据,减少合并操作提高效率
				int merge = 0;
				int i = skb_shinfo(skb)->nr_frags;
				struct page *page = TCP_PAGE(sk);//返回sk最近操作的page,每个frag的数据都是放在page中
				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 || !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) { //PAGE被填满了,重置
					 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;
				 }

				 //具体的数据拷贝过程
				 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;
			}	
			//如果这一次的数据超过了最大窗口的一半,设置PUSH标志调用__tcp_push_pending_frames发送sk_send_head的所有数据
			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))	//否则如果只有当前的SKB未发送过,调用tcp_push_one发送sk_send_head的第一个数据,即当前SKB
				tcp_push_one(sk, mss_now);
				
			//上面两个判断的结果最终都会调用tcp_write_xmit函数,这个函数会从传入的sock得到sk_send_head,发送该队列上的所有数据
			//因为第二个判断只有一个SKB,所以只发送一个SKB.但是两者传入tcp_write_xmit的参数不一样。前者会发送MTU探测包,后者只发送一个数据包
			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);

		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
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值