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


    一、sk_forward_alloc预分配额度
    接收路径中使用函数sk_rmem_schedule分配缓存额度,使用宏SK_MEM_RECV表示此次是为接收而分配。如果请求的缓存大小在预分配额度之内,可马上进行正常分配,否则,由__sk_mem_schedule函数分配新的额度。如果分配失败,但是此skb是由内存的PFMEMALLOC保留区分配而来,内核忽略之前的失败,返回成功。另外,还可使用函数sk_forced_mem_schedule强制分配额度。

    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_forward_alloc预分配额度使用由一对sk_mem_charge和sk_mem_uncharge函数组成。在得到套接口预分配额度后,函数sk_mem_charge可由额度中获取一定量的数值使用,函数sk_mem_uncharge可释放一定的额度。sk_mem_charge函数假定预分配额度足够使用,两个函数都不会做缓存超限判断。另外,与发送路径上的函数skb_set_owner_w类似,内核使用skb_set_owner_r封装了sk_mem_charge函数,用于接收路径。

    static inline void skb_set_owner_r(struct sk_buff *skb, struct sock *sk)
    {
        skb_orphan(skb);
        skb->sk = sk;
        skb->destructor = sock_rfree;
        atomic_add(skb->truesize, &sk->sk_rmem_alloc);
        sk_mem_charge(sk, skb->truesize);
    }
    对于L2TP,PACKET,RAW和PING等协议的类型的套接口,内核使用函数__sock_queue_rcv_skb将数据包接收到套接口的sk_receive_queue队列中。在此之前,使用sk_rmem_schedule函数检查以及分配缓存额度,继而调用skb_set_owner_r函数使用预分配的额度。

    int __sock_queue_rcv_skb(struct sock *sk, struct sk_buff *skb)
    {
        struct sk_buff_head *list = &sk->sk_receive_queue;
     
        if (!sk_rmem_schedule(sk, skb, skb->truesize)) {
            atomic_inc(&sk->sk_drops);
            return -ENOBUFS;
        }
        skb_set_owner_r(skb, sk);
        __skb_queue_tail(list, skb);
    }
    TCP的核心预分配缓存额度函数为tcp_try_rmem_schedule,如果无法分配缓存额度,将首先调用tcp_prune_queue函数尝试合并sk_receive_queue中的数据包skb以减少空间占用,如果空间仍然不足,最后调用tcp_prune_ofo_queue函数清理乱序数据包队列(out_of_order_queue)。

    static int tcp_try_rmem_schedule(struct sock *sk, struct sk_buff *skb, unsigned int size)
    {
        if (atomic_read(&sk->sk_rmem_alloc) > sk->sk_rcvbuf ||
            !sk_rmem_schedule(sk, skb, size)) {
     
            if (tcp_prune_queue(sk) < 0)
                return -1;
     
            while (!sk_rmem_schedule(sk, skb, size)) {
                if (!tcp_prune_ofo_queue(sk))
                    return -1;
            }
        }
    }
    函数tcp_prune_queue和tcp_prune_ofo_queue在清理空间之后,都会使用函数sk_mem_reclaim回收空间。

    static int tcp_prune_queue(struct sock *sk)
    {
        tcp_collapse_ofo_queue(sk);
        if (!skb_queue_empty(&sk->sk_receive_queue))
            tcp_collapse(sk, &sk->sk_receive_queue, NULL, skb_peek(&sk->sk_receive_queue), NULL, tp->copied_seq, tp->rcv_nxt);
        sk_mem_reclaim(sk);
    }
    函数tcp_try_rmem_schedule在TCP的接收路径中调用,比如tcp_data_queue函数和tcp_data_queue_ofo函数,以及用于TCP套接口REPAIR模式的tcp_send_rcvq函数。如下为tcp_data_queue函数,如果接收队列sk_receive_queue为空,使用sk_forced_mem_schedule函数强制分配缓存额度,否则,使用tcp_try_rmem_schedule函数进行正常分配并检查缓存是否超限。最后使用tcp_queue_rcv函数完成接收工作,同时通过调用skb_set_owner_r函数使用分配的缓存额度。函数tcp_send_rcvq的缓存相关操作与此类似。

    static void tcp_data_queue(struct sock *sk, struct sk_buff *skb)
    {
        if (TCP_SKB_CB(skb)->seq == tp->rcv_nxt) {
            /* Ok. In sequence. In window. */
    queue_and_out:
            if (skb_queue_len(&sk->sk_receive_queue) == 0)
                sk_forced_mem_schedule(sk, skb->truesize);
            else if (tcp_try_rmem_schedule(sk, skb, skb->truesize))
                goto drop;
     
            eaten = tcp_queue_rcv(sk, skb, 0, &fragstolen);
            if (skb->len)
                tcp_event_data_recv(sk, skb);
    }
    函数tcp_data_queue_ofo与以上两者的不同在于,其直接使用skb_set_owner_r函数使用预分配的缓存额度。

    static void tcp_data_queue_ofo(struct sock *sk, struct sk_buff *skb)
    {
        if (unlikely(tcp_try_rmem_schedule(sk, skb, skb->truesize))) {
            tcp_drop(sk, skb);
            return;
        }
    end:
        if (skb)
            skb_set_owner_r(skb, sk);
    }

    二、sk_forward_alloc额度的使用与回填
    预分配额度使用函数skb_set_owner_r将skb的销毁回调函数destructor设置为sock_rfree函数,其调用sk_mem_uncharge函数将skb占用的缓存回填到预分配额度sk_forward_alloc中。

    static inline void skb_set_owner_r(struct sk_buff *skb, struct sock *sk)
    {
        skb->destructor = sock_rfree;
        sk_mem_charge(sk, skb->truesize);
    }
    void sock_rfree(struct sk_buff *skb)
    {
        unsigned int len = skb->truesize;
     
        atomic_sub(len, &sk->sk_rmem_alloc);
        sk_mem_uncharge(sk, len);
    }
    另外,在TCP接收过程中,内核尝试将新接收到的数据包合并到之前的数据包中(sk_receive_queue接收队列的末尾数据包),参见函数tcp_try_coalesce,如果合并成功,将释放被合并的skb所占用的缓存,所以其skb结构体所占用缓存空间不需要使用缓存额度,仅需要计算其数据部分的缓存额度(delta)。

    static bool tcp_try_coalesce(struct sock *sk, struct sk_buff *to, struct sk_buff *from, bool *fragstolen)
    {
        if (!skb_try_coalesce(to, from, fragstolen, &delta))
            return false;
     
        atomic_add(delta, &sk->sk_rmem_alloc);
        sk_mem_charge(sk, delta);
    }
    如果合并失败,内核将skb添加到接收队列sk_receive_queue中,使用skb_set_owner_r函数将整个skb占用的缓存空间计入缓存额度。

    static int __must_check tcp_queue_rcv(struct sock *sk, struct sk_buff *skb, int hdrlen, bool *fragstolen)
    {
        struct sk_buff *tail = skb_peek_tail(&sk->sk_receive_queue);
     
        __skb_pull(skb, hdrlen);
        eaten = (tail && tcp_try_coalesce(sk, tail, skb, fragstolen)) ? 1 : 0;
        if (!eaten) {
            __skb_queue_tail(&sk->sk_receive_queue, skb);
            skb_set_owner_r(skb, sk);
        }
    }
     

    三、sk_forward_alloc额度回收
    在套接口关闭销毁的时候,回收预分配缓存额度。如函数tcp_close和inet_sock_destruct。或者接收到对端发送的FIN报文,要关闭TCP连接时,如tcp_fin函数。以及在函数tcp_try_rmem_schedule中,调用的tcp_prune_queue和tcp_prune_ofo_queue两个函数。

    static bool tcp_prune_ofo_queue(struct sock *sk)
    {   
        node = &tp->ooo_last_skb->rbnode;
        do {
            prev = rb_prev(node);
            rb_erase(node, &tp->out_of_order_queue);
            tcp_drop(sk, rb_to_skb(node));
            sk_mem_reclaim(sk);
            if (atomic_read(&sk->sk_rmem_alloc) <= sk->sk_rcvbuf && !tcp_under_memory_pressure(sk))
                break;
            node = prev;
        } while (node);
    }
    另外,在TCP入队列函数tcp_data_queue和tcp_rcv_established的快速路径中,都会调用到数据接收事件函数tcp_event_data_recv,如果此连接的两次接收时间之差大于当前的重传超时时长RTO,回收一次缓存额度sk_mem_reclaim。

    static void tcp_event_data_recv(struct sock *sk, struct sk_buff *skb)
    {
        if (!icsk->icsk_ack.ato) {
        } else {
            int m = now - icsk->icsk_ack.lrcvtime;
     
            if (m <= TCP_ATO_MIN / 2) {
                icsk->icsk_ack.ato = (icsk->icsk_ack.ato >> 1) + TCP_ATO_MIN / 2;
            } else if (m < icsk->icsk_ack.ato) {
                icsk->icsk_ack.ato = (icsk->icsk_ack.ato >> 1) + m;
                if (icsk->icsk_ack.ato > icsk->icsk_rto)
                    icsk->icsk_ack.ato = icsk->icsk_rto;
            } else if (m > icsk->icsk_rto) {
                /* Too long gap. Apparently sender failed to
                 * restart window, so that we send ACKs quickly.
                 */
                tcp_incr_quickack(sk);
                sk_mem_reclaim(sk);
            }
        }
    }

    在tcp_rcv_established函数的快速路径中,如果检测到当前正在接收的数据包skb占用的缓存长度大于套接口预分配的缓存额度,跳转到慢速路径执行。

    void tcp_rcv_established(struct sock *sk, struct sk_buff *skb, const struct tcphdr *th)
    {
        if ((tcp_flag_word(th) & TCP_HP_BITS) == tp->pred_flags &&
            TCP_SKB_CB(skb)->seq == tp->rcv_nxt && !after(TCP_SKB_CB(skb)->ack_seq, tp->snd_nxt)) {
     
            if (len <= tcp_header_len) {
            } else {
                if ((int)skb->truesize > sk->sk_forward_alloc)
                    goto step5;
            }
        }
    }


    四、sk_forward_alloc超限判断

    基础预分配缓存额度函数sk_rmem_schedule调用__sk_mem_raise_allocated函数进行协议缓存超限判断。如果协议总内存(allocated)还没有超出最大值(TCP协议:/proc/sys/net/ipv4/tcp_mem的第三个值),并且,套接口占用的接收缓存sk_rmem_alloc小于设定的接收缓存最小值(/proc/sys/net/ipv4/tcp_rmem第一个值),说明内存充足返回成功。如果协议总内存处于承压状态,但是当前套接口的发送队列缓存、接收缓存以及预分配缓存之和所占用的页面数,乘以当前套接口协议类型的所有套接口数量,小于系统设定的最大协议内存限值的话(TCP协议:/proc/sys/net/ipv4/tcp_mem),说明还有内存空间可供分配使用。

    int __sk_mem_raise_allocated(struct sock *sk, int size, int amt, int kind)
    {
        if (allocated > sk_prot_mem_limits(sk, 2))
            goto suppress_allocation;
     
        /* guarantee minimum buffer size under pressure */
        if (kind == SK_MEM_RECV) {
            if (atomic_read(&sk->sk_rmem_alloc) < sk_get_rmem0(sk, prot))
                return 1;
        }
        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;
        }
    }

  • 相关阅读:
    CentOS系统下,配制nginx访问favicon.ico
    互联网医院系统|互联网医院软件功能与广阔应用领域
    贪心算法
    微服务博客专栏汇总
    用户运营都做些什么?你需要知道这些
    Spring 缓存注解这样用,太香了!
    移动端布局方案
    图论学习笔记 - 树链剖分
    Oracle数据库概念简介
    C++笔记:从零开始一步步手撕高阶数据结构AVL树
  • 原文地址:https://blog.csdn.net/wuyongmao/article/details/126266100