TSO相关的内容充斥着TCP的整个发送过程,弄明白其机制对理解TCP的发送过程至关重要,这篇笔记就来看看TSO相关内容。
1. 基本概念
我们知道,网络设备一次能够传输的最大数据量就是MTU,即IP传递给网络设备的每一个数据包不能超过MTU个字节,IP层的分段和重组功能就是为了适配网络设备的MTU而存在的。从理论上来讲,TCP可以不关心MTU的限定,只需要按照自己的意愿随意的将数据包丢给IP,是否需要分段可以由IP透明的处理,但是由于分片会带来效率和性能上的损失,所以TCP在实现时总是会基于MTU设定自己的发包大小,尽量避免让数据包在IP层分片,也就是说TCP会保证一个TCP段经过IP封装后传给网络设备时,数据包的大小不会超过网络设备的MTU。
TCP的这种实现会使得其必须对用户空间传入的数据进行分段,这种工作很固定,但是会耗费CPU时间,所以在高速网络中就想优化这种操作。优化的思路就是TCP将大块数据(远超MTU)传给网络设备,由网络设备按照MTU来分段,从而释放CPU资源,这就是TSO(TCP Segmentation Offload)的设计思想。
显然,TSO需要网络设备硬件支持。更近一步,TSO实际上是一种延迟分段技术,延迟分段会减少发送路径上的数据拷贝操作,所以即使网络设备不支持TSO,只要能够延迟分段也是有收益的,而且也不仅仅限于TCP,对于其它L4协议也是可以的,这就衍生出了GSO(Generic Segmentation Offload)。这种技术是指尽可能的延迟分段,最好是在设备驱动程序中进行分段处理,但是这样一来就需要修改所有的网络设备驱动,不太现实,所以再提前一点,在将数据递交给网络设备的入口处由软件进行分段(见dev_queue_xmit()),这正是Linux内核的实现方式。
注:类似的一些概念如LSO、UFO等,可以类比理解,这里不再叙述。
2. TCP延迟分段判定
对于TCP来讲,无论最终延迟分段是由TSO(网络设备)实现,还是由软件来实现(GSO),TCP的处理都是一样的。下面来看看TCP到底是如何判断自己是否可以延迟分段的。
static inline int sk_can_gso(const struct sock *sk)
{
//实际上检查的就是sk->sk_route_caps是否设定了sk->sk_gso_type能力标记
return net_gso_ok(sk->sk_route_caps, sk->sk_gso_type);
}
static inline int net_gso_ok(int features, int gso_type)
{
int feature = gso_type << NETIF_F_GSO_SHIFT;
return (features & feature) == feature;
}
sk_route_caps字段代表的是路由能力;sk_gso_type表示的是L4协议期望底层支持的GSO技术。这两个字段都是在三次握手过程中设定的,客户端和服务器端的初始化分别如下。
2.1 客户端初始化
客户端是在tcp_v4_connect()中完成的,相关代码如下:
int tcp_v4_connect(struct sock *sk, struct sockaddr *uaddr, int addr_len)
{
...
//设置GSO类型为TCPV4,该类型值会体现在每一个skb中,底层在
//分段时需要根据该类型区分L4协议是哪个,以做不同的处理
sk->sk_gso_type = SKB_GSO_TCPV4;
//见下面
sk_setup_caps(sk, &rt->u.dst);
...
}
2.2 服务器端初始化
服务器端是在收到第三个ACK后进行的初始化,相关代码如下:
struct sock *tcp_v4_syn_recv_sock(struct sock *sk, struct sk_buff *skb,
struct request_sock *req,
struct dst_entry *dst)
{
...
//同上
newsk->sk_gso_type = SKB_GSO_TCPV4;
sk_setup_caps(newsk, dst);
...
}
2.3 sk_setup_caps()
设备和路由是相关的,L4协议会先查路由,所以设备的能力最终会体现在路由缓存中,sk_setup_caps()就是根据路由缓存中的设备能力初始化sk_route_caps字段。
enum {
SKB_GSO_TCPV4 = 1 << 0,
SKB_GSO_UDP = 1 << 1,
/* This indicates the skb is from an untrusted source. */
SKB_GSO_DODGY = 1 << 2,
/* This indicates the tcp segment has CWR set. */
SKB_GSO_TCP_ECN = 1 << 3,
SKB_GSO_TCPV6 = 1 <<