主要内容: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]4755: [Jsoi2016]扭动的回文串

    Time Limit: 10 Sec  Memory Limit: 512 MB Description JYY有两个长度均为N的字符串A和B. 一个"扭动字符串S(i,j,k)由A中的第i ...

  2. 51 nod 1427 文明 (并查集 + 树的直径)

    1427 文明 题目来源: CodeForces 基准时间限制:1.5 秒 空间限制:131072 KB 分值: 160 难度:6级算法题   安德鲁在玩一个叫“文明”的游戏.大妈正在帮助他. 这个游 ...

  3. 51 nod 1023 石子归并 V3(GarsiaWachs算法)

    1023 石子归并 V3基准时间限制:2 秒 空间限制:131072 KB 分值: 320 难度:7级算法题 N堆石子摆成一条线.现要将石子有次序地合并成一堆.规定每次只能选相邻的2堆石子合并成新的一 ...

  4. Codeforces April Fools Contest 2017

    都是神题,我一题都不会,全程听学长题解打代码,我代码巨丑就不贴了 题解见巨神博客 假装自己没有做过这套

  5. Codeforces Round #406 (Div. 1)

    B题打错调了半天,C题想出来来不及打,还好没有挂题 AC:AB Rank:96 Rating:2125+66->2191 A.Berzerk 题目大意:有一个东东在长度为n的环上(环上点编号0~ ...

  6. [bzoj1187][HNOI2007]神奇游乐园

    来自FallDream的博客,未经允许,请勿转载,谢谢, 经历了一段艰辛的旅程后,主人公小P乘坐飞艇返回.在返回的途中,小P发现在漫无边际的沙漠中,有一块狭长的绿地特别显眼.往下仔细一看,才发现这是一 ...

  7. bzoj1293[SCOI2009]生日礼物 尺取法

    1293: [SCOI2009]生日礼物 Time Limit: 10 Sec  Memory Limit: 162 MBSubmit: 2838  Solved: 1547[Submit][Stat ...

  8. 关于jsp中的文件下载

    第一种采用转发的方式: package cn.jbit.download.servlet; import java.io.IOException; import javax.servlet.Reque ...

  9. Vue2学习结合bootstrapTable遇到的问题

    Vue2学习 项目中在使用bootstrapTable的时候,在table里面会有操作结合vue使用过程中点击相应的操作不会起作用 解决办法 1.把事件绑定到父元素上即可,但要判断什么样的需要点击,用 ...

  10. Linux 下 HTTP连接超时

    将项目部署到现场环境,HTTP请求莫名奇妙的连接超时,通过抓包定位了问题,是请求的IP被禁止掉.其中用到了抓包,将记录记录于此. tcpdump host 120.197.89.51 -i any - ...