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

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

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

    1.排队机制 接收输入TCP报文时,有三个队列: ● 待处理队列 ● 预排队队列 ● 接收队列 接收队列包含了处理过的TCP数据段,也就是说,去除了全部的协议头,正准备将数据复制到用户应用程序.接收队 ...

  2. Linux 内核协议栈之TCP连接关闭

    Close行为: 当应用程序在调用close()函数关闭TCP连接时,Linux内核的默认行为是将套接口发送队列里的原有数据(比如之前残留的数据)以及新加入 的数据(比如函数close()产生的FIN ...

  3. Linux2.6内核进程调度系列--scheduler_tick()函数1.总体思想

    参考的是ULK第三版,Linux2.6.11.12内核版本. 调度程序依靠几个函数来完成调度工作,其中最重要的第一个函数是scheduler_tick函数,主要步骤如下: /** * 维持当前最新的t ...

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

    摘要:     本文简单介绍了TCP面向连接理论知识,详细讲述了TCP报文各个字段含义,并从Wireshark俘获分组中选取TCP连接建立相关报文段进行分析. 一.概述     TCP是面向连接的可靠 ...

  5. Linux2.6内核进程调度系列--scheduler_tick()函数3.更新普通进程的时间片

    RT /** * 运行到此,说明进程是普通进程.现在开始更新普通进程的时间片. */ /* 首先递减普通进程的时间片计数器.如果用完,继续执行以下操作 */ if (!--p->time_sli ...

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

    RT /** * 递减当前进程的时间片计数器,并检查是否已经用完时间片. * 由于进程的调度类型不同,函数所执行的操作也有很大差别. */ /* 如果是实时进程,就进一步根据是FIFO还是RR类型的实 ...

  7. 【TCP协议】(3)---TCP粘包黏包

    [TCP协议](3)---TCP粘包黏包 有关TCP协议之前写过两篇博客: 1.[TCP协议](1)---TCP协议详解 2.[TCP协议](2)---TCP三次握手和四次挥手 一.TCP粘包.拆包图 ...

  8. 第3章 TCP协议详解

    第3章 TCP协议详解 3.1 TCP服务的特点 传输协议主要有两个:TCP协议和UDP协议,TCP协议相对于UDP协议的特点是 面向连接使用TCP协议通信的双方必须先建立连接,完成数据交换后,通信双 ...

  9. TCP 协议详解

    TCP 协议是 更靠近应用层,因此在应用程序中具有更强可操作性,一些重要 socket 选项都和 TCP 协议相关. TCP 头部信息:TCP 头部信息出现在每个 TCP 报文段中,用于指定通信的源端 ...

随机推荐

  1. 执行git push出现"Everything up-to-date"

    在github上git clone一个项目,在里面创建一个目录,然后git push的时候,出现报错"Everything up-to-date" 原因:1)没有git add . ...

  2. ES6 箭头函数中的 this?你可能想多了(翻译)

    箭头函数=>无疑是ES6中最受关注的一个新特性了,通过它可以简写 function 函数表达式,你也可以在各种提及箭头函数的地方看到这样的观点——“=> 就是一个新的 function”. ...

  3. 让ASP.NET接受有“潜在危险”的提交

    什么是有“潜在危险”的提交?马上动手写个简单的例子:   用Visual Studio创建一个空白的ASP.NET MVC程序,一切默认即可,添加一个空白的HomeController,增加一个Ind ...

  4. MVVM大比拼之knockout.js源码精析

    简介 本文主要对源码和内部机制做较深如的分析,基础部分请参阅官网文档. knockout.js (以下简称 ko )是最早将 MVVM 引入到前端的重要功臣之一.目前版本已更新到 3 .相比同类主要有 ...

  5. 基于 SailingEase WinForm Framework 开发优秀的客户端应用程序(1:概述)

    本系统文章将详细阐述客户端应用程序的设计理念,实现方法. 本系列文章以  SailingEase WinForm Framework 为基础进行设计并实现,但其中的设计理念及方法,亦适用于任何类型的客 ...

  6. Go语言实战 - 使用SendCloud群发邮件

    山坡网需要能够每周给注册用户发送一封名为"本周最热书籍"的邮件,而之前一直使用的腾讯企业邮箱罢工了,提示说发送请求太多太密集. 一番寻找之后发现了大家口碑不错的搜狐SendClou ...

  7. 【Win 10 应用开发】透视效果

    所谓透视效果,就是在平面坐标空间上模拟出“好像”三维的效果.要是老周没有记错的话,以前在写WP8相关的内容时写过,UWP中的透视方法也保留了以前的Do法,其实这玩意儿是从 Silverlight 沿袭 ...

  8. 开发node桌面级应用工具:apk转化epub

    随着苹果ibooks对国内的开放,最近接了个麻烦的需求: 把现有的APK转化支持苹果ibooks电子书的epub格式 apk,基本都知道就是安卓的应用程序 epub,是ibooks支持的电子书格式 ( ...

  9. C# windows服务制作(包括安装及卸载)

    开篇语 因工作内容需要做一个windows服务,此前并没有相关经验,所以做了一个demo来跑跑这个梗(高手跳过,需要的来踩)- 效果如下:打开服务,可以找到我们新增的一个windows服务,这个dem ...

  10. spring帝国-开篇

    spring简介: spring是一个开源框架,spring是于2003 年兴起的一个轻量级的Java 开发框架,由Rod Johnson 在其著作Expert One-On-One J2EE Dev ...