TCP的发送系列 — tcp_sendmsg()的实现(一)
主要内容:Socket发送函数在TCP层的实现
内核版本:3.15.2
我的博客:http://blog.csdn.net/zhangskd
上一篇blog讲的是send()、sendto()、sendmsg()和sendmmsg()这些发送函数的系统调用和
Socket层实现,现在来看下它们的TCP层实现。
TCP层实现
SOCK_STREAM类socket的TCP层操作函数集实例为tcp_prot,其中使用tcp_sendmsg()来发送数据。
struct proto tcp_prot = {
.name = "TCP",
.owner = THIS_MODULE,
...
.sendmsg = tcp_sendmsg,
...
};
tcp_sendmsg()的主要工作是把用户层的数据,填充到skb中,然后加入到sock的发送队列。
之后调用tcp_write_xmit()来把sock发送队列中的skb尽量地发送出去。
另外TCP发送缓存的管理也主要发生在tcp_sendmsg()函数中,在接下来的blog中会有单独的分析。
tcp_sendmsg()做了以下事情:
1. 如果使用了TCP Fast Open,则会在发送SYN包的同时携带上数据。
2. 如果连接尚未建立好,不处于ESTABLISHED或者CLOSE_WAIT状态,
那么进程进行睡眠,等待三次握手的完成。
3. 获取当前的MSS、网络设备支持的最大数据长度size_goal。
如果支持GSO,size_goal会是MSS的整数倍。
4. 遍历用户层的数据块数组:
4.1 获取发送队列的最后一个skb,如果是尚未发送的,且长度尚未达到size_goal,
那么可以往此skb继续追加数据。
4.2 否则需要申请一个新的skb来装载数据。
4.2.1 如果发送队列的总大小sk_wmem_queued大于等于发送缓存的上限sk_sndbuf,
或者发送缓存中尚未发送的数据量超过了用户的设置值:
设置同步发送时发送缓存不够的标志。
如果此时已有数据复制到发送队列了,就尝试立即发送。
等待发送缓存,直到sock有发送缓存可写事件唤醒进程,或者等待超时。
4.2.2 申请一个skb,其线性数据区的大小为:
通过select_size()得到的线性数据区中TCP负荷的大小 + 最大的协议头长度。
如果申请skb失败了,或者虽然申请skb成功,但是从系统层面判断此次申请不合法,
等待可用内存,等待时间为2~202ms之间的一个随机数。
4.2.3 如果以上两步成功了,就更新skb的TCP控制块字段,把skb加入到sock发送队列的尾部,
增加发送队列的大小,减小预分配缓存的大小。
4.3 接下来就是拷贝消息头中的数据到skb中了。
如果skb的线性数据区还有剩余空间,就复制数据到线性数据区中,同时计算校验和。
4.4 如果skb的线性数据区已经用完了,那么就使用分页区:
4.4.1 检查分页是否有可用空间,如果没有就申请新的page。如果申请失败,说明系统内存不足。
之后会设置TCP内存压力标志,减小发送缓冲区的上限,睡眠等待内存。
4.4.2 判断能否往最后一个分页追加数据。不能追加时,检查分页数是否达到了上限、
或网卡不支持分散聚合。如果是的话,就为此skb设置PSH标志。
然后跳转到4.2处申请新的skb,来继续填装数据。
4.4.3 从系统层面判断此次分页发送缓存的申请是否合法。
4.4.4 拷贝用户空间的数据到skb的分页中,同时计算校验和。
更新skb的长度字段,更新sock的发送队列大小和预分配缓存。
4.4.5 如果把数据追加到最后一个分页了,更新最后一个分页的数据大小。否则初始化新的分页。
4.5 拷贝成功后更新:送队列的最后一个序号、skb的结束序号、已经拷贝到发送队列的数据量。
4.6 尽可能的将发送队列中的skb发送出去。
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, err, copied = 0;
int mss_now = 0, size_goal, copied_syn = 0, offset = 0;
bool sg;
long timeo; lock_sock(sk); flags = msg->msg_flags; /* Send data in TCP SYN.
* 使用了TCP Fast Open时,会在发送SYN时携带上数据。
*/
if (flags & MSG_FASTOPEN) {
err = tcp_sendmsg_fastopen(sk, msg, &copied_syn, size);
if (err == -EINPROGRESS && copied_syn > 0)
goto out;
else if (err)
goto out_err; offset = copied_syn;
} /* 发送的超时时间,如果是非阻塞的则为0 */
timeo = sock_sndtimeo(sk, flags & MSG_DONTWAIT); /* Wait for a connection to finish.
* One exception is TCP Fast Open (passive side) where data is allowed to
* be sent before a connection is fully established.
*/ /* 如果连接尚未完成三次握手,是不允许发送数据的,除非是Fast Open的被动打开方 */
if (((1 << sk->sk_state) & ~(TCPF_ESTABLISHED | TCPF_CLOSE_WAIT)) &&
! (tcp_passive_fastopen(sk)) { /* 等待连接的建立,成功时返回值为0 */
if ((err = sk_stream_wait_connect(sk, &timeo)) != 0)
goto do_error;
} /* 使用TCP_REPAIR选项时 */
if (unlikely(tp->repair)) { /* 发送到接收队列中 */
if (tp->repair_queue == TCP_RECV_QUEUE) {
copied = tcp_send_rcvq(sk, msg, size);
goto out;
} err = -EINVAL;
if (tp->repair_queue == TCP_NO_QUEUE)
goto out_err; /* common sending to sendq */
} /* This should be in poll.
* 清除使用异步情况下,发送队列满了的标志。
*/
clear_bit(SOCK_ASYNC_NOSPACE, &sk->sk_socket->flags); /* 获取当前的发送MSS.
* 获取可发送到网卡的最大数据长度,如果使用GSO,会是MSS的整数倍。
*/
mss_now = tcp_send_mss(sk, &size_goal, flags); /* Ok commence sending. */
iovlen = msg->msg_iovlen; /* 应用层数据块的个数*/
iov = msg->msg_iov; /* 应用层数据块数组的地址 */
copied = 0; /* 已拷贝到发送队列的字节数 */ err = -EPIPE; /* Broken pipe */
/* 如果连接有错误,或者不允许发送数据了,那么返回-EPIPE */
if (sk->sk_err || (sk->sk_shutdown & SEND_SHUTDOWN))
goto out_err; sg = !! (sk->sk_route_caps & NETIF_F_SG); /* 网卡是否支持分散聚合 */ /* 遍历用户层的数据块数组 */
while (--iovlen >= 0) { size_t seglen = iov->iov_len; /* 数据块的长度 */
unsigned char __user *from = iov->iov_base; /* 数据块的地址 */ iov++; /* 指向下一个数据块 */ /* Skip bytes copied in SYN.
* 如果使用了TCP Fast Open,需要跳过SYN包发送过的数据。
*/
if (unlikely(offset > 0)) {
if (offset >= seglen) {
offset -= seglen;
continue;
} seglen -= offset;
from += offset;
offset = 0;
} while (seglen > 0) {
int copy = 0;
int max = size_goal; /* 单个skb的最大数据长度,如果使用了GSO,长度为MSS的整数倍 */ skb = tcp_write_queue_tail(sk); /* 发送队列的最后一个skb */ if (tcp_send_head(sk)) { /* 还有未发送的数据,说明该skb还未发送 */
/* 如果网卡不支持检验和计算,那么skb的最大长度为MSS,即不能使用GSO */
if (skb->ip_summed == CHECKSUM_NONE)
max = mss_now; copy = max - skb->len; /* 此skb可追加的数据长度 */
} if (copy <= 0) { /* 需要使用新的skb来装数据 */
new_segment:
/* Allocate new segment. If the interface is SG,
* allocate skb fitting to single page.
*/ /* 如果发送队列的总大小sk_wmem_queued大于等于发送缓存的上限sk_sndbuf,
* 或者发送缓存中尚未发送的数据量超过了用户的设置值,就进入等待。
*/
if (! sk_stream_memory_free(sk))
goto wait_for_sndbuf; /* 申请一个skb,其线性数据区的大小为:
* 通过select_size()得到的线性数据区中TCP负荷的大小 + 最大的协议头长度。
* 如果申请skb失败了,或者虽然申请skb成功,但是从系统层面判断此次申请不合法,
* 那么就进入睡眠,等待内存。
*/
skb = sk_stream_alloc_skb(sk, select_size(sk, sg), sk->sk_allocation);
if (! skb)
goto wait_for_memory; /* All packets are restored as if they have already been sent.
* 如果使用了TCP REPAIR选项,那么为skb设置“发送时间”。
*/
if (tp->repair)
TCP_SKB_CB(skb)->when = tcp_time_stamp; /* Check whether we can use HW checksum.
* 如果网卡支持校验和的计算,那么由硬件计算报头和首部的校验和。
*/
if (sk->sk_route_caps & NETIF_F_ALL_CSUM)
skb->ip_summed = CHECKSUM_PARTIAL; /* 更新skb的TCP控制块字段,把skb加入到sock发送队列的尾部,
* 增加发送队列的大小,减小预分配缓存的大小。
*/
skb_entail(sk, skb); copy = size_goal;
max = size_goal;
} /* Try to append data to the end of skb.
* 本次可拷贝的数据量不能超过数据块的长度。
*/
if (copy > seglen)
copy = seglen; /* Where to copy to ?
* 如果skb的线性数据区还有剩余空间,就先复制到线性数据区。
*/
if (skb_availroom(skb) > 0) {
copy = min_t(int, copy, skb_availroom(skb)); /* 拷贝用户空间的数据到内核空间,同时计算校验和 */
err = skb_add_data_nocache(sk, skb, from, copy);
if (err)
goto do_fault; } else { /* 如果skb的线性数据区已经用完了,那么就使用分页区 */
bool merge = true;
int i = skb_shinfo(skb)->nr_frags; /* 分页数 */
struct page_frag *pfrag = sk_page_frag(sk); /* 上次缓存的分页 */ /* 检查分页是否有可用空间,如果没有就申请新的page。
* 如果申请失败,说明系统内存不足。
* 之后会设置TCP内存压力标志,减小发送缓冲区的上限,睡眠等待内存。
*/
if (! sk_page_frag_refill(sk, pfrag))
goto wait_for_memory; /* 判断能否往最后一个分页追加数据 */
if (! skb_can_coalesce(skb, i, pfrag->page, pfrag->offset)) { /* 不能追加时,检查分页数是否达到了上限,或者网卡不支持分散聚合。
* 如果是的话,就为此skb设置PSH标志,尽快地发送出去。
* 然后跳转到new_segment处申请新的skb,来继续填装数据。
*/
if (i == MAX_SKB_FRAGS || ! sg) {
tcp_mark_push(tp, skb);
goto new_segment;
}
merge = false;
} copy = min_t(int ,copy, pfrag->size - pfrag->offset); /* 从系统层面判断发送缓存的申请是否合法 */
if (! sk_wmem_schedule(sk, copy))
goto wait_for_memory; /* 拷贝用户空间的数据到内核空间,同时计算校验和。
* 更新skb的长度字段,更新sock的发送队列大小和预分配缓存。
*/
err = skb_copy_to_page_nocache(sk, from, skb, pfrag->page, pfrag->offset, copy);
if (err)
goto do_error; /* Update the skb. */
if (merge) { /* 如果把数据追加到最后一个分页了,更新最后一个分页的数据大小 */
skb_frag_size_add(&skb_shinfo(skb)->frags[i - 1], copy);
} else {
/* 初始化新增加的页 */
skb_fill_page_desc(skb, i, pfrag->page, pfrag->offset, copy);
get_page(pfrag->page);
} pfrag->offset += copy;
} /* 如果这是第一次拷贝,取消PSH标志 */
if (! copied)
TCP_SKB_CB(skb)->tcp_flags &= ~TCPHDR_PSH; tp->write_seq += copy; /* 更新发送队列的最后一个序号 */
TCP_SKB_CB(skb)->send_seq += copy; /* 更新skb的结束序号 */
skb_shinfo(skb)->gso_segs = 0; from += copy; /* 下次拷贝的地址 */
copied += copy; /* 已经拷贝到发送队列的数据量 */ /* 如果所有数据都拷贝好了,就退出 */
if ((seglen -= copy) == 0 && iovlen == 0)
goto out; /* 如果skb还可以继续填充数据,或者发送的是带外数据,或者使用TCP REPAIR选项,
* 那么继续拷贝数据,先不发送。
*/
if (skb->len < max || (flags & MSG_OOB) || unlikely(tp->repair))
continue; /* 如果需要设置PSH标志 */
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))
tcp_push_one(sk, mss_now); /* 只发送一个skb */ 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, size_goal); /* 分两种情况:
* 1. sock的发送缓存不足。等待sock有发送缓存可写事件,或者超时。
* 2. TCP层内存不足,等待2~202ms之间的一个随机时间。
*/
if ((err = sk_stream_wait_memory(sk, &timeo)) != 0)
goto do_err; /* 睡眠后MSS和TSO段长可能会发生变化,重新计算 */
mss_now = tcp_send_mss(sk, &size_goal, flags); } // end while seglen > 0
} // end while --iovlen >= 0 out:
/* 如果已经有数据复制到发送队列了,就尝试立即发送 */
if (copied)
tcp_push(sk, flags, mss_now, tp->nonagle, size_goal); release_sock(sk);
return copied + copied_syn; do_fault:
if (! skb->len) { /* 如果skb没有负荷 */
tcp_unlink_write_queue(skb, sk); /* 把skb从发送队列中删除 */ /* 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->sk_send_head */
sk_wmem_free_skb(sk, skb); /* 更新发送队列的大小和预分配缓存,释放skb */
} do_error:
if (copied + copied_syn)
goto out; out_err:
err = sk_stream_error(sk, flags, err);
release_sock(sk);
return err;
}
TCP的发送系列 — tcp_sendmsg()的实现(一)的更多相关文章
- TCP的发送系列 — tcp_sendmsg()的实现(二)
主要内容:Socket发送函数在TCP层的实现 内核版本:3.15.2 我的博客:http://blog.csdn.net/zhangskd 在上篇blog中分析了tcp_sendmsg()这个主要函 ...
- TCP的发送系列 — 发送缓存的管理(二)
主要内容:从TCP层面判断发送缓存的申请是否合法,进程因缺少发送缓存而进行睡眠等待. 因为有发送缓存可写事件而被唤醒. 内核版本:3.15.2 我的博客:http://blog.csdn.net/zh ...
- TCP的发送系列 — 发送缓存的管理(一)
主要内容:TCP发送缓存的初始化.动态调整.申请和释放. 内核版本:3.15.2 我的博客:http://blog.csdn.net/zhangskd 数据结构 TCP对发送缓存的管理是在两个层面上进 ...
- C# TCP socket发送大数据包时,接收端和发送端数据不一致 服务端接收Receive不完全
简单的c# TCP通讯(TcpListener) C# 的TCP Socket (同步方式) C# 的TCP Socket (异步方式) C# 的tcp Socket设置自定义超时时间 C# TCP ...
- Java基础知识强化之网络编程笔记06:TCP之TCP协议发送数据 和 接收数据
1. TCP协议发送数据 和 接收数据 TCP协议接收数据:• 创建接收端的Socket对象• 监听客户端连接.返回一个对应的Socket对象• 获取输入流,读取数据显示在控制台• 释放资源 TCP协 ...
- 高效的TCP消息发送组件
目前的.net 架构下缺乏高效的TCP消息发送组件,而这种组件是构建高性能分布式应用所必需的.为此我结合多年的底层开发经验开发了一个.net 下的高效TCP消息发送组件.这个组件在异步发送时可以达到每 ...
- TCP的核心系列 — ACK的处理(二)
本文主要内容:tcp_ack()中的一些细节,如发送窗口的更新.持续定时器等. 内核版本:3.2.12 Author:zhangskd @ csdn 发送窗口的更新 什么时候需要更新发送窗口呢? (1 ...
- 网络编程--使用TCP协议发送接收数据
package com.zhangxueliang.tcp; import java.io.IOException; import java.io.OutputStream; import java. ...
- Tcp Udp发送包的大小限制问题
以太网(Ethernet)数据帧的长度必须在46-1500字节之间,这是由以太网的物理特性决定的. 这个1500字节被称为链路层的MTU(最大传输单元). 但这并不是指链路层的长度被限制在 ...
随机推荐
- [Ahoi2005]LANE 航线规划
题目描述 对Samuel星球的探险已经取得了非常巨大的成就,于是科学家们将目光投向了Samuel星球所在的星系——一个巨大的由千百万星球构成的Samuel星系. 星际空间站的Samuel II巨型计算 ...
- hihocoder 1035 : 自驾旅行 III
描述 给定一棵含有 n 个结点的树,结点从 1 标号.你从 1 号结点驾车出发,希望遍历一些关键结点(访问到就好,不需要按照这些关键结点的输入顺序).每条边有两个权值,c0, c1 分别表示步行和驾车 ...
- 例10-11 uva11181
题意:n个人去逛超市,第i个人买东西的概率是pi,,计算每个人实际买了东西的概率 思路: 搜索标处理所以的情况,得出所有概率和all(开始天真的以为是1 - -,就说怎么案例看着怪怪的),用tt[i] ...
- 【bzoj4571&&SCOI2016美味】
4571: [Scoi2016]美味 Time Limit: 30 Sec Memory Limit: 256 MBSubmit: 656 Solved: 350[Submit][Status][ ...
- Elasticsearch 创建、更新、删除文档、处理冲突
----创建新文档---- 1._index,_type和_id的组合可以唯一标识一个文档,所以确保一个新文档的最简单的办法就是,使用索引请求的POST形式让elsticsearch自动生成唯一_id ...
- win10下python环境变量设置
我用的是python_2.7.3.msi,从官网下载之后,一路按照默认进行安装. 安装之后配置环境变量的步骤如下: 1,点“我的电脑”,右键选“属性” 2,选择“高级系统设置”--->选“环境变 ...
- C语言第二次作业-----顺序结构
一:改错题 (1)输出指定信息: 将给定源代码输入编译器: 执行编译命令,发现编译器报错,错误信息如下: 经检查,发现源程序将"stdio.h"误拼为"stido.h&q ...
- html取地址栏参数的方法
现在的人写的博客真是日常挖坑 闲的蛋疼 想把所有东西都转成jstl格式 有个界面是取地址栏的信息的 之前用的是 <%--不用jstl的方法直接传递--%> <%--String ro ...
- SQL部分常用指令整理
dual 伪表 用来测试函数和表达式 1.查询EMP表中所有人的信息,结果格式样例为"某人的月薪是1000$" SELECT ENAME||'的月薪是'||SAL||'$' FRO ...
- 导出数据子集:带where条件的exp导出
举个例子:用select * from all_objects创建了一张表T.想要导出object_id小于5000的所有行.(1)windows下: exp userid=cms0929/cms09 ...