套接口发送缓存队列统计与控制

发布于:2023-01-22 ⋅ 阅读:(157) ⋅ 点赞:(0)

套接口发送缓存长度统计sk_wmem_queued,记录了当前套接口的发送队列与重传队列所占用空间的大小值。套接口创建时,发送与重传缓存都为空,将其值初始化为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_wmem_queued   = 0;
}


一、发送缓存的增加

当我们向套接口的发送队列(sk_write_queue)或者发送重传队列(tcp_rtx_queue)添加数据包skb时,增加sk_wmem_queued的统计值,增加量为skb的truesize长度,其包括skb结构体所占用的空间长度和skb_shared_info结构体占用空间长度,以及数据长度三者之和。

#define SKB_TRUESIZE(X) ((X) +      \
        SKB_DATA_ALIGN(sizeof(struct sk_buff)) + SKB_DATA_ALIGN(sizeof(struct skb_shared_info)))
TCP连接控制报文,包括用于发送FIN报文的函数tcp_send_fin调用tcp_queue_skb增加发送缓存队列的长度。用于发送SYN相关报文的函数tcp_send_syn_data和tcp_connect调用函数tcp_connect_queue_skb增加发送缓存队列的长度,增加量都为数据包skb的truesize长度。

static void tcp_queue_skb(struct sock *sk, struct sk_buff *skb)
{
    struct tcp_sock *tp = tcp_sk(sk);
 
    tcp_add_write_queue_tail(sk, skb);
    sk->sk_wmem_queued += skb->truesize;
    sk_mem_charge(sk, skb->truesize);
}
static void tcp_connect_queue_skb(struct sock *sk, struct sk_buff *skb)
{
    struct tcp_sock *tp = tcp_sk(sk);
    struct tcp_skb_cb *tcb = TCP_SKB_CB(skb);
 
    tcb->end_seq += skb->len;
    __skb_header_release(skb);
    sk->sk_wmem_queued += skb->truesize;
    sk_mem_charge(sk, skb->truesize);
    tp->write_seq = tcb->end_seq;
}

在TCP两端同时建立连接的情况下,函数tcp_send_synack负责发送SYN+ACK数据包,将数据包nskb插入到套接口的重传队列中,发送缓存队列长度sk_wmem_queued的值增加nskb的truesize长度。

int tcp_send_synack(struct sock *sk)
{   
    if (!(TCP_SKB_CB(skb)->tcp_flags & TCPHDR_ACK)) {
        if (skb_cloned(skb)) {
            tcp_rbtree_insert(&sk->tcp_rtx_queue, nskb);
            sk->sk_wmem_queued += nskb->truesize;
        }
    }
}
除了连接建立阶段之外,TCP分段函数tcp_fragment、tso_fragment和PROBE函数tcp_mtu_probe,以及tcp_sendmsg_locked和do_tcp_sendpages等数据发送函数都会增加发送缓存队列的长度统计值。

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;
    skb = tcp_send_head(sk);
}
以内核套接口发送函数tcp_sendmsg_locked为例,如果发送队列末尾的skb已经没有空间或者其设置了EOR(MSG_EOR),就需要新分配一个SKB来存储用户层的数据,之后内核使用skb_entail将新分配的skb添加到发送队列中,并且增加发送缓存队列长度统计(sk_wmem_queued)。如果此skb的线性缓存区中有可用空间,使用函数skb_add_data_nocache将数据拷贝到其中,由于分配SKB之时增加了发送缓存队列长度统计值,此处不需要再增加。但是如果此SKB没有可用线性空间,则使用函数skb_copy_to_page_nocache分配共享页面空间,并将数据拷贝到共享页面空间中,此处需要增加发送缓存队列长度的统计值,如下函数skb_copy_to_page_nocache所示。

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)) {
            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) {
            copy = min_t(int, copy, skb_availroom(skb));
            err = skb_add_data_nocache(sk, skb, &msg->msg_iter, copy);
        } else if (!uarg || !uarg->zerocopy) {
            err = skb_copy_to_page_nocache(sk, &msg->msg_iter, skb, pfrag->page, pfrag->offset, copy);
        } else {
            err = skb_zerocopy_iter_stream(sk, skb, msg, copy, uarg);
        }
    }
}
static void skb_entail(struct sock *sk, struct sk_buff *skb)
{
    tcp_add_write_queue_tail(sk, skb);
    sk->sk_wmem_queued += skb->truesize;
}
static inline int skb_copy_to_page_nocache(struct sock *sk, struct iov_iter *from, struct sk_buff *skb, struct page *page, int off, int copy)
{           
    err = skb_do_copy_data_nocache(sk, skb, from, page_address(page) + off, copy, skb->len);
    skb->truesize        += copy;
    sk->sk_wmem_queued   += copy;
}    

二、发送缓存的释放
函数sk_wmem_free_skb释放skb占用的内存,同时将套接口发送缓存中的数据长度值减去此数据包的长度(skb->truesize)。

static inline void sk_wmem_free_skb(struct sock *sk, struct sk_buff *skb)
{
    sk->sk_wmem_queued -= skb->truesize;
    __kfree_skb(skb);
}
内核在断开TCP连接(tcp_disconnect)或者销毁套接口(tcp_v4_destroy_sock)时,要清空套接口的发送缓存和重传缓存区,调用以上的sk_wmem_free_skb释放缓存,减少sk_wmem_queued的值。

void tcp_write_queue_purge(struct sock *sk)
{
    while ((skb = __skb_dequeue(&sk->sk_write_queue)) != NULL) {
        tcp_skb_tsorted_anchor_cleanup(skb);
        sk_wmem_free_skb(sk, skb);
    }
    tcp_rtx_queue_purge(sk);
}
另外,内核在重传数据包之前,检查数据包中是否包含有部分对端已经确认过(ACK)的数据。如果TCP套接口中等待ACK的序列号snd_una大于当前skb的起始序列号,表明skb包含已确认数据,重传报文中不应包含这些数据,使用函数tcp_trim_head进行去除操作。套接口的sk_wmem_queued发送缓存长度相应的减去这些数据的长度(delta_truesize)。

int tcp_trim_head(struct sock *sk, struct sk_buff *skb, u32 len)
{
    delta_truesize = __pskb_trim_head(skb, len);
 
    TCP_SKB_CB(skb)->seq += len;
    if (delta_truesize) {
        skb->truesize      -= delta_truesize;
        sk->sk_wmem_queued -= delta_truesize;
        sk_mem_uncharge(sk, delta_truesize);
        sock_set_flag(sk, SOCK_QUEUE_SHRUNK);
    }
}
int __tcp_retransmit_skb(struct sock *sk, struct sk_buff *skb, int segs)
{
    struct tcp_sock *tp = tcp_sk(sk);
 
    if (before(TCP_SKB_CB(skb)->seq, tp->snd_una)) {
        if (before(TCP_SKB_CB(skb)->end_seq, tp->snd_una))
            BUG();
        if (tcp_trim_head(sk, skb, tp->snd_una - TCP_SKB_CB(skb)->seq))
            return -ENOMEM;
    }
}

三、剩余发送缓存空间
剩余发送缓存空间等于总得发送缓存长度(sk_sndbuf)减去已用空间(sk_wmem_queued)。但是,内核判断是否可写入的函数sk_stream_is_writeable并不仅仅是查看有无剩余发送空间,而是需要满足剩余发送缓存空间大于等于已用发送空间的一半,并且协议回调函数stream_memory_free返回为真。

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_is_writeable(const struct sock *sk)
{
    return sk_stream_wspace(sk) >= sk_stream_min_wspace(sk) &&
           sk_stream_memory_free(sk);
}
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;
}

对于TCP协议来说,回调函数为tcp_stream_memory_free,当发送缓存中还未发送的数据长度小于tcp_notsent_lowat函数的值时,内核才会判定为缓存有空余。tcp_notsent_lowat函数的默认值为最大值UINT_MAX,所以此条件总是成立。但是,用户可通过PROC文件/proc/sys/net/ipv4/tcp_notsent_lowat全局性质的修改此值,也可通过setsockopt系统调用的TCP_NOTSENT_LOWAT选项修改此特定套接口的notsent_lowat数值,后者拥有高优先级。

static inline u32 tcp_notsent_lowat(const struct tcp_sock *tp)
{
    struct net *net = sock_net((struct sock *)tp);
    return tp->notsent_lowat ?: net->ipv4.sysctl_tcp_notsent_lowat;
}
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用于控制发送缓存中未发送数据的长度,超过此长度将禁止用户层向此套接口添加新数据。如内核函数tcp_sendmsg_locked所示,如果sk_stream_memory_free不成立,跳转到wait_for_sndbuf执行,如果用户设置了MSG_DONTWAIT标志,立即返回错误码-EAGAIN,否则,要等待发送缓存中未发送数据长度小于tcp_notsent_lowat函数计算所得的值。

int tcp_sendmsg_locked(struct sock *sk, struct msghdr *msg, size_t size)
{
    while (msg_data_left(msg)) {
        if (copy <= 0 || !tcp_skb_can_collapse_to(skb)) {
 
new_segment:
            /* Allocate new segment. If the interface is SG,
             * allocate skb fitting to single page.
             */
            if (!sk_stream_memory_free(sk))
                goto wait_for_sndbuf;
        } 
    }
}

四、发送缓存长度控制
在TCP的超时重传函数或者PROBE超时处理函数中,如果当前处理的套接口已经是orphan套接口,其sk_flags设置了SOCK_DEAD标志,进行TCP使用资源检查。其中一项为TCP使用内存是否超限,参见函数tcp_out_of_memory,如果TCP协议占用的全部内存空间大于系统规定的最大值(/proc/sys/net/ipv4/tcp_mem),并且当前套接口的发送缓存队列长度大于宏SOCK_MIN_SNDBUF定义的最小发送缓存长度值,内核判定TCP内存耗尽。

static inline bool tcp_out_of_memory(struct sock *sk)
{
    if (sk->sk_wmem_queued > SOCK_MIN_SNDBUF && sk_memory_allocated(sk) > sk_prot_mem_limits(sk, 2))
        return true;
    return false;
}
static int tcp_write_timeout(struct sock *sk)
{
    if ((1 << sk->sk_state) & (TCPF_SYN_SENT | TCPF_SYN_RECV)) {
    } else {
        if (sock_flag(sk, SOCK_DEAD)) {
            if (tcp_out_of_resources(sk, do_reset))
                return 1;
        }
    }
}

另外,如下数据包重传函数__tcp_retransmit_skb,套接口提交发送的数据长度sk_wmem_alloc,不能够大于以下两者之中的较小值:发送缓存的最大值或者发送缓存队列长度与其1/4之和。否则返回错误-EAGAIN。此处预留四分之一的发送缓存队列空间,以便分片、隧道等操作进行。

int __tcp_retransmit_skb(struct sock *sk, struct sk_buff *skb, int segs)
{
    struct tcp_sock *tp = tcp_sk(sk);
 
    if (refcount_read(&sk->sk_wmem_alloc) >
        min_t(u32, sk->sk_wmem_queued + (sk->sk_wmem_queued >> 2), sk->sk_sndbuf))
        return -EAGAIN;
}
如下,内核中最重要的网络协议缓存空间控制函数__sk_mem_raise_allocated,对于发送缓存SK_MEM_SEND而言,如果套接口的类型为SOCK_TREAM(例如TCP),并且发送缓存队列的长度值小于TCP套接口设定的最小内存值(/proc/sys/net/ipv4/tcp_wmem),说明内存足够,直接返回结束判断。

如果网络协议的内存(如TCP内存)空间正处于承压状态,并且此协议的所有套接口占用的内存页面小于协议设定的最大页面值(/proc/sys/net/ipv4/tcp_mem),说明还有可用空间。套接口总占用空间等于协议的套接口总数,与当前套接口的发送缓存队列空间、接收缓存空间和套接口预分配空间sk_forward_alloc三者之和的乘积。这种算法是不准确的,不能真实的反映该协议类型的所有套接口所占用空间。

int __sk_mem_raise_allocated(struct sock *sk, int size, int amt, int kind)
{
    /* guarantee minimum buffer size under pressure */
    if (kind == SK_MEM_RECV) {
    } else {                /* SK_MEM_SEND */
        int wmem0 = sk_get_wmem0(sk, prot);
 
        if (sk->sk_type == SOCK_STREAM) {
            if (sk->sk_wmem_queued < wmem0)
                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;
    }
suppress_allocation:
 
    if (kind == SK_MEM_SEND && sk->sk_type == SOCK_STREAM) {
        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;
    }
    return 0;
}

对于发送缓存而言,函数__sk_mem_raise_allocated的封装函数为sk_wmem_schedule。其在发送路径函数do_tcp_sendpages和tcp_sendmsg_locked中被调用,已判断新分配的发送缓存是否超限。例如函数tcp_sendmsg_locked,判断点有三个,首先是sk_stream_memory_free判断,其次在分配skb时由分配函数sk_stream_alloc_skb内部判断,最后,在分配skb共享页面时进行判断,函数sk_wmem_schedule。

int tcp_sendmsg_locked(struct sock *sk, struct msghdr *msg, size_t size)
{
    while (msg_data_left(msg)) {
        if (copy <= 0 || !tcp_skb_can_collapse_to(skb)) {
new_segment:
            if (!sk_stream_memory_free(sk))
                goto wait_for_sndbuf;
            skb = sk_stream_alloc_skb(sk, select_size(sk, sg, first_skb), sk->sk_allocation, first_skb);
        }
        if (skb_availroom(skb) > 0) {
        } else if (!uarg || !uarg->zerocopy) {
 
            copy = min_t(int, copy, pfrag->size - pfrag->offset);
            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);
        }
    }
}

前述的分片函数tcp_fragment、tso_fragment和tcp_mtu_probe函数,以及tcp_send_syn_data函数和tcp_connect函数都会使用到sk_stream_alloc_skb函数分配skb内存,在此函数内,将会执行sk_wmem_schedule进行缓存超限判断。

struct sk_buff *sk_stream_alloc_skb(struct sock *sk, int size, gfp_t gfp, bool force_schedule)
{
    skb = alloc_skb_fclone(size + sk->sk_prot->max_header, gfp);
    if (likely(skb)) {
        if (force_schedule) {
        } else {
            mem_scheduled = sk_wmem_schedule(sk, skb->truesize);
        }
    }
}

本文含有隐藏内容,请 开通VIP 后查看