主要内容:从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. 洛谷P3980:[NOI2008]志愿者招募

    线性规划: #include<cstdio> #include<cstdlib> #include<algorithm> #include<cstring&g ...

  2. [LSGDOJ 1505]售货员的难题 状压DP

    题目描述 某 乡有n个村庄(1<n<15),有一个售货员,他要到各个村庄去售货,各村庄之间的路程s(0<s<1000)是已知的,且A村 到B村与B村到A村的路大多不同.为了提高 ...

  3. 【LSGDOJ 1408】邮局

    题目描述 一些村庄被建在一条笔直的高边公路边上.我们用一条坐标轴来描述这条高边公路,每一个村庄的坐标都是整数.没有两个村庄坐标相同.两个村庄问的距离,定义为它们坐标值差的绝对值. 我们需要在一些村庄建 ...

  4. ●POJ 2007 Scrambled Polygon

    题链: http://poj.org/problem?id=2007 题解: 计算几何,极角排序 按样例来说,应该就是要把凸包上的i点按 第三像限-第四像限-第一像限-第二像限 的顺序输出. 按 叉积 ...

  5. poj 1113 凸包周长

    Wall Time Limit: 1000MS   Memory Limit: 10000K Total Submissions: 33888   Accepted: 11544 Descriptio ...

  6. Python Django rest framework

    本节内容 Django rest framework 安装 Django rest framwwork 环境配置 简单举例说明 Django中使用 rest framework 1.1 安装 Djan ...

  7. Linux mint界面过小无法安装(解决方法)

    安装Linux mint时,到了分区的时候,有时候会出现界面过小(就是你点击不到确定键). 如下图所示: 大家可以看到下面的确定键无法显示,导致无法安装.网上查了一下都是清一色的下载vwware to ...

  8. 如何理解Spring AOP

    什么是AOP? AOP(Aspect-OrientedProgramming,面向方面编程),可以说是OOP(Object-Oriented Programing,面向对象编程)的补充和完善.OOP允 ...

  9. 利用css3+js实现简单带立体过渡效果的图片切换(chrome浏览器)

    <!DOCTYPE HTML> <html lang="en-US"> <head> <meta charset="UTF-8& ...

  10. maven generating project in batch mode hang

    最新版已经自己就设置了 设置maven不要从远程服务器上获取catalog,增加参数-DarchetypeCatalog=internal 如何在idea里设置maven参数: