IT技术博客大学习 共学习 共进步

TSQ 的原理

A Geek's Page 2012-11-05 22:11:03 浏览 6,883 次

   通常看到 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(),看它的定义:


  • 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);
  •        }
  • }

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

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


  • 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);
  •        }
  • }

   这里对加入到 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 实现非常熟悉才可以,由此亦可见作者水平之高。

建议继续学习

  1. 无锁消息队列 (阅读 14,125)
  2. 多线程队列的算法优化 (阅读 7,603)
  3. 各消息队列软件产品大比拼 (阅读 6,084)
  4. Gearman Server 使用 MySQL UDFs 来管理和保持队列 (阅读 5,762)
  5. RabbitMQ与Redis队列对比 (阅读 4,225)
  6. 一些队列理论 吞吐量、延迟和带宽 (阅读 4,162)
  7. 实现多线程对队列的读写操作(封装类) (阅读 4,084)
  8. 无锁消息队列 (阅读 3,980)
  9. Feed消息队列架构分析 (阅读 3,723)
  10. 一淘网offline系统简介 (阅读 3,220)