主要内容:从TCP层面判断发送缓存的申请是否合法,进程因缺少发送缓存而进行睡眠等待、

因为有发送缓存可写事件而被唤醒。

内核版本:3.15.2

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

TCP的发送缓存管理发生在两个层面上:单个Socket和整个TCP层。

上一篇blog讲述了单个Socket层面上的发送缓存管理,现在来看下整个TCP层面上的发送缓存管理。

从TCP层面判断发送缓存的申请是否合法

在申请发送缓存时,会调用sk_stream_memory_free()来判断sock发送队列的大小是否超过

了sock发送缓存的上限,如果超过了,就要进入睡眠来等待sock的发送缓存可写事件。

这是从单个socket层面来判断是否允许分配发送缓存。

在调用sk_stream_alloc_skb()申请完发送缓存后,还要从TCP层面来判断此次的申请是否合法。

如果不合法,就使用__kfree_skb()来释放申请好的skb。可见发送缓存的申请,需要经过两重关卡。

从TCP层面来判断发送缓存的申请是否合法,需要考虑整个TCP层面的内存使用量,以及此socket

的发送缓存使用量。sk->sk_forward_alloc为sock预分配缓存的大小,是sock事先分配好还未使用的内存。

当申请新的发送缓存后,如果发现sk->sk_forward_alloc < skb->truesize,即预分配缓存用光了,

才需要调用sk_wme_schedule()来从TCP层面判断合法性,否则不用再做检查。

static inline bool sk_wmem_schedule(struct sock *sk, int size)
{
/* TCP层是有统计内存使用的,所以条件为假 */
if (! sk_has_account(sk))
return true; /* 如果本次使用的内存skb->truesize,少于sk预分配且未使用的缓存的大小,那么不用进行
* 进一步检查。否则需要从TCP层面判断此次发送缓存的申请是否合法。
*/
return size <= sk->sk_forward_alloc || __sk_mem_schedule(sk, size, SK_MEM_SEND);
} static inline bool sk_has_account(struct sock *sk)
{
/* return ture if protocol supports memory accounting */
return !! sk->sk_prot->memory_allocated;
} /* return minimum truesize of one skb containing X bytes of data */
#define SKB_TRUESIZE(X) ((X) + \
SKB_DATA_ALIGN(sizeof(struct sk_buff)) + \
SKB_DATA_ALIGN(sizeof(struct skb_shared_info)))

__sk_mem_schedule()用来从TCP层面判断此次发送缓存的申请是否合法,如果是合法的,

会更新预分配缓存sk->sk_forward_alloc和TCP层总的内存使用量tcp_memory_allocated,

后者的单位为页。

Q:哪些情况下此次发送缓存的申请是合法的呢?

1. TCP层的内存使用量低于最小值sysctl_tcp_mem[0]。

2. sock的发送缓存使用量低于最小值sysctl_tcp_wmem[0]。

3. TCP层不处于内存压力状态,即TCP层的内存使用量低于sysctl_tcp_wmem[1]。

4. TCP层处于内存压力状态,但当前socket使用的内存还不是太高。

5. TCP层的内存使用量超过最大值sysctl_tcp_wmem[2],降低发送缓存的上限后,发送队列的总大小超过

了发送缓存的上限了。因此之后会进入睡眠等待,所以也判为合法的。

可以看到,在绝大多数情况下发送缓存的申请都是合法的,除非TCP的内存使用量已经到极限了。

除了判断此次发送缓存申请的合法性,__sk_mem_schedule()还做了如下事情:

1. 如果TCP的内存使用量低于最小值sysctl_tcp_mem[0],就清零TCP的内存压力标志tcp_memory_pressure。

2. 如果TCP的内存使用量高于压力值sysclt_tcp_mem[1],把TCP的内存压力标志tcp_memory_pressure置为1。

3. 如果TCP的内存使用量高于最大值sysctl_tcp_mem[2],就减小sock发送缓存的上限sk->sk_sndbuf。

返回值为1时,表示发送缓存的申请是合法的;返回值为0时,表示不合法。

/* increase sk_forward_alloc and memory_allocated
* @sk: socket
* @size: memory size to allocate
* @kind: allocation type
* If kind is SK_MEM_SEND, it means wmem allocation.
* Otherwise it means rmem allocation. This function assumes that
* protocols which have memory pressure use sk_wmem_queued as
* write buffer accounting.
*/ int __sk_mem_schedule(struct sock *sk, int size, int kind)
{
struct proto *prot = sk->sk_prot; /* 实例为tcp_prot */
int amt = sk_mem_pages(size); /* 把size转换为页数,向上取整 */
long allocated;
int parent_status = UNDER_LIMIT; sk->sk_forward_alloc += amt * SK_MEM_QUANTUM; /* 更新预分配缓存的大小 */ /* 更新后的TCP内存使用量tcp_memory_allocated,单位为页 */
allocated = sk_memory_allocated_add(sk, amt, &parent_status); /* Under limit. 如果TCP的内存使用量低于最小值sysctl_tcp_mem[0] */
if (parent_status == UNDER_LIMIT && allocated <= sk_prot_mem_limits(sk, 0)) {
sk_leave_memory_pressure(sk); /* 清零TCP层的内存压力标志tcp_memory_pressure */
return 1;
} /* Under pressure. (we or our parents).
* 如果TCP的内存使用量高于压力值sysclt_tcp_mem[1],把TCP层的内存压力标志
* tcp_memory_pressure置为1。
*/
if ((parent_status > SOFT_LIMIT) || allocated > sk_prot_mem_limits(sk, 1))
sk_enter_memory_pressure(sk); /* Over hard limit (we or our parents).
* 如果TCP层的内存使用量高于最大值sysctl_tcp_mem[2],就减小sock发送缓存的上限
* sk->sk_sndbuf。
*/
if ((parent_status == OVER_LIMIT || (allocated > sk_prot_mem_limits(sk, 2)))
goto suppress_allocation; /* guarantee minimum buffer size under pressure */
/* 不管是在发送还是接收时,都要保证sock至少有sysctl_tcp_{r,w}mem[0]的内存可用 */
if (kind == SK_MEM_RECV) {
if (atomic_read(&sk->sk_rmem_alloc) < prot->sysctl_rmem[0])
return 1; } else { /* SK_MEM_SEND */
if (sk->sk_type == SOCK_STREAM) {
if (sk->sk_wmem_queued < prot->sysctl_wmem[0])
return 1;
} else if (atomic_read(&sk->sk_wmem_alloc) < prot->sysctl_wmem[0])
return 1;
} if (sk_has_memory_pressure(sk)) {
int alloc; /* 如果TCP不处于内存压力状态,直接返回 */
if (! sk_under_memory_pressure(sk))
return 1; alloc = sk_sockets_allocated_read_positive(sk); /* 当前使用TCP的socket个数 */ /* 如果当前socket使用的内存还不是太高时,返回真 */
if (sk_prot_mem_limits(sk, 2) > alloc * sk_mem_pages(sk->sk_wmem_queued +
atomic_read(&sk->sk_rmem_alloc) + sk->sk_forward_alloc))
return 1;
} suppress_allocation:
if (kind == SK_MEM_SEND && sk->sk_type == SOCK_STREAM) { /* 减小sock发送缓冲区的上限,使得sndbuf不超过发送队列总大小的一半,
* 不低于两个数据包的MIN_TRUESIZE。
*/
sk_stream_moderate_sndbuf(sk); /* Fail only if socket is under its sndbuf.
* In this case we cannot block, so that we have to fail.
*/
if (sk->sk_wmem_queued + size >= sk->sk_sndbuf)
return 1;
}
trace_sock_exceed_buf_limit(sk, prot, allocated); /* 走到这里,判定此次发送缓存的申请为不合法的,撤销之前的内存计数更新 */
/* Alas. Undo changes. */
sk->sk_forward_alloc -= amt * SK_MEM_QUANTUM;
sk_memory_allocated_sub(sk, amt);
return 0;
} /* 把字节数amt转换为页数,向上取整 */
static inline int sk_mem_pages(int amt)
{
return (amt + SK_MEM_QUANTUM - 1) >> SK_MEM_QUANTUM_SHIFT;
}
#define SK_MEM_QUANTUM ((int) PAGE_SIZE) /* 返回更新后的TCP内使用量tcp_memory_allocated,单位为页 */
static inline long sk_memory_allocated_add(struct sock *sk, int amt, int *parent_status)
{
struct proto *prot = sk->sk_prot; /* Cgroup相关,此处略过 */
if (mem_cgroup_sockets_enabled && sk->sk_cgrp) {
...
} return atomic_long_add_return(amt, prot->memory_allocated);
}

sysctl_tcp_mem[0]:最小值

sysctl_tcp_mem[1]:压力值

sysctl_tcp_mem[2]:最大值

static inline long sk_prot_mem_limits(const struct sock *sk, int index)
{
long *prot = sk->sk_prot->sysctl_mem; /* Cgroup相关 */
if (mem_cgroup_sockets_enabled && sk->sk_cgrp)
prot = sk->sk_cgrp->sysctl_mem; return prot[index];
}

因缺少发送缓存而睡眠等待

在tcp_sendmsg()中,如果发送队列的总大小sk_wmem_queued大于等于发送缓存的上限sk_sndbuf,

或者发送缓存中尚未发送的数据量超过了用户的设置值,就进入睡眠等待。

如果申请发送缓存失败了,也会进行睡眠等待。

(1) 判断条件

sk_stream_memory_free()用来判断sock是否有剩余的发送缓存。

static inline bool sk_stream_memory_free(const struct sock *sk)
{
if (sk->sk_wmem_queued >= sk->sk_sndbuf)
return false; return sk->sk_prot->stream_memory_free ? sk->sk_prot->stream_memory_free(sk) : true;
} static inline bool tcp_stream_memory_free(const struct sock *sk)
{
const struct tcp_sock *tp = tcp_sk(sk);
u32 notsent_bytes = tp->write_seq - tp->snd_nxt; /* 尚未发送的数据大小 */ /* 当尚未发送的数据,少于配置的值时,才返回真。
* 这是为了避免发送缓存占用过多的内存。
*/
return notsent_bytes < tcp_notsent_lowat(tp);
}

如果有使用TCP_NOTSENT_LOWAT选项,则使用用户设置的值。

否则使用sysctl_tcp_notsent_lowat,默认为无穷大。

static inline u32 tcp_notsent_lowat(const struct tcp_sock *tp)
{
return tp->notsent_lowat ?: sysctl_tcp_notsent_lowat;
}

(2) 睡眠等待

如果发送队列的总大小sk_wmem_queued大于等于发送缓存的上限sk_sndbuf,

或者发送缓存中尚未发送的数据量超过了用户的设置,就进入等待。

如果因为TCP层的内存不足,导致申请发送缓存失败了,也会进行睡眠等待。

Q:需要睡眠等待多长的时间呢?

需要分两种情况:

1. 等待的原因是TCP层的内存不足。

刚进入函数时,会判断sock的发送缓存是否达到了上限。

如果此时sock尚有发送缓存额度,说明是TCP层内存不足导致发送缓存申请失败的,

设置等待时间为一个2~202ms的伪随机数,超时后就结束等待。

2. 等待的原因是sock的发送缓存不足。

在睡眠的过程中,当有可用的发送缓存时,进程会被唤醒,从而结束等待。

否则达到超时时间后,返回错误。

/* Wait for more memory for a socket
* @sk: socket to wait for memory
* @timeo_p: for how long
*/ int sk_stream_wait_memory(struct sock *sk, long *timeo_p)
{
int err = 0;
long vm_wait = 0;
long current_timeo = *timeo_p;
DEFINE_WAIT(wait); /* 初始化等待任务 */ /* 如果sock还有发送缓存额度,说明是TCP层内存不足导致的。
* 初始化等待时间为一个2~202ms的伪随机数。
*/
if (sk_stream_memory_free(sk))
current_timeo = vm_wait = (prandom_u32() % (HZ / 5)) + 2; while (1) {
/* 设置异步发送时,发送缓存不够的标志 */
set_bit(SOCK_ASYNC_NOSPACE, &sk->sk_socket->flags); /* 把等待任务加入到socket等待队列头部,把进程的状态设为TASK_INTERRUPTIBLE */
prepare_to_wait(sk_sleep(sk), &wait, TASK_INTERRUPTIBLE); /* 如果连接有错误,或者不允许发送数据了,那么返回-EPIPE */
if (sk->sk_err || (sk->sk_shutdown & SEND_SHUTDOWN))
goto do_error; /* 如果是非阻塞的,或者等待超时了,返回-EAGAIN */
if (! *timeo_p)
goto do_nonblock; /* 如果进程有待处理的信号,如果没有设置超时时间返回-ERESTARTSYS,
* 否则返回-EINTR.
*/
if (signal_pending(current))
goto do_interrupte; clear_bit(SOCK_ASYNC_NOSPACE, &sk->sk_socket->flags); /* 如果sock已经有可用的发送缓存了。并满足以下任一条件:
* 1. 此次等待是由于sock的发送缓存不足。
* 2. 此次等待是由于TCP层内存不足,经过了一次睡眠vm_wait设为0。
*/
if (sk_stream_memory_free(sk) && ! vm_wait)
break; set_bit(SOCK_NOSPACE, &sk->sk_socket->flags);
sk->sk_write_pending++; /* 进入睡眠等待 */
sk_wait_event(sk, ¤tt_timeo, sk->sk_err ||
(sk->sk_shutdown & SEND_SHUTDOWN) ||
(sk_stream_memory_free(sk) && ! vm_wait));
sk->sk_write_pending--; /* 如果vm_wait不为0,睡眠2~202ms后,就把vm_wait清零了 */
if (vm_wait) {
vm_wait -= current_timeo;
current_timo = *timeo_p; if (current_timeo != MAX_SCHEDULE_TIMEOUT &&
(current_timeo -= vm_wait) < 0)
current_timeo = 0; vm_wait = 0;
} *timeo_p = current_timeo; /* 更新发送的超时等待时间 */
} out:
/* 把等待任务从等待队列中删除,把当前进程的状态设为TASK_RUNNING */
finish_wait(sk_sleep(sk), &wait);
return err; do_error:
err = -EPIPE;
goto out; do_nonblock:
err = -EAGAIN;
goto out; do_interrupted:
err = sock_intr_errno(*timeo_p);
goto out;
}

因有发送缓存可写事件而被唤醒

sk->sk_write_space的实例为sock_def_write_space()。

如果socket是SOCK_STREAM类型的,那么函数指针的值会更新为sk_stream_write_space()。

sk_stream_write_space()在TCP中的调用路径为:

tcp_rcv_established / tcp_rcv_state_process

tcp_data_snd_check

tcp_check_space

tcp_new_space

static void tcp_check_space(struct sock *sk)
{
/* 如果发送队列中有skb被释放了 */
if (sock_flag(sk, SOCK_QUEUE_SHRUNK)) { sock_reset_flag(sk, SOCK_QUEUE_SHRUNK); /* 如果设置了同步发送时,发送缓存不足的标志 */
if (sk->sk_socket && test_bit(SOCK_NOSPACE, &sk->sk_socket->flags))
tcp_new_space(sk); /* 更新发送缓存 */
}
}
/* When incoming ACK allowed to free some skb from write_queue,
* we remember this event in flag SOCK_QUEUE_SHRUNK and wake up socket
* on the exit from tcp input handler.
*/
static void tcp_new_space(struct sock *sk)
{
struct tcp_sock *tp = tcp_sk(sk); if (tcp_should_expand_sndbuf(sk)) {
tcp_sndbuf_expand(sk);
tp->snd_cwnd_stamp = tcp_time_stamp;
} /* 检查是否需要触发有缓存可写事件 */
sk->sk_write_space(sk);
}
void sk_stream_write_space(struct sock *sk)
{
struct socket *sock = sk->sk_socket;
struct socket_wq *wq; /* 等待队列和异步通知队列 */ /* 如果剩余的发送缓存不低于发送缓存上限的1/3,且尚未发送的数据不高于一定值时 */
if (sk_stream_is_writeable(sk) && sock) {
clear_bit(SOCK_NOSPACE, &sock->flags); /* 清除发送缓存不够的标志 */ rcu_read_lock();
wq = rcu_dereference(sk->sk_wq); /* socket的等待队列和异步通知队列 */
if (wq_has_sleeper(wq)) /* 如果等待队列不为空,则唤醒一个睡眠进程 */
wake_up_interruptible_poll(&wq->wait, POLLOUT | POLLWRNORM | POLLWRBAND); /* 异步通知队列不为空,且允许发送数据时。
* 检测sock的发送队列是否曾经到达上限,如果有的话发送SIGIO信号,告知异步通知队列上
* 的进程有发送缓存可写。
*/
if (wq && wq->fasync_list && !(sk->sk_shutdown & SEND_SHUTDOWN))
sock_wake_async(sock, SOCK_WAKE_SPACE, POLL_OUT); rcu_read_unlock();
}
} #define wake_up_interruptible_poll(x, m) \
__wake_up(x, TASK_INTERRUPTIBLE, 1, (void *) (m))

如果剩余的发送缓存大于发送缓存上限的1/3,且尚未发送的数据少于一定值时,才会触发有发送

缓存可写的事件。

static inline bool sk_stream_is_writeable(const struct sock *sk)
{
return sk_stream_wspace(sk) >= sk_stream_min_wspace(sk) &&
sk_stream_memory_free(sk);
} static inline int sk_stream_wspace(const struct sock *sk)
{
return sk->sk_sndbuf - sk->sk_wmem_queued;
} static inline int sk_stream_min_wspace(const struct sock *sk)
{
return sk->sk_wmem_queued >> 1;
}

检查尚未发送的数据是否已经够多了,如果超过了用户设置的值,就不用触发有发送缓存可写事件,

以免使用过多的内存。

static inline bool sk_stream_memory_free(const struct sock *sk)
{
if (sk->sk_wmem_queued >= sk->sk_sndbuf)
return false; return sk->sk_prot->stream_memory_free ? sk->sk_prot->stream_memory_free(sk) : true;
} static inline bool tcp_stream_memory_free(const struct sock *sk)
{
const struct tcp_sock *tp = tcp_sk(sk);
u32 notsent_bytes = tp->write_seq - tp->snd_nxt; /* 尚未发送的数据大小 */ /* 当尚未发送的数据,少于配置的值时,才触发有发送缓存可写的事件。
* 这是为了避免发送缓存占用过多的内存。
*/
return notsent_bytes < tcp_notsent_lowat(tp);
}

如果有使用TCP_NOTSENT_LOWAT选项,则使用用户设置的值。

否则使用sysctl_tcp_notsent_lowat,默认为无穷大。

static inline u32 tcp_notsent_lowat(const struct tcp_sock *tp)
{
return tp->notsent_lowat ?: sysctl_tcp_notsent_lowat;
}

TCP的发送系列 — 发送缓存的管理(二)的更多相关文章

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

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

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

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

  3. TCP的发送系列 — tcp_sendmsg()的实现(二)

    主要内容:Socket发送函数在TCP层的实现 内核版本:3.15.2 我的博客:http://blog.csdn.net/zhangskd 在上篇blog中分析了tcp_sendmsg()这个主要函 ...

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

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

  5. TCP/IP详解--发送ACK和RST的场景

    在有以下几种情景,TCP会把ack包发出去: 1.收到1个包,启动200ms定时器,等到200ms的定时器到点了(第二个包没来),于是对这个包的确认ack被发送.这叫做“延迟发送”: 2.收到1个包, ...

  6. TCP和UDP 协议发送数据包的大小

    在进行UDP编程的时候,我们最容易想到的问题就是,一次发送多少bytes好? 当然,这个没有唯一答案,相对于不同的系统,不同的要求,其得到的答案是不一样的,这里仅对像ICQ一类的发送聊天消息的情况作分 ...

  7. TCP滑动窗口(发送窗口和接受窗口)

    TCP窗口机制 TCP header中有一个Window Size字段,它其实是指接收端的窗口,即接收窗口.用来告知发送端自己所能接收的数据量,从而达到一部分流控的目的. 其实TCP在整个发送过程中, ...

  8. (转)tcp和udp能否发送0字节的数据包

    版权声明:本文为博主原创文章,未经博主允许不得转载. 转自:http://blog.csdn.net/wzx19840423/article/details/6643094  最近去一家牛逼的公司面试 ...

  9. 微信公众号开发C#系列-7、消息管理-接收事件推送

    1.概述 在微信用户和公众号产生交互的过程中,用户的某些操作会使得微信服务器通过事件推送的形式通知到开发者在开发者中心处设置的服务器地址,从而开发者可以获取到该信息.其中,某些事件推送在发生后,是允许 ...

随机推荐

  1. [Codeforces]762F - Tree nesting

    题目大意:给出一棵n个点的树和一棵m个点的树,问第一棵树有多少个连通子树与第二棵树同构.(n<=1000,m<=12) 做法:先找出第二棵树的重心(可能为边),以这个重心为根,可以避免重复 ...

  2. hdu 5887 搜索+剪枝

    Herbs Gathering Time Limit: 3000/1000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others)T ...

  3. 【NOIP2017 OFO】

    ·奇怪的标题可能预示着这一篇博文不是讲算法或者分享题目的吧. [一只情绪化的兔子]      今年的11月12日出奇地比去年温暖.两场比赛结束后的我们在临走前去尝试了OFO共享单车,在成都电子科技大学 ...

  4. [Codeforces Round #431]简要题解

    来自FallDream的博客,未经允许, 请勿转载,谢谢. 好久没写cf题解了zzz 代码比较丑不贴了,cf上都可以看 Div2A. 给你一个长度为n(n<=100)的序列 判断是否可以分成奇数 ...

  5. 第三节基础篇—SQL的约束

    1.约束是一种限制,它通过对表的行或列的数据做出限制,来确保表的数据的完整性.唯一性.本节实验将在实践操作中熟悉 MySQL 中的几种约束. 约束分类: 2.删除数据库语句为DROP DATABASE ...

  6. C语言程序设计第一次实验

    一. 1.输入圆的半径,计算圆的面积周长问题 2流程图 3测试数据及运算结果 4实验分析 不会输入r,后来问了同学得到解决. 二. 1.输入一个四位年份,判断其是否是闰年.闰年的判别条件是概念年份能被 ...

  7. mysql 拼接

    SELECT  RTRIM(CONCAT(belong_master_ip ,'(',host_name,')')) AS cloudIP  FROM `cloud_master_cfg`

  8. Spring的注解@Qualifier小结

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

  9. java后台通过Servlet给用户发送手机短信验证码,第一次写勿喷,欢迎转载

    短信验证码跟自己在Servlet画的验证码不一样,我们不用管短信验证码是怎么产生的,我们只需要关注如何调用短信验证码,在短信验证码里面添加 自己需要的随机数或者其他的内容. 现在直接上流程 第一步找一 ...

  10. Spring消息之AMQP.

    一.AMQP 概述 AMQP(Advanced Message Queuing Protocol),高级消息队列协议. 简单回忆一下JMS的消息模型,可能会有助于理解AMQP的消息模型.在JMS中,有 ...