TCP小队列(TSQ)机制的主要目标是限制单个TCP连接在排队规则(qdisc)层或设备层中积压的数据包数量。通过减少队列中的积压,可以有效降低网络传输的往返时间(RTT)和拥塞窗口(cwnd)的波动,这是缓解“缓冲膨胀”(bufferbloat)这一常见网络性能瓶颈问题的重要手段。
该机制的核心是限制套接字的sk->sk_wmem_alloc值,使其增长不超过一个预设的上限(默认约为128KB)。这意味着,在任何给定时刻,单个TCP套接字在队列中等待发送的数据总量被严格控制。
对于启用了TCP分段卸载(TSO)以提升吞吐量的场景,TSQ机制会将单个TSO大包的大小限制为此上限的一半。这一设计保证了最多只有两个TSO数据包处于“发送中”(in flight)状态,从而在有效控制队列深度的同时,维持了较高的带宽利用率。
此外,设置此限制还有一个有益的副作用:它会自动将通用分段卸载(GSO)的最大数据块大小从标准的65536字节降低到约20000字节。使用更小的GSO/TSO数据包有助于降低高优先级流量的排队延迟,提升整体网络性能。
实现机制
为了实现上述控制,内核将TCP套接字原本的sock_wfree()释放回调函数替换为专用的tcp_wfree()。当队列中的skb(套接字缓冲区)被释放(调用skb_orphan())时,此函数会被触发,从而安排后续数据帧的发送。
由于skb的析构函数(destructor)在执行时可能已经持有了qdisc锁,无法直接重新启动发送流程,因此TSQ将实际的发送工作委托给一个tasklet(软中断下半部任务)。为了提升多核性能,系统为每个CPU核心都维护了一个独立的tasklet及其对应的待处理套接字队列。
效果与可调参数
在实际测试中(例如使用tg3网卡和标准pfifo_fast排队规则),TSQ机制效果显著。它能够在不降低网络标称带宽的前提下,有效抑制队列膨胀。例如,单个netperf会话不会再在qdisc中造成数MB的数据积压,避免了套接字自动调优占用过多系统资源。
该机制的行为可以通过一个新增的Linux系统参数进行调节:
/proc/sys/net/ipv4/tcp_limit_output_bytes:此参数定义了每个TCP套接字输出队列的字节数限制。
注意事项
skb_orphan()通常在网络传输完成时被调用,但有些网络设备驱动程序会在其start_xmit()函数中提前调用它。对于这类驱动,除非它们同时启用了字节队列限制(BQL),否则TSQ机制可能无法生效,单个TCP连接仍有可能会占满整个网卡的发送环缓冲。
核心代码摘要
以下补丁代码片段展示了TSQ在Linux内核中的关键实现逻辑,主要包括状态标志、tasklet初始化、专用的缓冲区释放函数tcp_wfree以及对发送路径的检查限制。
// 在tcp_sock结构中增加TSQ相关成员
struct tcp_sock {
...
struct list_head tsq_node; // 用于挂载到TSQ任务列表
unsigned long tsq_flags;
...
};
// TSQ状态标志定义
enum tsq_flags {
TSQ_THROTTLED, // 发送被限制
TSQ_QUEUED, // 套接字已加入任务队列
TSQ_OWNED, // 套接字被用户态锁定
};
// 每个CPU的TSQ任务结构
struct tsq_tasklet {
struct tasklet_struct tasklet;
struct list_head head; // TCP套接字队列
};
static DEFINE_PER_CPU(struct tsq_tasklet, tsq_tasklet);
// 初始化和定义全局限制参数
int sysctl_tcp_limit_output_bytes __read_mostly = 131072; // 默认限制 ~128KB
void __init tcp_tasklet_init(void) { ... }
// 核心的skb释放函数
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)) {
// 将套接字加入到当前CPU的TSQ任务列表,并调度tasklet
...
tsq = &__get_cpu_var(tsq_tasklet);
list_add(&tp->tsq_node, &tsq->head);
tasklet_schedule(&tsq->tasklet);
...
} else {
sock_wfree(skb); // 回退到标准释放函数
}
}
// 在发送路径中检查并设置限制
static bool tcp_write_xmit(struct sock *sk, ...)
{
...
while ((skb = tcp_send_head(sk))) {
...
// 如果已分配的内存超过限制,则设置THROTTLED标志并中断发送循环
if (atomic_read(&sk->sk_wmem_alloc) >= sysctl_tcp_limit_output_bytes) {
set_bit(TSQ_THROTTLED, &tp->tsq_flags);
break;
}
...
}
}
文章来源参考:LWN.net - TCP Small Queues