主要内容: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. 小明搬家_NOI导刊2010提高(05)

    题目描述 小明要搬家了,大家都来帮忙. 小明现在住在第N楼,总共K个人要把X个大箱子搬上N楼. 最开始X个箱子都在1楼,但是经过一段混乱的搬运已经乱掉了.最后大家发现这样混乱地搬运过程效率太低了,于是 ...

  2. [BZOJ]3532: [Sdoi2014]Lis

    Time Limit: 10 Sec  Memory Limit: 512 MB Description 给定序列A,序列中的每一项Ai有删除代价Bi和附加属性Ci.请删除若干项,使得4的最长上升子序 ...

  3. 【SPOJ839】Optimal Marks 网络流

    You are given an undirected graph G(V, E). Each vertex has a mark which is an integer from the range ...

  4. hdu 5463(水水)

    Sample Input 2 3 2 33 3 33 2 33 10 5 467 6 378 7 309 8 499 5 320 3 480 2 444 8 391 5 333 100 499   S ...

  5. 【Halum操作-UVA 11478】

    ·英文题,述大意:      输入有向图一个(什么边的端点啊,边权啊).每次可以选择一个节点和一个整数,然后把这个结点的出边边权加上该整数,入边边权减去该整数,目标:使得所有边的最小值非负且尽量大. ...

  6. hdu 5505(GT and numbers)

    题意: 给你a和b,a每次和它的因子相乘得到一个新的a,求多少次后可以得到b. 输入样例 3 1 1 1 2 2 4 输出样例 0 -1 1 思路: 每次找出a和b/a的最大公约数(即当前a想得到b能 ...

  7. 计科1702冯亚杰C语言程序设计预备作业

    阅读邹欣老师的博客--师生关系,针对文中的几种师生关系谈谈你的看法,你期望的师生关系是什么样的? 答:首先老师和学生之间要互相尊重,我认为这是必要的.在第一点的基础上师生要互相帮助,互相配合,共同进步 ...

  8. Spring的注解@Qualifier小结

    有以下接口: public interface EmployeeService { public EmployeeDto getEmployeeById(Long id); } 有两个实现类: @Se ...

  9. tree的遍历--广度优先遍历

    一.二叉树demo var tree = { value: '一', left: { value: '二', left: { value: '四', right: { value: '六' } } } ...

  10. 13.QT-QMainWindow组件使用

    QMainWindow介绍 主窗口是与用户进行长时间交互的顶层窗口,比如记事本 主窗口通常是应用程序启动后显示的第一个窗口 QMainWindow是Qt中主窗口的基类,继承于QWidget,如下图所示 ...