预分配缓存额度sk_forward_alloc(TCP发送)

TCPIP协议 专栏收录该内容
105 篇文章 6 订阅

预分配缓存额度sk_forward_alloc与发送缓存队列统计sk_wmem_queued一同用于计算当前套接口所占用的内存量。sk_forward_alloc属于为套接口预分配,所以缓存并没有实际分配出去。

 
sk_forward_alloc初始化

 
对于面向连接的套接口类型如TCP,在创建子套接口时,将其sk_forward_alloc初始化为0。

struct sock *sk_clone_lock(const struct sock *sk, const gfp_t priority)
{
    newsk = sk_prot_alloc(sk->sk_prot, priority, sk->sk_family);
    if (newsk != NULL) {
        newsk->sk_forward_alloc = 0;
}

sk_forward_alloc预分配

套接口内存的页面是按照SK_MEM_QUANTUM的大小为单位,定义为4096(4K),与大多数系统的PAGE_SIZE定义相同。曾经在老一点版本的内核中,SK_MEM_QUANTUM直接使用PAGE_SIZE宏定义,导致一些将页面大小PAGE_SIZE定义为64K字节的系统,预分配不必要的大量内存额度。

#define SK_MEM_QUANTUM 4096
#define SK_MEM_QUANTUM_SHIFT ilog2(SK_MEM_QUANTUM)
static inline int sk_mem_pages(int amt)
{       
    return (amt + SK_MEM_QUANTUM - 1) >> SK_MEM_QUANTUM_SHIFT;
}

但是,sk_forward_alloc的单位还是以字节表示,只不过其大小为SK_MEM_QUANTUM的整数倍。 预分配额度的基础函数为__sk_mem_schedule如下,函数__sk_mem_raise_allocated判断此次分配是否符合协议的内存限定,如果不符合,需要回退预分配的额度。
 

int __sk_mem_schedule(struct sock *sk, int size, int kind)
{
    int ret, amt = sk_mem_pages(size);

    sk->sk_forward_alloc += amt << SK_MEM_QUANTUM_SHIFT;
    ret = __sk_mem_raise_allocated(sk, size, amt, kind);
    if (!ret)
        sk->sk_forward_alloc -= amt << SK_MEM_QUANTUM_SHIFT;
    return ret;
}

__sk_mem_raise_allocated函数还将增加协议总内存的占用统计(memory_allocated),其单位为SK_MEM_QUANTUM大小的页面数量(第三个参数amt)。由于内核的网络协议内存限额是以PAGE_SIZE大小页面为单位,如TCP协议,可通过PROC文件/proc/sys/net/ipv4/tcp_mem查看。所以在进行比较时,内核使用函数sk_prot_mem_limits将限定的页面数值转换为以SK_MEM_QUANTUM为单位的页面值。

static inline long sk_prot_mem_limits(const struct sock *sk, int index)
{       
    long val = sk->sk_prot->sysctl_mem[index];
        
#if PAGE_SIZE > SK_MEM_QUANTUM
    val <<= PAGE_SHIFT - SK_MEM_QUANTUM_SHIFT;
#elif PAGE_SIZE < SK_MEM_QUANTUM
    val >>= SK_MEM_QUANTUM_SHIFT - PAGE_SHIFT;
#endif  
    return val;
}

函数__sk_mem_schedule的封装函数有两个sk_wmem_schedule和sk_rmem_schedule,对应于发送SK_MEM_SEND和接收SK_MEM_RECV两个类别的缓存使用。对于sk_wmem_schedule函数,如果请求的大小在预分配额度内,进行正常分配,否则,由__sk_mem_schedule函数分配新的额度。

static inline bool sk_wmem_schedule(struct sock *sk, int size)
{
    if (!sk_has_account(sk))
        return true;
    return size <= sk->sk_forward_alloc || __sk_mem_schedule(sk, size, SK_MEM_SEND);
}
static inline bool sk_rmem_schedule(struct sock *sk, struct sk_buff *skb, int size)
{
    if (!sk_has_account(sk))
        return true;
    return size<= sk->sk_forward_alloc || __sk_mem_schedule(sk, size, SK_MEM_RECV) || skb_pfmemalloc(skb);
}

另外一个预分配缓存额度的函数为sk_forced_mem_schedule,与以上的__sk_mem_schedule函数不同,如果内存额度不够,其强制进行缓存额度的预分配,而不管是否超出网络协议的内存限定。用在比如FIN报文发送等情况下,其不必等待可尽快结束一个连接,否则可能导致FIN报文的延迟或者放弃发送FIN而关闭连接,其结束后又可释放连接的缓存占用。

void sk_forced_mem_schedule(struct sock *sk, int size)
{
    if (size <= sk->sk_forward_alloc)
        return;
    amt = sk_mem_pages(size);
    sk->sk_forward_alloc += amt * SK_MEM_QUANTUM;
    sk_memory_allocated_add(sk, amt);
}

sk_forward_alloc预分配额度使用


sk_forward_alloc预分配额度使用有一对sk_mem_charge和sk_mem_uncharge函数组成。在得到套接口预分配额度后,函数sk_mem_charge可由额度中获取一定量的数值使用。

static inline void sk_mem_charge(struct sock *sk, int size)
{
    if (!sk_has_account(sk))
        return;
    sk->sk_forward_alloc -= size;
}

函数sk_mem_uncharge可将一定量回填到预分配额度中。如果sk_forward_alloc预分配额度大于2M字节,立即回收1M字节,预分配额度没有必要太大,立即回收以防止sk_mem_reclain回收不及时导致溢出。

static inline void sk_mem_uncharge(struct sock *sk, int size)
{
    if (!sk_has_account(sk))
        return;
    sk->sk_forward_alloc += size;

    /* Avoid a possible overflow.
     * TCP send queues can make this happen, if sk_mem_reclaim()
     * is not called and more than 2 GBytes are released at once.
     *
     * If we reach 2 MBytes, reclaim 1 MBytes right now, there is
     * no need to hold that much forward allocation anyway.
     */
    if (unlikely(sk->sk_forward_alloc >= 1 << 21))
        __sk_mem_reclaim(sk, 1 << 20);
}

在清空发送队列函数tcp_write_queue_purge和清空重传队列函数tcp_rtx_queue_purge,以及重传队列元素移除函数tcp_rtx_queue_unlink_and_free中调用sk_wmem_free_skb释放skb,并且uncharge预分配的额度。

static inline void sk_wmem_free_skb(struct sock *sk, struct sk_buff *skb)
{
    sock_set_flag(sk, SOCK_QUEUE_SHRUNK);
    sk->sk_wmem_queued -= skb->truesize;
    sk_mem_uncharge(sk, skb->truesize);
    __kfree_skb(skb);
}

 

sk_forward_alloc预分配额度回收

基础的回收函数为__sk_mem_reclaim,以上介绍的sk_mem_uncharge函数也会使用到。除此之外,内核使用两个函数回收预分配内存额度:分别为sk_mem_reclaim和sk_mem_reclaim_partial函数。前者回收之后有可能将额度全部回收或者仅留下小于SK_MEM_QUANTUM大小的额度;而后者不会全部回收额度,其在额度大于SK_MEM_QUANTUM时,执行回收操作,并且保证留下小于SK_MEM_QUANTUM大小的额度。

void __sk_mem_reclaim(struct sock *sk, int amount)
{
    amount >>= SK_MEM_QUANTUM_SHIFT;
    sk->sk_forward_alloc -= amount << SK_MEM_QUANTUM_SHIFT;
    __sk_mem_reduce_allocated(sk, amount);
}

static inline void sk_mem_reclaim(struct sock *sk)
{
    if (!sk_has_account(sk))
        return;
    if (sk->sk_forward_alloc >= SK_MEM_QUANTUM)
        __sk_mem_reclaim(sk, sk->sk_forward_alloc);
}

static inline void sk_mem_reclaim_partial(struct sock *sk)
{
    if (!sk_has_account(sk))
        return;
    if (sk->sk_forward_alloc > SK_MEM_QUANTUM)
        __sk_mem_reclaim(sk, sk->sk_forward_alloc - 1);
}

sk_forward_alloc预分配时机

在TCP重要的skb缓存分配函数sk_stream_alloc_skb中,如果TCP协议总的内存处于承压状态,首先回收部分预分配缓存,因为马上要为skb分配内存,不应进行全部回收。在分配skb之后有两种情况,如果指定了强制分配force_schedule参数,即强制增加分配额度而不进行内存超限判断;否则,使用sk_wmem_schedule进行额度分配。只有在分配额度成功之后返回分配的skb,反之释放skb返回失败。

struct sk_buff *sk_stream_alloc_skb(struct sock *sk, int size, gfp_t gfp, bool force_schedule)
{
    if (unlikely(tcp_under_memory_pressure(sk)))
        sk_mem_reclaim_partial(sk);

    skb = alloc_skb_fclone(size + sk->sk_prot->max_header, gfp);
    if (likely(skb)) {
        if (force_schedule) {
            mem_scheduled = true;
            sk_forced_mem_schedule(sk, skb->truesize);
        } else {
            mem_scheduled = sk_wmem_schedule(sk, skb->truesize);
        }
        if (likely(mem_scheduled)) {
            return skb;
        }
        __kfree_skb(skb);
    }
}

对于sk_stream_alloc_skb函数的使用,发生在TCP发送路径上比如tcp_sendmsg_locked和do_tcp_sendpages发送函数,分片函数tcp_fragment和tso_fragment,以及tcp_mtu_probe、tcp_send_syn_data和tcp_connect函数。sk_stream_alloc_skb函数获取到了相应的缓存额度,紧接其后就需要使用此额度。如函数tcp_mtu_probe,其调用sk_mem_charge使用了skb的truesize长度的分配额度。

static int tcp_mtu_probe(struct sock *sk)
{
    /* We're allowed to probe.  Build it now. */
    nskb = sk_stream_alloc_skb(sk, probe_size, GFP_ATOMIC, false);

    sk->sk_wmem_queued += nskb->truesize;
    sk_mem_charge(sk, nskb->truesize);
}

另外,对于TCP发送函数tcp_sendmsg_locked和do_tcp_sendpages,如果此时套接口发送队列以及重传队列为空,将强制为此数据包分配额度,其额度的使用在函数skb_entail中完成。如果skb已经没有可用的空间,内核需要将数据拷贝到skb的共享页面中,首先sk_wmem_schedule检查额度使用够用,如果额度不足并且不能分配出所需的额度,跳转到等待处理。在数据复制函数skb_copy_to_page_nocache中,使用sk_mem_charge使用预分配的额度。

int tcp_sendmsg_locked(struct sock *sk, struct msghdr *msg, size_t size)
{
    while (msg_data_left(msg)) {
        skb = tcp_write_queue_tail(sk);

        if (copy <= 0 || !tcp_skb_can_collapse_to(skb)) {
            first_skb = tcp_rtx_and_write_queues_empty(sk);
            skb = sk_stream_alloc_skb(sk, select_size(sk, sg, first_skb), sk->sk_allocation, first_skb);
            skb_entail(sk, skb);
        }  
        if (skb_availroom(skb) > 0) {
        } else if (!uarg || !uarg->zerocopy) {
            struct page_frag *pfrag = sk_page_frag(sk);

            if (!sk_wmem_schedule(sk, copy))
                goto wait_for_memory;
            err = skb_copy_to_page_nocache(sk, &msg->msg_iter, skb, pfrag->page, pfrag->offset, copy);
        }
    }
}

sk_forward_alloc回收时机


在套接口关闭销毁的时候,回收预分配额度。如函数tcp_close和inet_sock_destruct,或者接收到对端发送的FIN报文,如tcp_fin函数。

void tcp_close(struct sock *sk, long timeout)
{
    sk_mem_reclaim(sk);
}
void inet_sock_destruct(struct sock *sk)
{
    struct inet_sock *inet = inet_sk(sk);
    sk_mem_reclaim(sk);
}

TCP的延时ACK处理函数tcp_delack_timer_handler,首先会调用函数sk_mem_reclaim_partial回收部分预分配额度,在执行最后,如果网络协议内存处于承压状态,还会调用sk_mem_reclaim回收函数。另外,在TCP超时重传函数tcp_write_timer_handler和keepalive超时函数中也有调用sk_mem_reclaim回收函数。

void tcp_delack_timer_handler(struct sock *sk)
{
    struct inet_connection_sock *icsk = inet_csk(sk);

    sk_mem_reclaim_partial(sk);
out:
    if (tcp_under_memory_pressure(sk))
        sk_mem_reclaim(sk);
}
void tcp_write_timer_handler(struct sock *sk)
{
    sk_mem_reclaim(sk);
}
static void tcp_keepalive_timer (struct timer_list *t)
{
    sk_mem_reclaim(sk);
}

以上的延时ACK处理函数,或者TCP超时重传处理函数,在TCP套接口的release_sock函数中也都有调用。内核需要保持回收函数的及时调用,保证可用额度。

void release_sock(struct sock *sk)
{
    if (sk->sk_prot->release_cb)
        sk->sk_prot->release_cb(sk);
}
void tcp_release_cb(struct sock *sk)
{
    if (flags & TCPF_WRITE_TIMER_DEFERRED) {
        tcp_write_timer_handler(sk);
        __sock_put(sk);
    }
    if (flags & TCPF_DELACK_TIMER_DEFERRED) {
        tcp_delack_timer_handler(sk);
        __sock_put(sk);
    }
}

最后,对发送队里的清除操作,也会伴随预分配额度的回收,如函数sk_stream_kill_queues和tcp_write_queue_purge函数。

void sk_stream_kill_queues(struct sock *sk)
{    
    /* Account for returned memory. */
    sk_mem_reclaim(sk); 
}
void tcp_write_queue_purge(struct sock *sk)
{
    sk_mem_reclaim(sk);
}

sk_forward_alloc超限判断


在函数__sk_mem_schedule预分配额度时,使用函数__sk_mem_raise_allocated判断TCP协议内存是否超过限定值。如下在协议内存承压状态下,如果当前套接口的发送队列缓存、接收缓存已经预分配缓存之和所占用的页面数,乘以当前套接口协议的所有套接口数量,小于系统设定的最大协议内存限值的话(TCP协议:/proc/sys/net/ipv4/tcp_mem),说明还有内存空间可供分配使用。

int __sk_mem_raise_allocated(struct sock *sk, int size, int amt, int kind)
{
    if (sk_has_memory_pressure(sk)) {
        if (!sk_under_memory_pressure(sk))
            return 1;
        alloc = sk_sockets_allocated_read_positive(sk);
        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;
    }
}

 

内核版本 4.15

 

  • 0
    点赞
  • 0
    评论
  • 1
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

相关推荐
©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值