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()负责传输在写队列中排队的多个数据段。

时间: 2024-10-19 11:06:59

Linux2.6内核协议栈系列--TCP协议1.发送的相关文章

Linux2.6内核协议栈系列--TCP协议2.接收

1.排队机制 接收输入TCP报文时,有三个队列: ● 待处理队列 ● 预排队队列 ● 接收队列 接收队列包含了处理过的TCP数据段,也就是说,去除了全部的协议头,正准备将数据复制到用户应用程序.接收队列包含了所有按顺序接收的数据段,在其他两个队列中的TCP数据段则需要进一步处理. TCP报文首先由tcp_v4_rcv()进行处理.该函数要决定是否需要处理报文或者在待处理队列和预排队队列中排队. /* 传输层报文处理入口 */ int tcp_v4_rcv(struct sk_buff *skb)

转_结合Wireshark捕获分组深入理解TCP/IP协议栈之TCP协议(TCP报文格式+三次握手实例)

转自: http://blog.chinaunix.net/uid-9112803-id-3212041.html 摘要: 本文简单介绍了TCP面向连接理论知识,详细讲述了TCP报文各个字段含义,并从Wireshark俘获分组中选取TCP连接建立相关报文段进行分析. 一.概述 TCP是面向连接的可靠传输协议,两个进程互发数据之前需要建立连接,这里的连接只不过是端系统中分配的一些缓存和状态变量,中间的分组交换机不维护任何连接状态信息.连接建立整个过程如下(即三次握手协议): 首先,客户机发送一个特

Linux2.6内核进程调度系列--scheduler_tick()函数2.更新实时进程的时间片

RT /** * 递减当前进程的时间片计数器,并检查是否已经用完时间片. * 由于进程的调度类型不同,函数所执行的操作也有很大差别. */ /* 如果是实时进程,就进一步根据是FIFO还是RR类型的实时进程 */ if (rt_task(p)) { /** * 对SCHED_RR类型(时间片轮转)的实时进程,需要递减它的时间片. * 对SCHED_FIFO类型(先进先出)的实时进程,什么都不做, * 退出.在这种情况下, * current进程不可能被比其优先级低或其优先级相等的进程所抢占, *

TCP协议的那些事(总结篇)

传输层概述 传输层概述 TCP协议特点:面向连接.字节流.可靠传输 面向链接: 1.使用TCP协议的双方必须先建立连接,并且双方都必须分配相应的内核资源.TCP的连接是全双工的,也就是说双方可以根据一个连接进行读写操作. 字节流: 1.当发送方应用多次进行写操作的时候,TCP发送模块会先把数据放在发送缓冲区中,当TCP发送模块真正发送的时候,这些在发送缓冲区中的数据才可能被封装成一个或多个报文段发出.所有根据以上结论,应用程序执行的写操作的次数和TCP发送的报文段个数没有对应的数量关系. 2.当

如何旁路内核协议栈

如何旁路内核协议栈 时间  2017-01-15 原文   http://blog.csdn.net/liukun321/article/details/54564798 此文转自:http://blog.csdn.net/wwh578867817/article/details/50139819 在前两篇文章中,我们讨论了如何每秒接收 1M UDP 数据包 以及 如何减少往返时间 .我们在 Linux 上做试验,因为它是一个性能非常好的通用操作系统. 不幸的是,对于一些更加专业的工作,Vani

TCP协议详解即实例分析

 TCP协议详解 3.1 TCP服务的特点 TCP协议相对于UDP协议的特点是面向连接.字节流和可靠传输. 使用TCP协议通信的双方必须先建立链接,然后才能开始数据的读写.双方都必须为该链接分配必要的内核资源,以还礼链接状态和连接上数据的传输.TCP链接是全双工的,即双方的数据读写可以通过一个连接进行.完成数据交换之后,通信双方都必须断开连接以释放系统资源. TCP协议的这种连接是一对一的,所以基于广播和多播(目标是多个主机地址)的应用程序不能使用TCP服务.而无连接协议UDP则非常适合于广

[转帖]Linux TCP/IP协议栈,数据发送接收流程,TCP协议特点

Linux TCP/IP协议栈,数据发送接收流程,TCP协议特点 http://network.51cto.com/art/201909/603780.htm 可以毫不夸张的说现如今的互联网是基于TCP/IP构建起来的网络.弄懂协议栈的原理,无论对调试网络IO性能还是解决网络问题都是有很大帮助的.本片文章就带领大家来看看内核是如何控制网络数据流的. 作者:底层软件架构来源:今日头条|2019-09-30 09:28 收藏 分享 可以毫不夸张的说现如今的互联网是基于TCP/IP构建起来的网络.弄懂

协议系列之TCP协议

3.TCP协议 从上一节我们了解了什么是IP协议,以及IP协议的一些特性,利用IP协议传输都是单向的,不可靠的,无连接状态的.正是这些特性,于是便产生了TCP协议.TCP协议属于传输层,在IP协议网络层之上,竟然IP协议不可靠,那就必须要在其上多一个TCP协议以实现传输的可靠性.就像我们寄出一封信,如果对方不回信,不通过别的渠道告诉你,你永远都无法保证这封信能准确送到对方手上.同样,TCP协议采取了类似的措施来保证数据包的准确送达,它规定接收端发送一个确认数据包回来. 严格地说,TCP协议提供了

Linux内核中影响tcp三次握手的一些协议配置

在Linux的发行版本中,都存在一个/proc/目录,有的也称它为Proc文件系统.在 /proc 虚拟文件系统中存在一些可调节的内核参数.这个文件系统中的每个文件都表示一个或多个参数,它们可以通过 cat 工具进行读取,或使用 echo 命令进行修改.下面给出了几个可调节的参数是关于Linux TCP/IP 栈的参数,相关的帮助可以通过man tcp或info tcp获取.在这个目录中,包括了一些特殊的文件,不仅能用来反映内核的现行状态和查看硬件信息,而且,有些文件还允许用户来修改其中的内容,