How TSQ works

通常看到 TCP Small Queue (简称 TSQ)之后第一问题是,既然我们已经有 tcp_wmem 了,为何还需要一个新的 sysctl?

这个问题是理解 TSQ 的关键,其实 tcp_wmem 仅仅是从 TCP socket 层限制队列(或 buffer)中最多可以 queue 多少数据包,而实际上,一个包从 TCP 层发出到最后到达网卡,中间还经历了很多个 queue,TCP socket 只是其中一层罢了。而 TSQ 用了一个非常聪明的技巧来限制在所有这些 queue 中的包,从而降低 latency。

TSQ 的代码中最关键的一个地方是它实现了一个新的 skb destructor,也就是 tcp_wfree(),看它的定义:

[c]
static void tcp_wfree(struct sk_buff skb)
{
struct sock
sk = skb->sk;
struct tcp_sock *tp = tcp_sk(sk);

    if (test_and_clear_bit(TSQ_THROTTLED, &tp->tsq_flags) &&
        !test_and_set_bit(TSQ_QUEUED, &tp->tsq_flags)) {
            unsigned long flags;
            struct tsq_tasklet *tsq;

            /* Keep a ref on socket.
             * This last ref will be released in tcp_tasklet_func()
             */
            atomic_sub(skb->truesize - 1, &sk->sk_wmem_alloc);

            /* queue this socket to tasklet queue */
            local_irq_save(flags);
            tsq = &__get_cpu_var(tsq_tasklet);
            list_add(&tp->tsq_node, &tsq->head);
            tasklet_schedule(&tsq->tasklet);
            local_irq_restore(flags);
    } else {
            sock_wfree(skb);
    }

}
[/c]

我们知道在Linux内核网络子系统中,kfree_skb() 是内核丢包的地方,而 skb 的 destructor 就是在丢包时被调用的,用来清理和该 skb 相关的东西。TSQ 实现新的 destructor 就是想在包被丢弃的时候做一些动作,也就是如果条件合适(设置了 TSQ_THROTTLED,没有设置 TSQ_QUEUED),那么就把它加入进 TSQ。

而在下一个 softIRQ 时,相应的 tasklet 就会调度,从而触发 tcp_tasklet_func():

[c]
static void tcp_tasklet_func(unsigned long data)
{
struct tsq_tasklet tsq = (struct tsq_tasklet )data;
LIST_HEAD(list);
unsigned long flags;
struct list_head q, n;
struct tcp_sock tp;
struct sock
sk;

    local_irq_save(flags);
    list_splice_init(&tsq->head, &list);
    local_irq_restore(flags);

    list_for_each_safe(q, n, &list) {
            tp = list_entry(q, struct tcp_sock, tsq_node);
            list_del(&tp->tsq_node);

            sk = (struct sock *)tp;
            bh_lock_sock(sk);

            if (!sock_owned_by_user(sk)) {
                    tcp_tsq_handler(sk);
            } else {
                    /* defer the work to tcp_release_cb() */
                    set_bit(TCP_TSQ_DEFERRED, &tp->tsq_flags);
            }
            bh_unlock_sock(sk);

            clear_bit(TSQ_QUEUED, &tp->tsq_flags);
            sk_free(sk);
    }

}
[/c]

这里对加入到 TSQ 队列中的 socket 进行处理,对于没有 owner 的 socket(不是由用户使用的socket),直接发送;否则就是推迟到 tcp_release_cb() 中发送,即 release_sock() 的时候。

现在,我们从总体上可以看出 TSQ 的用意了:当发送的包超过 sysctl_tcp_limit_output_bytes 时,就会发生抖动(throttle),这时就会把包推迟发送,推迟到什么时候呢?当然是该队列有空闲的时候!那么什么时候有空闲呢?包被丢弃的时候!内核发送的包是在即将被丢弃的时候(忽略tasklet,用tasklet仅仅是因为skb destructor 中不能进行发送),用户层发送的包则是关闭 socket 的时候,这种时候 TSQ 会有新的空间,所以可以重新入队。

可见,TSQ 的聪明之处在于,它虽然有自己的所谓队列,但并没有计算该队列的长度,而是巧妙地利用了内核中几个关键点来判断何时可以入队!这当然需要对 TCP 实现非常熟悉才可以,由此亦可见作者水平之高。