预分配缓存额度sk_forward_alloc(TCP接收)

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

基础介绍请参见 https://blog.csdn.net/sinat_20184565/article/details/88747124

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;
    }
}

 

内核版本 4.15

 

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

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

抵扣说明:

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

余额充值