主要内容:Socket发送函数在TCP层的实现

内核版本:3.15.2

我的博客:http://blog.csdn.net/zhangskd

在上篇blog中分析了tcp_sendmsg()这个主要函数的实现,现在来看下之前略过的一些细节,

包括等待连接的建立、tcp_push()的实现、tcp_autocorking和数据的复制。

等待连接建立

在tcp_sendmsg()中如果发现连接尚未建立,会调用sk_stream_wait_connect()来等待连接的建立,

连接成功建立时返回0,之后才能发送数据。

/* Wait for a socket to get into the connected state
* @sk: sock to wait on
* @timeo_p: for how long to wait
* Must be called with the socket locked.
*/ int sk_stream_wait_connect(struc sock *sk, long *timeo_p)
{
struct task_struct *tsk = current;
DEFINE_WAIT(wait); /* 初始化等待任务 */
int done; do {
int err = sock_error(sk); /* 连接发生错误 */
if (err)
return err; /* 此时连接必须处于SYN_SENT或SYN_RECV的状态 */
if (1 << sk->sk_state) & ~(TCPF_SYN_SENT | TCPF_SYN_RECV))
return -EPIPE; /* Broken pipe */ /* 如果是非阻塞的,或者等待时间耗尽了,直接返回 */
if (! *timeo_p)
return -EAGAIN; /* Try again */ /* 如果进程有待处理的信号,返回 */
if (signal_pending(tsk))
return sock_intr_errno(*timeo_p); /* 把等待任务加入到socket等待队列头部,把进程的状态设为TASK_INTERRUPTIBLE */
prepare_to_wait(sk_sleep(sk), &wait, TASK_INTERRUPTIBLE);
sk->sk_write_pending++; /* 更新写等待计数 */ /* 进入睡眠,返回值为真的条件:
* 连接没有发生错误,且状态为ESTABLISHED或CLOSE_WAIT。
*/
done = sk_wait_event(sk, timeo_p, ! sk->sk_err &&
! ((1 << sk->sk_state) & ~(TCPF_ESTABLISHED | TCPF_CLOSE_WAIT))); /* 把等待任务从等待队列中删除,把当前进程的状态设为TASK_RUNNING */
finish_wait(sk_sleep(sk), &wait);
sk->sk_write_pending--; /* 更新写等待计数 */
} while (! done) return 0;
}
#define sk_wait_event(__sk, __timeo, __condition)    \
({ int __rc; \
release_sock(__sk); \
__rc = __condition; \ if (! __rc) { \
*(__timeo) = schedule_timeout(*(__timeo)); \
} \ lock_sock(__sk); \
__rc = __condition; \
__rc; \
}) static inline long sock_sndtimeo(const struct sock *sk, bool noblock)
{
return noblock ? 0 : sk->sk_sndtimeo;
}

tcp_push

tcp_sendmsg()中,在sock发送缓存不足、系统内存不足或应用层的数据都拷贝完毕等情况下,

都会调用tcp_push()来把已经拷贝到发送队列中的数据给发送出去。

tcp_push()主要做了以下事情:

1. 检查是否有未发送过的数据。

2. 检查是否需要设置PSH标志。

3. 检查是否使用了紧急模式。

4. 检查是否需要使用自动阻塞。

5. 尽可能地把发送队列中的skb给发送出去。

static void tcp_push(struct sock *sk, int flags, int mss_now, int nonagle, int size_goal)
{
struct tcp_sock *tp = tcp_sk(sk);
struct sk_buff *skb; /* 如果没有未发送过的数据 */
if (! tcp_send_head(sk))
return; /* 发送队列的最后一个skb */
skb = tcp_write_queue_tail(sk); /* 如果接下来没有更多的数据需要发送,或者距离上次PUSH后又有比较多的数据,
* 那么就需要设置PSH标志,让接收端马上把接收缓存中的数据提交给应用程序。
*/
if (! (flags & MSG_MORE) || forced_push(tp))
tcp_mark_push(tp, skb); /* 如果设置了MSG_OOB标志,就记录紧急指针 */
tcp_mark_urg(tp, flags); /* 如果需要自动阻塞小包 */
if (tcp_should_autocork(sk, skb, size_goal)) {
/* avoid atomic op if TSQ_THROTTED bit is already set, 设置阻塞标志位 */
if (! test_bit(TSQ_THROTTLED, &tp->tsq_flags)) {
NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPAUTOCORKING);
set_bit(TSQ_THROTTLED, &tp->tsq_flags);
} /* It is possible TX completion already happened before we set TSQ_THROTTED.
* 我的理解是,当提交给IP层的数据包都发送出去后,sk_wmem_alloc的值就会变小,
* 此时这个条件就为假,之后可以发送被阻塞的数据包了。
*/
if (atomic_read(&sk->sk_wmem_alloc) > skb->truesize)
return;
} /* 如果之后还有更多的数据,那么使用TCP CORK,显式地阻塞发送 */
if (flags & MSG_MORE)
nonagle = TCP_NAGLE_CORK; /* 尽可能地把发送队列中的skb发送出去。
* 如果发送失败,检查是否需要启动零窗口探测定时器。
*/
__tcp_push_pending_frames(sk, mss_now, nonagle);
}

判断是否要设置PSH标志:

如果此时发送队列的最后一个字节序号,和上次PSH的最后一个字节序号,

它们的间隔超过了对端通告过的最大接收窗口的一半,就需要设置。

static inline bool forced_push(const struct tcp_sock *tp)
{
/* write_seq:发送队列最后一个字节的序号+1
* pushed_seq:上次PUSH的最后一个字节
* max_window:对端层通告过的最大接收窗口
*/
return after(tp->write_seq, tp->pushed_seq + (tp->max_window >> 1));
} static inline void tcp_mark_push(struct tcp_sock *tp, struct sk_buff *skb)
{
TCP_SKB_CB(skb)->tcp_flags |= TCPHDR_PSH; /* 设置PSH标志 */
tp->pushed_seq = tp->write_seq; /* 记录本次PUSH的最后一个字节序号 */
} static inline void tcp_mark_urg(struct tcp_sock *tp, int flags)
{
if (flags & MSG_OOB)
tp->snd_up = tp->write_seq;
}

tcp_push_pending_frames()和__tcp_push_pending_frames()简单的封装了下tcp_write_xmit()。

从tcp_write_xmit()开始,TCP层才真正开始发送数据。

/* Push out any pending frames which were held back due to TCP_CORK
* or attempt at coalescing tiny packets.
* The socket must be locked by the caller.
*/
void __tcp_push_pending_frames(struct sock *sk, unsigned int cur_mss, int nonagle)
{
/* If we are closed, the bytes will have to remain here.
* In time closedown will finish, we empty the write queue and
* all will be happy.
*/
if (unlikely(sk->sk_state == TCP_CLOSE))
return; /* 如果发送失败 */
if (tcp_write_xmit(sk, cur_mss, nonagle, 0, sk_gfp_atomic(sk, GFP_ATOMIC)))
tcp_check_probe_timer(sk); /* 检查是否需要启用0窗口探测定时器*/

tcp_autocorking

当应用程序连续地发送小包时,如果能够把这些小包合成一个全尺寸的包再发送,无疑可以减少

总的发包个数。tcp_autocorking的思路是当规则队列Qdisc、或网卡的发送队列中有尚未发出的

数据包时,那么就延迟小包的发送,等待应用层的后续数据,直到Qdisc或网卡发送队列的数据

包成功发送出去为止。

sysctl_tcp_autocorking - BOOLEAN

Enable TCP auto corking:

When  applications do consecutive small write()/sendmsg() system calls,

we try to coalesce these small writes as much as possible, to lower total

amount of sent packets. This is done if at least one prior packet for the flow

is waiting in Qdisc queues or device transmit queue. Applications can still

use TCP_CORK for optimal behavior when they know how/when to uncork

their sockets.

Default: 1

Patch: http://lwn.net/Articles/576263/

同时满足以下条件时,tcp_push()才会自动阻塞:

1. 数据包为小包,即数据长度小于最大值。

2. 使用了tcp_autocorking,这个值默认为1。

3. 此数据包不是发送队列的第一个包,即前面有数据包被发送了。

4. Qdisc或Nic queues必须有数据包,而不能只是纯ACK包。

/* If a not yet filled skb is pushed, do not send it if we have data packets
* in Qdisc or NIC queues: Because TX completion will happen shortly,
* it gives a chance to coalesce future sendmsg() payload into this skb,
* without need for a timer, and with no latency trade off.
* As packts containing data payload have a bigger truesize than pure
* acks (dataless) packets, the last checks prevent autocorking if we only
* have an ACK in Qdisc/NIC queues, or if TX completion was delayed
* after we processed ACK packet.
*/ static bool tcp_should_autocork(struct sock *sk, struct sk_buff *skb, int size_goal)
{
return skb->len < size_goal && sysctl_tcp_autocorking &&
skb != tcp_write_queue_head(sk) &&
atomic_read(&sk->sk_wmem_alloc) > skb->truesize;
}

Q:什么时候会取消自动阻塞呢?

A:在tcp_push()中会检查,if (atomic_read(&sk->sk_wmem_alloc) > skb->truesize)

当提交给IP层的数据包都发送出去后,sk_wmem_alloc的值就会变小,此时这个条件就为假,

之后可以发送被阻塞的数据包了。

数据的复制

tcp_sendmsg()的一项主要工作,就是把用户层的数据填充到发送队列的skb中。

skb_availroom返回skb的data room大小,如果有非线性数据区,就返回0。

/*
* bytes at buffer end.
* Return the number of bytes of free space at the tail of an sk_buff
* allocated by sk_stream_alloc()
*/
static inline int skb_availroom(const struct sk_buff *skb)
{
/* skb->data_len不为零,表示有非线性的数据区 */
if (skb_is_nonlinear(skb))
return 0; /* data room的大小 */
return skb->end - skb->tail - skb->reserved_tailroom;
}

验证用户空间的数据可读,拷贝用户空间的数据到内核空间。如果需要TCP自己计算校验和,

那么同时计算用户层数据的校验和。

static inline int skb_add_data_nocache(struct sock *sk, struct sk_buff *skb,
char __user *from, int copy)
{
int err, offset = skb->len; /* 拷贝用户空间的数据到内核空间,同时计算校验和 */
err = skb_do_copy_data_nocache(sk, skb, from, skb_put(skb, copy), copy, offset); /* 如果拷贝失败,恢复skb->len和data room的大小 */
if (err)
__skb_trim(skb, offset); return err;
} static inline int skb_do_copy_data_nocache(struct sock *sk, struct sk_buff *skb,
char __user *from, char *to, int copy, int offset)
{
/* 需要TCP自己计算校验和 */
if (skb->ip_summed == CHECKSUM_NONE) {
int err = 0; /* 拷贝用户空间的数据到内核空间,同时计算用户数据的校验和 */
__wsum csum = csum_and_copy_from_user(from, to, copy, 0, &err); skb->csum = csum_block_add(skb->csum, csum, offset); /* 累加校验和 */ } else if (sk->sk_route_caps & NETIF_F_NOCACHE_COPY) {
if (! access_ok(VERIFY_READ, from, copy) ||
__copy_from_user_nocache(to, from, copy)) return -EFAULT; } else if (copy_from_user(to, from, copy))
return -EFAULT; return 0;
}

向下扩大data room,返回扩大之前的tail指针。

/* add data to a buffer.
* This function extends the used data area of the buffer. If this would
* exceed the total buffer size the kernel will panic. A pointer to the first
* byte of the extra data is returned.
*/
unsigned char *skb_put(struct sk_buff *skb, unsigned int len)
{
unsigned char *tmp = skb_tail_pointer(skb);
SKB_LINEAR_ASSERT(skb); skb->tail += len;
skb->len += len;
if (unlikely(skb->tail > skb->end))
skb_over_panic(skb, len, __builtin_return_address(0)); return tmp;
} static inline void __skb_trim(struct sk_buff *skb, unsigned int len)
{
if (unlikely(skb_is_nolinear(skb))) {
WARN_ON(1);
return;
} skb->len = len; /* 恢复原来的长度 */
skb_set_tail_pointer(skb, len); /* 恢复data room的大小 */
}

拷贝用户空间的数据到内核空间,同时计算校验和。同时更新skb->len、skb->data_len、

skb->truesize、sk->sk_wmem_queued和sk->sk_forward_alloc。

static inline int skb_copy_to_page_nocache(struct sock *sk, char __user *from,
struct sk_buff *skb, struct page *page, int off, int copy)
{
int err; /* 拷贝用户空间的数据到内核空间,同时计算校验和 */
err = skb_do_copy_data_nocache(sk, skb, from, page_address(page) + off,
copy, skb->len);
if (err)
return err; skb->len += copy;
skb->data_len += copy;
skb->truesize += copy; sk->sk_wmem_queued += copy;
sk_mem_charge(sk, copy); return 0;
}

TCP的发送系列 — tcp_sendmsg()的实现(二)的更多相关文章

  1. TCP的发送系列 — tcp_sendmsg()的实现(一)

    主要内容:Socket发送函数在TCP层的实现 内核版本:3.15.2 我的博客:http://blog.csdn.net/zhangskd 上一篇blog讲的是send().sendto().sen ...

  2. TCP的发送系列 — 发送缓存的管理(二)

    主要内容:从TCP层面判断发送缓存的申请是否合法,进程因缺少发送缓存而进行睡眠等待. 因为有发送缓存可写事件而被唤醒. 内核版本:3.15.2 我的博客:http://blog.csdn.net/zh ...

  3. TCP的发送系列 — 发送缓存的管理(一)

    主要内容:TCP发送缓存的初始化.动态调整.申请和释放. 内核版本:3.15.2 我的博客:http://blog.csdn.net/zhangskd 数据结构 TCP对发送缓存的管理是在两个层面上进 ...

  4. TCP的核心系列 — ACK的处理(二)

    本文主要内容:tcp_ack()中的一些细节,如发送窗口的更新.持续定时器等. 内核版本:3.2.12 Author:zhangskd @ csdn 发送窗口的更新 什么时候需要更新发送窗口呢? (1 ...

  5. Android系列之网络(二)----HTTP请求头与响应头

    ​[声明] 欢迎转载,但请保留文章原始出处→_→ 生命壹号:http://www.cnblogs.com/smyhvae/ 文章来源:http://www.cnblogs.com/smyhvae/p/ ...

  6. IPFS系列 多节点搭建 二

    IPFS系列 多节点搭建 二 上一篇介绍了IPFS的分布式点对点超媒体传输协议的背景和安装介绍,本篇将继续指导搭建多节点的IPFS私有网络 文件服务.如果没还没开始搭建IPFS节点的小伙伴, 请戳此链 ...

  7. Spring Boot干货系列:(十二)Spring Boot使用单元测试(转)

    前言这次来介绍下Spring Boot中对单元测试的整合使用,本篇会通过以下4点来介绍,基本满足日常需求 Service层单元测试 Controller层单元测试 新断言assertThat使用 单元 ...

  8. 《手把手教你》系列技巧篇(二十七)-java+ selenium自动化测试- quit和close的区别(详解教程)

    1.简介 尽管有的小伙伴或者童鞋们觉得很简单,不就是关闭退出浏览器,但是宏哥还是把两个方法的区别说一下,不然遇到坑后根本不会想到是这里的问题. 2.源码 本文介绍webdriver中关于浏览器退出操作 ...

  9. C# TCP socket发送大数据包时,接收端和发送端数据不一致 服务端接收Receive不完全

    简单的c# TCP通讯(TcpListener) C# 的TCP Socket (同步方式) C# 的TCP Socket (异步方式) C# 的tcp Socket设置自定义超时时间 C# TCP ...

随机推荐

  1. bzoj 3451 Normal

    Description 某天WJMZBMR学习了一个神奇的算法:树的点分治! 这个算法的核心是这样的: 消耗时间=0 Solve(树 a) 消耗时间 += a 的 大小 如果 a 中 只有 1 个点 ...

  2. kafka快速入门

    一.kafka简介 kafka,ActiveMQ,RabbitMQ是当今最流行的分布式消息中间件,其中kafka在性能及吞吐量方面是三者中的佼佼者,不过最近查阅官网时,官方与它的定义为一个分布式流媒体 ...

  3. WebStorm 快捷键一览

    查找/代替 快捷键 说明 ctrl+shift+N 通过文件名快速查找工程内的文件(必记) ctrl+shift+alt+N 通过一个字符快速查找位置(必记) ctrl+F 在文件内快速查找代码 F3 ...

  4. java实现微信支付之扫码支付

    本文直接从代码调用微信扫码支付讲起.账号配置,参数生成等请参考官方文档:https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=6_1 微信 ...

  5. Mysql根据一个基库生成其他库与其不同的库升级脚本

    今天研究了一下不同数据库之间如何做同步.弄了一个升级工具类,希望以后还能有所帮助. public class UpgradeDataBase { public static void main(Str ...

  6. 模仿天猫实战【SSM版】——项目起步

    前言:现在自己的学习似乎遇到了瓶颈,感觉学习了 SSM 之后有一些迷茫,不知道接下来该往哪里去努力了,我觉得这是个很不好的状态,为了度过这段时期,我准备把天猫模仿下来(给自己找点事做)之后开始去巩固 ...

  7. 用burpsuite暴力破解后台

    [实验原理] Burp Suite是Web应用程序测试的最佳工具之一,其多种功能执行各种任务.请求的拦截和修改,扫描web应用程序漏洞,以暴力破解登录表单,执行会话令牌等多种的随机性检查. Burp ...

  8. EXISTS的使用详解

    .exists的使用场合: exists 用于只能用于子查询,可以替代in,若匹配到结果,则退出内部 查询,并将条件标志为true,传回全部结果资料,in 不管匹配到匹配不到都 全部匹配完毕,使用ex ...

  9. Node.js HTTPS

    稳定性: 3 - 稳定 HTTPS 是基于 TLS/SSL 的 HTTP 协议.在 Node 里作为单独的模块来实现. 类: https.Server 这是 tls.Server 的子类,并且和 ht ...

  10. Dapper连接与事务的简单封装

    增删改查方面,已经有Dapper.Extension这么强大的工具了,我也实在没啥好写的,就随手写了个看起来比较优雅的连接与事务的封装.在之后使用Dapper.Extension类库时,完全可以照搬进 ...