• TCP协议之《预分配缓存额度sk_forward_alloc--TCP发送》


    预分配缓存额度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;
        }
    }

  • 相关阅读:
    了解Pytorch|Get Started with PyTorch
    SD-WAN是零售业的创收者
    搭建Docker+SRS服务器实现推流拉流的效果
    Leetcode 35. 搜索插入位置(二分)
    spring笔记-ioc容器 大概流程
    服务器数据恢复—OceanStor存储中NAS卷数据丢失如何恢复数据?
    springBoot中starter
    回溯算法(回溯搜索法)
    Cesium Vue(一)— 项目初始化配置
    《鸿蒙生态应用开发白皮书》读后感
  • 原文地址:https://blog.csdn.net/wuyongmao/article/details/126265994