TCP协议之《套接口sk_backlog接收队列》

本文解析了TCP协议中Backlog链表的工作原理,包括数据包的添加与处理过程,以及处理时机。针对不同场景,如用户进程使用套接口、内存紧张等情况,详细介绍了Backlog链表的管理机制。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在接收到数据包之后,如果判断此套接口当前正被用户进程所使用,数据包将被保存到套接口结构的sk_backlog成员的head所定义的skb缓存列表中,tail指向链表的末尾,len变量记录了当前链表中所有skb的总长度。

struct sock {
    struct {
        atomic_t    rmem_alloc;
        int     len;
        struct sk_buff  *head;
        struct sk_buff  *tail;
    } sk_backlog;
#define sk_rmem_alloc sk_backlog.rmem_alloc
}
TCP在创建子套接口时,将sk_backlog结构的成员head和tail都置为空,链表元素数量为零。

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

一、backlog链表添加

如下函数tcp_v4_rcv所示,函数sock_owned_by_user检查套接口是否被应用进程在使用,如果没有,执行正常的TCP接收操作;否则,将调用函数tcp_add_backlog将数据包添加到backlog链表中,在用户进程释放套接口后,内核还是会调用tcp_v4_do_rcv函数执行接收操作。

int tcp_v4_rcv(struct sk_buff *skb)
{
    if (!sock_owned_by_user(sk)) {
        ret = tcp_v4_do_rcv(sk, skb);
    } else if (tcp_add_backlog(sk, skb)) {
        goto discard_and_relse;
    }
}
backlog链表的添加操作不能执行接收队列(包括sk_receive_queue和out_of_order_queue)的减小内存占用类的操作(collapse/prune),只有套接口的所有者可执行此类操作。在tcp_add_backlog函数执行链表添加操作之前,内核在最大接收缓存和发送缓存之和的基础之上,再增加64K的额外量以保证成功添加,由于系统中在sk_backlog链表中同时存储有数据的套接口属于少数情况,增加额外量不会有问题。

之后,还是要使用函数skb_condense尝试一下对skb进行空间压缩,算法很简单:如果skb的线性空间有足够的剩余,就可将其共享空间中的页面片段拷贝到线性空间中,以释放页面片段。如果线性空间中的剩余量小于页面片段的长度,或者此skb被克隆过(页面片段共享给了其它skb),不执行任何压缩。

bool tcp_add_backlog(struct sock *sk, struct sk_buff *skb)
{   
    u32 limit = sk->sk_rcvbuf + sk->sk_sndbuf;
    
    limit += 64*1024;
    skb_condense(skb);
    
    if (unlikely(sk_add_backlog(sk, skb, limit))) {
        bh_unlock_sock(sk);
        return true;
    }
    return false;
}
函数sk_add_backlog如下,如果即便内核增加了64K的缓存限额,backlog链表占用的空间与套接口接收缓存之和仍然大于限额,返回无缓存的错误。此处的qsize未考虑当前skb数据包的空间占用量,即不管其大小,只要还有空间就将其接收。但是,如果skb的空间是由系统的PF_MEMALLOC保留区分配而来,并且套接口未设置SOCK_MEMALLOC标志,即内存已经处于紧张状态,此套接口还不能够帮助释放内存,返回无内存错误。最终在函数__sk_add_backlog将数据包添加到backlog链表后,为链表的长度增加skb的truesize值。

static inline bool sk_rcvqueues_full(const struct sock *sk, unsigned int limit)
{   
    unsigned int qsize = sk->sk_backlog.len + atomic_read(&sk->sk_rmem_alloc);
    return qsize > limit;

static inline __must_check int sk_add_backlog(struct sock *sk, struct sk_buff *skb, unsigned int limit)
{
    if (sk_rcvqueues_full(sk, limit))
        return -ENOBUFS;
    
    /*
     * If the skb was allocated from pfmemalloc reserves, only
     * allow SOCK_MEMALLOC sockets to use it as this socket is
     * helping free memory
     */
    if (skb_pfmemalloc(skb) && !sock_flag(sk, SOCK_MEMALLOC))
        return -ENOMEM;
    
    __sk_add_backlog(sk, skb);
    sk->sk_backlog.len += skb->truesize;
    return 0;
}

函数__sk_add_backlog执行简单的单链表添加操作,backlog链表的尾部skb的next指针指向新添加的skb,并且,将新的skb地址赋值给backlog的尾部tail指针。

static inline void __sk_add_backlog(struct sock *sk, struct sk_buff *skb)
{   
    if (!sk->sk_backlog.tail) 
        sk->sk_backlog.head = skb;
    else
        sk->sk_backlog.tail->next = skb;
    
    sk->sk_backlog.tail = skb;
    skb->next = NULL;
}

二、backlog链表处理
以上提到backlog链表中的数据最终还是由tcp_v4_do_rcv函数进行处理。由于在TCP套接口初始化时,内核已经将TCP协议结构的成员backlog_rcv回调函数指针赋予了函数tcp_v4_do_rcv的值,后续又将backlog_rcv指针赋给了套接口的sk_backlog_rcv函数指针。

struct proto tcp_prot = {
    .name           = "TCP",
    .backlog_rcv        = tcp_v4_do_rcv,
    .release_cb     = tcp_release_cb,
}
static int inet_create(struct net *net, struct socket *sock, int protocol, int kern)
{
    sk = sk_alloc(net, PF_INET, GFP_KERNEL, answer_prot, kern);
    if (!sk)
        goto out;
 
    sk->sk_backlog_rcv = sk->sk_prot->backlog_rcv;
}
backlog链表接收处理函数为sk_backlog_rcv,与链表添加时的操作对应,对于设置了SOCK_MEMALLOC标志的套接口,并且数据包skb的内存是由系统的PF_MEMALLOC保留内存区分配而来的情况,使用__sk_backlog_rcv函数处理数据包。其它情况下直接调用回调函数sk_backlog_rcv处理。

函数memalloc_noreclaim_save将为当前进程增设PF_MEMALLOC标志,memalloc_noreclaim_restore函数还原前值。

static inline int sk_backlog_rcv(struct sock *sk, struct sk_buff *skb)
{
    if (sk_memalloc_socks() && skb_pfmemalloc(skb))
        return __sk_backlog_rcv(sk, skb);
 
    return sk->sk_backlog_rcv(sk, skb);
}
int __sk_backlog_rcv(struct sock *sk, struct sk_buff *skb)
{
    BUG_ON(!sock_flag(sk, SOCK_MEMALLOC));
 
    noreclaim_flag = memalloc_noreclaim_save();
    ret = sk->sk_backlog_rcv(sk, skb);
    memalloc_noreclaim_restore(noreclaim_flag);
}

三、backlog链表处理时机

以应用层进程的数据发送为例,内核的tcp_sendmsg函数在处理请求之后,调用release_sock释放套接口锁。如果在此期间接收到网络数据包,内核已将其放入了backlog链表中,在释放套接口时,就需要检查以下backlog链表,已有机会进行尽快处理。如果其尾部tail不为空,说明链表非空,调用__release_sock处理。

int tcp_sendmsg(struct sock *sk, struct msghdr *msg, size_t size)
{
    lock_sock(sk);
    ret = tcp_sendmsg_locked(sk, msg, size);
    release_sock(sk);
    return ret;
}
void release_sock(struct sock *sk)
{
    spin_lock_bh(&sk->sk_lock.slock);
    if (sk->sk_backlog.tail)
        __release_sock(sk);
    if (sk->sk_prot->release_cb)
        sk->sk_prot->release_cb(sk);
    sock_release_ownership(sk);
    spin_unlock_bh(&sk->sk_lock.slock);
}

需要特别注意的是函数__release_sock存在两个嵌套的循环,内层的循环是遍历backlog链表,处理其中的每个数据包skb元素,在进入内存循环之前将backlog链表的头尾两个指针清空,并且在遍历过程中内核有可能发送重调度,如果在调度其将有新的网络数据包到来,就需要外层的循环在次对backlog链表进行判断处理。

static void __release_sock(struct sock *sk)
    __releases(&sk->sk_lock.slock)
    __acquires(&sk->sk_lock.slock)
{
    while ((skb = sk->sk_backlog.head) != NULL) {
        sk->sk_backlog.head = sk->sk_backlog.tail = NULL;
 
        spin_unlock_bh(&sk->sk_lock.slock);
        do {
            next = skb->next;
            prefetch(next);
            WARN_ON_ONCE(skb_dst_is_noref(skb));
            skb->next = NULL;
            sk_backlog_rcv(sk, skb);
 
            cond_resched();
            skb = next;
        } while (skb != NULL);
        spin_lock_bh(&sk->sk_lock.slock);
    }
    sk->sk_backlog.len = 0;
}

另外,内核定义了backlog链表flush函数,如下sk_flush_backlog,其本质上是对__release_sock函数的封装。其调用点位于TCP发送函数tcp_sendmsg_locked中,使得内核在发送大量数据时,不必等到函数退出才执行backlog链表的处理。

static inline bool sk_flush_backlog(struct sock *sk)
{
    if (unlikely(READ_ONCE(sk->sk_backlog.tail))) {
        __sk_flush_backlog(sk);
        return true;
    }
    return false;
}
void __sk_flush_backlog(struct sock *sk)
{
    spin_lock_bh(&sk->sk_lock.slock);
    __release_sock(sk);
    spin_unlock_bh(&sk->sk_lock.slock);
}
与TCP发送函数类似,在TCP的tcp_recvmsg接收函数中,如果用户进程设置了非阻塞模式接收,在拷贝完套接口接收队列sk_receive_queue中的数据之后,假设用户层提供的接收缓存长度还有余量,并且backlog链表中有数据,调用release_sock处理backlog链表,处理之后的数据可能又填充到了sk_receive_queue接收队列中,再次处理此队列时,可尽量返回给用户层要求长度的数据。

int tcp_recvmsg(struct sock *sk, struct msghdr *msg, size_t len, int nonblock, int flags, int *addr_len)
{
    do {
        last = skb_peek_tail(&sk->sk_receive_queue);
        skb_queue_walk(&sk->sk_receive_queue, skb) {
        }
 
        /* Well, if we have backlog, try to process it now yet. */
        if (copied >= target && !sk->sk_backlog.tail)
            break;
 
        if (copied >= target) {
            /* Do not sleep, just process backlog. */
            release_sock(sk);
            lock_sock(sk);
        } else {
            sk_wait_data(sk, &timeo, last);
        }
        continue;
    } while (len > 0);
}

总结
对于非TCP协议套接口,例如L2TP、PPTP和PPPOE等类型套接口,其控制报文需要上送应用层处理,内核使用sk_receive_skb函数实现。最终的逻辑在函数__sk_receive_skb中。

static int l2tp_ip_recv(struct sk_buff *skb)
{
    /* RFC3931: L2TP/IP packets have the first 4 bytes containing
     * the session_id. If it is 0, the packet is a L2TP control
     * frame and the session_id value can be discarded.
     */
    if (session_id == 0) {
        __skb_pull(skb, 4);
        goto pass_up;
    }
pass_up:
    return sk_receive_skb(sk, skb, 1);
}
如下,如果套接口被用户进程使用,也是要添加到backlog链表中,否则,直接使用sk_backlog_rcv进行处理。对于以上三个类型的套接口,其backlog_rcv回调函数与TCP套接口的不同,分别为pppol2tp_backlog_recv、pptp_rcv_core和pppoe_rcv_core。

int __sk_receive_skb(struct sock *sk, struct sk_buff *skb, const int nested, unsigned int trim_cap, bool refcounted)
{
    if (sk_rcvqueues_full(sk, sk->sk_rcvbuf)) {
        atomic_inc(&sk->sk_drops);
        goto discard_and_relse;
    }
    if (!sock_owned_by_user(sk)) {
        mutex_acquire(&sk->sk_lock.dep_map, 0, 1, _RET_IP_);
        rc = sk_backlog_rcv(sk, skb);
        mutex_release(&sk->sk_lock.dep_map, 1, _RET_IP_);
    } else if (sk_add_backlog(sk, skb, sk->sk_rcvbuf)) {
        bh_unlock_sock(sk);
        atomic_inc(&sk->sk_drops);
        goto discard_and_relse;
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

C++编程指南

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值