评价linux协议栈tcp实现中的prequeue

对tcp-ack的影响:某种情形之下只有用户进程处理了这个skb的时候才发送ack。如果skb直接排入receive_queue的话,那么很可能直接就会发送skb,但是如果排入了prequeue,那么只有到了进程的上下文处理prequeue的时候才会处理ack应答,理论上如果这个进程很久才能被进程调度器调度到,那么很久才会发送ack,linux的解决办法是设置一个timer,规定一个阀值到期,到期后timer将prequeue中的skb放到receive_queue中。从对ack的影响来看看似延迟的ack是一件坏事,但是有时它会是一件好事,因为它能平缓对端的发送速率,如果ack马上回复的话,也就是说skb到达receive_queue中就回复的话,某一时刻突然有很多数据从对端发来,那么按照速率线性增加的原则来看,对端可以增长到很大的速率,但是本端可能并没有太活跃的进程在读取,于是就会快速盛满tcp接收缓冲区,由于迟迟收不到ack,对端发送瞬间停止,这种抖动无论从什么意义上讲都是不好的,如果在进程上下文处理ack的话,就真正建立了端到端的连接,是进程到进程的连接,而不是协议栈1-tcp到协议栈2-tcp的连接,这样的话无论收还是发都会受到进程活跃度,进程调度器等关于操作系统更全局因素的影响,网络性能和操作系统性能以及机器性能会更好的匹配。不过话又说回来,如果对于某些突发的小量的但是非常重要或者对延迟敏感的消息而言,接受者的prequeue会对发送者产生不好的影响。
对公平性的影响:tcp协议标准内置了理想理论情况下的公平性,操作系统本身也必须是公平的,不论是进程调度还是中断处理还是内存使用等等都应该是加权公平的,现在看看软中断上下文中的tcp_rcv_established会做些什么:它会调用tcp_checksum_complete_user来计算checksum。而计算checksum是一个很耗时的操作,任意上下文的软中断不应该做如此长延迟的操作,因此最好将它们放到进程上下文中运行,然而此处的考虑有一个前提,那就是当前socket的使用者必须存在,这是因为socket是可以被子进程继承的,必须将数据复制到正确的进程,另外就是当前进程必须没有在读取该socket上的数据,这是为了保证读取顺序。prequeue这个特性在理论上给与了别的进程以及别的socket连接更多的公平性,但是由于当今很多网卡都内置了checksum计算,因此很少需要再在将skb入队的时候计算checksum了,因此prequeue的这点公平性上的优势就意义不大了。
对cache的影响:尽量不要做wake-up的操作,尽量去信任内核的调度系统而不要人为的睡眠或者唤醒,除非遇到没有资源这一类情况或者资源多多,这是为了保证cache的活力,因为每切换一次进程就需要刷新一次cache,因此prequeue就没有将skb放入后唤醒进程,而是直接返回,实际上软中断的tcp_v4_rcv也会有可能被用户进程抢占的,因为它也有一个ksoftirqd可用,prequeue机制就是尽可能的不做进程切换,这看起来很好,但是存在一个时间问题,内核cache不刷新,但是可以被替换,毕竟cache的大小要比内存小得多,skb的数据在内核中被处理,因此其数据会填充cache,但是如果这些cache很长一段时间没有被访问到的话,那么就很可能会被替换,用全相联cache很容易理解这一点,那么如果想得到更好的效率,就必须尽快地唤醒用户进程读取skb,在没有prequeue之前的做法是将skb放入receive_queue,然后唤醒等待网络数据的用户进程,如果没有用户等待,则内核什么也不做,而prequeue的做法可能导致很久很久以后用户进程才会醒来:
sk_wait_data(sk, &timeo);
也就是timeo时间之后,虽然这种情况发生的概率很低,但是确实是可以发生的,sk_wait_data中的睡眠和release_sock之间有个空档,如果这个空档中有进程排入prequeue,那么skb入队函数tcp_prequeue下面的skb_queue_len(&tp->ucopy.prequeue) == 1就不会通过判断,因此就不会唤醒进程,最终导致进程睡眠timeo的时候,这段时间内可能发生了很多的事情,比如替换skb数据的cache。

对一些概念的澄清:tcp接收的路径分为fast和slow两种,所谓的fast并不是处理prequeue的path,而slow也并不是处理backlog和receive_queue的path,所谓的fast-path需要很多附加的条件,fast的含义是复制到用户空间或者按序进入receive_queue的处理路径,而slow-path指的是对那些乱序包或者别的不符合fast-path要求的包的处理路径。这些path和skb进入哪个队列没有关系,一般可以将fast-path分为两类,直接复制进用户空间的路径是most-fast-path,没有进程上下文导致按序的skb进如receive_queue的是more-fast-path。由于处理乱序包或者缓冲区用尽这些情况很麻烦,因此处理这些情况的path就是slow-path,具体是什么path是根据tcp语义和数据包特性来确定的,和具体协议栈的实现没有关系。
prequeue的积累性:prequeue将skb积累于一个队列,这种积累时间长了会带来延迟,因此它只在ucopy拥有进程上下文的时候才进行skb的积累,最小化延迟,最终prequeue的积累最小化了进程切换,否则如果没有prequeue的话,将尽可能往receive_queue中放置skb,然后每放置一个就会wakeup那个进程,这可能会导致进程频繁切换,还不如统一放置进一个prequeue队列,事实上仅就积累skb来讲应该存在另外的方式,我们知道prequeue虽然做到了累积skb,但是也存在了很多的弊端,见上述文字。
总结:linux内核中有一个很可笑的开关,那就是sysctl_tcp_low_latency,从字面上理解,打开它就会得到相对低的延迟,然而打开它就禁用了prequeue,于是我们将prequeue和低效联系起来,于是我们反思prequeueu的起源和当今的意义。

你可能感兴趣的:(linux)