zoukankan      html  css  js  c++  java
  • Linux2.6内核协议栈系列--TCP协议1.发送

    在介绍tcp发送函数之前得先介绍很关键的一个结构sk_buff,在linux中,sk_buff结构代表了一个报文:

    然后见发送函数源码,这里不关注硬件支持的分散-聚集:

    /* sendmsg系统调用在TCP层的实现 */
    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;
    	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,如果支持OOB,则不能支持TSO,MSS则应当是比较小的值 */
    	mss_now = tcp_current_mss(sk, !(flags&MSG_OOB));
    
    	/* Ok commence sending. */
    	/* 获取待发送缓冲区数组指针及其长度 */
    	iovlen = msg->msg_iovlen;
    	iov = msg->msg_iov;
    	/* copied表示从用户数据块复制到skb中的字节数。 */
    	copied = 0;
    
    	err = -EPIPE;
    	/* 如果套接口存在错误,则不允许发送数据,返回EPIPE错误 */
    	if (sk->sk_err || (sk->sk_shutdown & SEND_SHUTDOWN))
    		goto do_error;
    
    	while (--iovlen >= 0) {/* 处理所有待发送数据块 */
    		/*要分段缓冲区的指针和长度*/
    		int seglen = iov->iov_len;
    		unsigned char __user *from = iov->iov_base;
    
    		iov++;
    
    		while (seglen > 0) {/* 处理单个数据块中的所有数据 */
    			int copy;
    			/*取传输队列的上一个sk_buff指针,这下面一小段的目的是检查
    			传输队列是否有未满段,未满段是当前分段长度小于1mss。只有
    			当现有分段满负荷时,才能生成新的数据分段*/
    			skb = sk->sk_write_queue.prev;
    
    			/*队列是一个双向链表,通过队列头的prev来访问套接字最后一个分段。
    			首先检查传输队列头是否有数据分段,如果该值是NULL,就没必要
    			检查未满分段。*/
    			if (!sk->sk_send_head ||/* 发送队列为空,前面取得的skb无效 */
    				/* 如果skb有效,但是它已经没有多余的空间复制新数据了 */
    			    (copy = mss_now - skb->len) <= 0) {
    /*为用户数据创建一个新的分段*/
    new_segment:
    				/* 发送队列中数据长度达到发送缓冲区的上限,等待缓冲区 */
    				if (!sk_stream_memory_free(sk))
    					goto wait_for_sndbuf;
    
    				/*如果有足够的内存,就为TCP数据分段分配新缓冲区。如果硬件
    				支持分散-聚集技术,就分配一个数据页大小的缓冲区。否则就
    				分配大小为1mss的缓冲区。*/
    				skb = sk_stream_alloc_pskb(sk, select_size(sk, tp),
    							   0, sk->sk_allocation);/* 分配新的skb */
    				/* 分配失败,说明系统内存不足,等待内存释放 */
    				if (!skb)
    					goto wait_for_memory;
    
    				/* 根据路由网络设备的特性,确定是否由硬件执行校验和 */
    				if (sk->sk_route_caps &
    				    (NETIF_F_IP_CSUM | NETIF_F_NO_CSUM |
    				     NETIF_F_HW_CSUM))
    					skb->ip_summed = CHECKSUM_HW;
    
    				skb_entail(sk, tp, skb);/* 将SKB新分段添加到发送队列尾部 */
    				copy = mss_now;/* 本次需要复制的数据量是MSS */
    			}
    
    			/* 要复制的数据长度copy不能大于当前段剩余的长度,
    			seglen的减法在下面。*/
    			if (copy > seglen)
    				copy = seglen;
    
    			/* skb线性存储区底部还有空间 */
    			if (skb_tailroom(skb) > 0) {
    				/* 本次只复制skb存储区底部剩余空间大小的数据量 */
    				if (copy > skb_tailroom(skb))
    					copy = skb_tailroom(skb);
    				/* 从用户空间复制指定长度的数据到skb中,如果失败,则退出 */
    				if ((err = skb_add_data(skb, from, copy)) != 0)
    					goto do_fault;
    			} 
    			/* 线性存储区底部已经没有空间了,复制到分散/聚集存储区中 */
    			else {
                            ...//忽略
                            }
    
    			if (!copied)/* 如果没有复制数据,则取消PSH标志 */
    				TCP_SKB_CB(skb)->flags &= ~TCPCB_FLAG_PSH;
    
    
    
    			/* 更新发送队列最后一个包的序号 */
    			tp->write_seq += copy;
    			/* 更新当前数据分段skb的结尾序号,以使其能够包括当前段所
    			覆盖的全部序列。*/
    			TCP_SKB_CB(skb)->end_seq += copy;
    			skb_shinfo(skb)->tso_segs = 0;
    
    			/* 更新数据复制的指针,使其指向下一个要复制的数据起始位置,
    			然后更新要复制的字节数。*/
    			from += copy;
    			copied += copy;
    			/* 如果所有数据已经复制完毕则退出,会调用tcp_push()将传输
    			队列中的数据分段发送出去。*/
    			if ((seglen -= copy) == 0 && iovlen == 0)
    				goto out;
    
    			/* 走到这里说明还没有将全部用户缓冲区复制到套接字缓冲区,就检查
    			如果当前skb中的数据小于mss,说明可以往里面继续复制数据。
    			或者发送的是OOB数据,则也跳过发送过程,继续复制数据 */
    			if (skb->len != mss_now || (flags & MSG_OOB))
    				continue;
    
    			/* 走到这里说明当前数据段已满。就调用foced_push()来检查是否为
    			传输队列中的最后一个分段设置了强制push标志。如果设置了强制push
    			标志,需要告诉接收端应用程序首先处理该数据。
    			必须立即发送数据,即上次发送后产生的数据已经超过通告窗口值的一半 */
    			if (forced_push(tp)) {
    				/* 设置PSH标志后发送数据 */
    				tcp_mark_push(tp, skb);
    				/*然后根据Nagle算法、拥塞窗口、发送窗口调用此函数来启动
    				待发送分段的传输。*/
    				__tcp_push_pending_frames(sk, tp, mss_now, TCP_NAGLE_PUSH);
    			} 
    			/* 虽然不是必须发送数据,但是发送队列上只存在当前段,也将其发送出去 */
    			/*如果不能强制push该数据,并且传输队列中仅有一个数据段,就调用
    			tcp_push_one()来将该数据段push到传输队列中。*/
    			else if (skb == sk->sk_send_head)
    				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将其发送出去 */
    				tcp_push(sk, tp, flags & ~MSG_MORE, mss_now, TCP_NAGLE_PUSH);
    
    			/* 等待内存可用 */
    			if ((err = sk_stream_wait_memory(sk, &timeo)) != 0)
    				goto do_error;/* 确实没有内存了,超时后返回失败 */
    
    			/* 睡眠后,MSS可能发生了变化,重新计算 */
    			mss_now = tcp_current_mss(sk, !(flags&MSG_OOB));
    		}
    	}
    
    out:
    	if (copied)/* 从用户态复制了数据,发送它 */
    		tcp_push(sk, tp, flags, mss_now, tp->nonagle);
    	TCP_CHECK_TIMER(sk);
    	release_sock(sk);/* 释放锁以后返回 */
    	return copied;
    
    do_fault:
    	if (!skb->len) {/* 复制数据失败了,如果skb长度为0,说明是新分配的,释放它 */
    		if (sk->sk_send_head == skb)/* 如果skb是发送队列头,则清空队列头 */
    			sk->sk_send_head = NULL;
    		__skb_unlink(skb, skb->list);
    		sk_stream_free_skb(sk, skb);/* 释放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;
    }
    

    详细的说明见注释。

    注意几点主要的流程:

    1.TCP以1mss为单元来发送数据,最大段大小基于MTU计算获得,MTU是一个链路层的特征参数,并且可以从tcp_current_mss()获取。

    2.sk_stream_alloc_pskb()为TCP数据分配一个新的缓冲区,它的最小长度是1mss。

    3.skb_entail()将报文发往传输缓存区排队,并计算已分配的缓冲区内存。

    4.tcp_push_one()负责传输写队列中的一个数据段。__tcp_push_pending_frames()负责传输在写队列中排队的多个数据段。

  • 相关阅读:
    继承-方法重写
    继承2
    继承
    JAVA-基本数据类型与引用数据类型区别
    JS判断一个数是否为质数
    Yslow
    Sublime Less 自动编译成css
    chrom调试
    解决在微信中部分IOS不能自动播放背景音乐
    常用的jq插件
  • 原文地址:https://www.cnblogs.com/joey-hua/p/5899471.html
Copyright © 2011-2022 走看看