tcp sk_backlog(后备队列分析)

三个接收队列

  tcp协议栈数据接收实现了三个接收缓存分别是prequeue、sk_write_queue、sk_backlog。

之所以需要三个接收缓存的原因如下:

tcp协议栈接收到数据包时struct sock *sk 可能被进程下上文或者中断上下文占用:

1、如果处于进程上下文sk_lock.owned=1,软中断因为sk_lock.owned=1,所以数据只能暂存在后备队列中(backlog),当进程上下文逻辑处理完成后会回调tcp_v4_do_rcv处理backlog队列作为补偿,具体看tcp_sendmsg 函数 release_sock的实现。

2、如果当前处于中断上下文,sk_lock.owned=0,那么数据可能被放置到receive_queue或者prequeue,数据优先放置到prequeue中,如果prequeue满了则会放置到receive_queue中,理论上这里有一个队列就行了,但是TCP协议栈为什么要设计两个呢?其实是为了快点结束软中断数据处理流程,软中断处理函数中禁止了进程抢占和其他软中断发生,效率应该是很低下的,如果数据被放置到prequeue中,那么软中断流程很快就结束了,如果放置到receive_queue那么会有很复杂的逻辑需要处理。receive_queue队列的处理在软中断中,prequeue队列的处理则是在进程上下文中。总的来说就是为了提高TCP协议栈的效率。

后备队列的处理逻辑

1、什么时候使用后备队列

tcp协议栈对struct sock *sk有两把锁,第一把是sk_lock.slock,第二把则是sk_lock.owned。sk_lock.slock用于获取struct sock *sk对象的成员的修改权限;sk_lock.owned用于区分当前是进程上下文或是软中断上下文,为进程上下文时sk_lock.owned会被置1,中断上下文为0。

如果是要对sk修改,首先是必须拿锁sk_lock.slock,其后是判断当前是软中断或是进程上下文,如果是进程上下文,那么接收到的skb则只能先放置到后备队列中sk_backlog中。如果是软中断上下文则可以放置到prequeue和sk_write_queue中。

代码片段如下:

bh_lock_sock_nested(sk);

ret = 0;

if (!sock_owned_by_user(sk)) {

#ifdef CONFIG_NET_DMA

struct tcp_sock *tp = tcp_sk(sk);

if (!tp->ucopy.dma_chan && tp->ucopy.pinned_list)

tp->ucopy.dma_chan = dma_find_channel(DMA_MEMCPY);

if (tp->ucopy.dma_chan)

ret = tcp_v4_do_rcv(sk, skb);

else

#endif

{

if (!tcp_prequeue(sk, skb))

ret = tcp_v4_do_rcv(sk, skb);

}

} else if (unlikely(sk_add_backlog(sk, skb,

sk->sk_rcvbuf + sk->sk_sndbuf))) {

bh_unlock_sock(sk);

NET_INC_STATS_BH(net, LINUX_MIB_TCPBACKLOGDROP);

goto discard_and_relse;

}

bh_unlock_sock(sk);

bh_lock_sock_nested(sk);

获取第一把锁。

if (!sock_owned_by_user(sk))

判断第二把锁,区分是处于进程上下文还是软中断上下文。

if (!tcp_prequeue(sk, skb))

ret = tcp_v4_do_rcv(sk, skb);

如果处于中断上下文,则优先放置到prequeue中,如果prequeue满则放置到sk_write_queue中。

} else if (unlikely(sk_add_backlog(sk, skb,

sk->sk_rcvbuf + sk->sk_sndbuf))) {

如果是处于进程上下文则直接放置到后备队列中(sk_backlog中)。

2、skb怎么add到sk_backlog中

sk_add_backlog函数用于add sbk到sk_backlog中,所以下面我们分析次函数。

/* The per-socket spinlock must be held here. */

static inline __must_check int sk_add_backlog(struct sock *sk, struct sk_buff *skb,

unsigned int limit)

{

if (sk_rcvqueues_full(sk, skb, limit))

return -ENOBUFS;

__sk_add_backlog(sk, skb);

sk_extended(sk)->sk_backlog.len += skb->truesize;

return 0;

}

if (sk_rcvqueues_full(sk, skb, limit))

return -ENOBUFS;

判断接收缓存是否已经用完了,很明显sk_backlog的缓存大小也算在了总接收缓存中。

__sk_add_backlog(sk, skb);

将skb添加到sk_backlog队列中。

sk_extended(sk)->sk_backlog.len += skb->truesize;

更新sk_backlog中已经挂载的数据量。

/* OOB backlog add */

static inline void __sk_add_backlog(struct sock *sk, struct sk_buff *skb)

{

if (!sk->sk_backlog.tail) {

sk->sk_backlog.head = sk->sk_backlog.tail = skb;

} else {

sk->sk_backlog.tail->next = skb;

sk->sk_backlog.tail = skb;

}

skb->next = NULL;

}

if (!sk->sk_backlog.tail) {

sk->sk_backlog.head = sk->sk_backlog.tail = skb;

如果当前sk_backlog为NULL,此时head和tail都指向skb。

} else {

sk->sk_backlog.tail->next = skb;

sk->sk_backlog.tail = skb;

分支表示sk_backlog中已经有数据了,那么skb直接挂在tail的尾部,之后tail指针后移到skb。

skb->next = NULL;

这种很重要,在sk_backlog处理时会用来判断skb是否处理完毕。

3、sk_backlog中skb的处理

很明显sk_backlog的处理必然中进程上下文进行,对于数据接收,进成上下文的接口是tcp_recvmmsg,所以sk_backlog肯定要在tcp_recvmmsg中处理。

tcp_recvmmsg sk_backlog的代码处理片段如下:

tcp_cleanup_rbuf(sk, copied);

TCP_CHECK_TIMER(sk);

release_sock(sk);

release_sock(sk)涉及到sk_backlog处理。

void release_sock(struct sock *sk)

{

/*

* The sk_lock has mutex_unlock() semantics:

*/

mutex_release(&sk->sk_lock.dep_map, 1, _RET_IP_);

spin_lock_bh(&sk->sk_lock.slock);

if (sk->sk_backlog.tail)

__release_sock(sk);

if (proto_has_rhel_ext(sk->sk_prot, RHEL_PROTO_HAS_RELEASE_CB) &&

sk->sk_prot->release_cb)

sk->sk_prot->release_cb(sk);

sk->sk_lock.owned = 0;

if (waitqueue_active(&sk->sk_lock.wq))

wake_up(&sk->sk_lock.wq);

spin_unlock_bh(&sk->sk_lock.slock);

}

spin_lock_bh(&sk->sk_lock.slock);

获取第一把锁。

if (sk->sk_backlog.tail)

__release_sock(sk);

如果后备队列不为NULL,则开始处理。

sk->sk_lock.owned = 0;

进成上下文skb处理完了,释放第二把锁。

spin_unlock_bh(&sk->sk_lock.slock);

释放第一把锁。

__release_sock(sk)是后备队列的真正处理函数。

static void __release_sock(struct sock *sk)

{

struct sk_buff *skb = sk->sk_backlog.head;

do {

sk->sk_backlog.head = sk->sk_backlog.tail = NULL;

bh_unlock_sock(sk);

do {

struct sk_buff *next = skb->next;

skb->next = NULL;

sk_backlog_rcv(sk, skb);

/*

* We are in process context here with softirqs

* disabled, use cond_resched_softirq() to preempt.

* This is safe to do because we‘ve taken the backlog

* queue private:

*/

cond_resched_softirq();

skb = next;

} while (skb != NULL);

bh_lock_sock(sk);

} while ((skb = sk->sk_backlog.head) != NULL);

/*

* Doing the zeroing here guarantee we can not loop forever

* while a wild producer attempts to flood us.

*/

sk_extended(sk)->sk_backlog.len = 0;

}

sk->sk_backlog.head = sk->sk_backlog.tail = NULL;

bh_unlock_sock(sk);

重置sk->sk_backlog.head ,sk->sk_backlog.tail为NULL。sk_backlog是一个双链表,head指向了链表头部的skb,而tail则指向了链表尾部的skb。这里之所以置NULL head 和tail,是因为struct sk_buff *skb = sk->sk_backlog.head 提前取到了head指向的skb,之后就可以通过skb->next来获取下一个skb处理,结束的条件是skb->next=NULL,这个是在__sk_add_backlog函数中置位的,也就说对于sk_backlog的处理head和tail指针已经没有用了。

为什么要置NULLsk->sk_backlog.head ,sk->sk_backlog.tail呢?第一想法是它可能要被重新使用了。那么在什么情况下会被重新使用呢?试想一下当前是在进程上下文,并且sk->sk_lock.slock没有被锁住,那是不是可能被软中断打断呢?如果被软中断打断了是不是要接收数据呢,tcp协议栈为了效率考虑肯定是要接收数据的,前面分析道这种情况的数据必须放置到后备队列中(sk_backlog),所以可以肯定置NULL sk->sk_backlog.head ,sk->sk_backlog.tail是为了在处理上一个sk_backlog时,能重用sk_backlog,建立一条新的sk_backlog,或许有人会问为什么不直接添加到原先的sk_backlog tail末尾呢?这个问题我也没有想太清楚,或许是同步不好做吧。

sk_backlog_rcv(sk, skb);

skb的处理函数,其实调用的是tcp_v4_do_rcv函数。

} while (skb != NULL);

如果skb=NULL,那么说明之前的sk_backlog已经处理完了。

} while ((skb = sk->sk_backlog.head) != NULL);

在处理上一个sk_backlog时,可能被软中断中断了,建立了新的sk_backlog,新建立的sk_backlog也将一并被处理。

4、skb被处理到哪去了

很明显接收的数据最终都将被传递到应用层,在传递到应用层前必须要保证三个接收队列中的数据有序,那么这三个队列是怎么保证数据字节流有序的被递交给应用层呢?如果仔细分析tcp_v4_do_rcv函数能发现,这个函数能保证数据有序的排列在一起,所以无论是在处理sk_backlog还是prequeue,最终都会调用tcp_v4_do_rcv函数将数据有效地插入到sk_write_queue中,最后被应用层取走。

时间: 2024-08-02 16:14:38

tcp sk_backlog(后备队列分析)的相关文章

TCP协议疑难杂症全景分析

说明: 1).本文以TCP的发展历程解析容易引起混淆,误会的方方面面 2).本文不会贴大量的源码,大多数是以文字形式描述,我相信文字看起来是要比代码更轻松的 3).针对对象:对TCP已经有了全面了解的人.因为本文不会解析TCP头里面的每一个字段或者3次握手的细节,也不会解释慢启动和快速重传的定义 4).除了<TCP/IP详解>(卷一,卷二)以及<Unix网络编程>以及Linux源代码之外,学习网络更好的资源是RFC 5).本文给出一个提纲,如果想了解细节,请直接查阅RFC 6).翻

基于Jpcap的TCP/IP数据包分析(一)

基于Jpcap的TCP/IP数据包分析原作:赵新辉目 录第一章 以太网的结构和TCP/IP1.1 以太网的结构1.1.1 基于网络架构的以太网1.1.2 以太网的数据交换1.1.3 以太网帧的结构1.2 IP数据报的构成 1.2.1 IP地址1.2.2 路由1.2.3 IP数据报的构成1.2.4 其他报文结构1.3 TCP/UDP1.3.1 TCP/UDP的作用1.3.2 TCP和UDP报文的结构第二章 Jpcap类库2.1 Jpcap的使用2.1.1 Jpcap的运行环境的安装2.1.2 Jp

TCP与UDP协议分析

1 案例1:TCP与UDP协议分析1.1 问题1.通过抓包分析TCP与UDP的封装格式2.通过抓包分析TCP三次握手1.2 方案1.实验环境由两台主机PC1和PC2组成,PC1使用宿主机,PC2使用VMWare虚拟机,确保两台主机通信正常(需要关闭PC2的防火墙)2.在PC1上运行科来进行抓包3.在PC1上通过远程桌面访问PC2,然后在PC1上分析数据包,如图-1所示.1.3 步骤实现此案例需要按照如下步骤进行. 1.第一次握手,如图-2所示. 2.第二次握手,如图-3所示.3.第三次握手,如图

TCP粘包问题分析和解决(全)

TCP通信粘包问题分析和解决(全) 在socket网络程序中,TCP和UDP分别是面向连接和非面向连接的.因此TCP的socket编程,收发两端(客户端和服务器端)都要有成对的socket,因此,发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法),将多次间隔较小.数据量小的数据,合并成一个大的数据块,然后进行封包.这样,接收端,就难于分辨出来了,必须提供科学的拆包机制. 对于UDP,不会使用块的合并优化算法,这样,实际上目前认为,是由于UDP支持的是一对多的模式,

操作系统tcp流量监控与分析

把back_log打满导致的连接异常 dmesg里面也看到了队列溢出的日志,应试是刚才分析的原因没错了.TCP: time wait bucket table overflow 原文地址:https://www.cnblogs.com/sq892246139/p/8555795.html

关于TCP全连接队列和半连接队列

转:https://www.toutiao.com/a6721163619758768647/ 在TCP的三次握手中存在着两个队列.backlog.tcp_abort_on_overflow等概念知识点.常见的连接服务异常有很多,如Connection refused等问题.通过对这些知识的理解有助于结合一些排查手段有效地解决一些生产上出现的连接服务异常问题.下面将对这些进行讨论分析. 一.TCP三次握手 握手过程 第一次:client发送syn到server进行握手 第二次:server收到s

TCP内核源码分析笔记

Table of Contents 1 术语 1.1 ABC 1.2 SACK 1.3 D-SACK 1.4 F-RTO 1.5 template 2 tcp_v4_connect() 3 sys_accept() 3.1 tcp_accept() 4 三次握手 4.1 客户端发送SYN段 4.2 服务端接收到SYN段后,发送SYN/ACK处理 4.3 客户端回复确认ACK段 4.3.1 tcp_rcv_synsent_state_process() 4.4 服务端收到ACK段 5 数据传输 5

数据结构--二项队列分析及实现

一,介绍 什么是二项队列,为什么会用到二项队列? 与二叉堆一样,二项队列也是优先级队列的一种实现方式.在 数据结构--堆的实现之深入分析 的末尾 ,简单地比较了一下二叉堆与二项队列. 对于二项队列而言,它可以弥补二叉堆的不足:merge操作的时间复杂度为O(N).二项队列的merge操作的最坏时间复杂度为O(logN). 二,二项队列的基本操作及实现 在详细介绍二项的队列的基本操作之前,先了解下二项队列这种数据结构: 1)一个二项队列是若干棵树的集合.也就是说,二项队列不仅仅是一棵树,而是多棵树

epoll()无论涉及wait队列分析

事件1. epfd-file->eventpoll->wq: struct eventpoll { ... wait_queue_head_t wq;     //用于epoll_pwait()事件的等待队列 情况1分析 struct list_head rdllist; //就绪的fd队列 ready list struct rb_root rbr;       //红黑树根,epitem->rbn为红黑树结构的节点 struct file *file;        //epoll文