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 << 4,
};#define NETIF_F_GSO_SHIFT 16
#define NETIF_F_GSO_MASK    0xffff0000
#define NETIF_F_TSO     (SKB_GSO_TCPV4 << NETIF_F_GSO_SHIFT)
#define NETIF_F_UFO     (SKB_GSO_UDP << NETIF_F_GSO_SHIFT)
#define NETIF_F_TSO_ECN     (SKB_GSO_TCP_ECN << NETIF_F_GSO_SHIFT)
#define NETIF_F_TSO6        (SKB_GSO_TCPV6 << NETIF_F_GSO_SHIFT)#define NETIF_F_GSO_SOFTWARE  (NETIF_F_TSO | NETIF_F_TSO_ECN | NETIF_F_TSO6)void sk_setup_caps(struct sock *sk, struct dst_entry *dst)
{__sk_dst_set(sk, dst);//初始值来源于网络设备中的features字段sk->sk_route_caps = dst->dev->features;//如果支持GSO,那么路由能力中的TSO标记也会设定,因为对于L4协议来讲,//延迟分段具体是用软件还是硬件来实现自己并不关心if (sk->sk_route_caps & NETIF_F_GSO)sk->sk_route_caps |= NETIF_F_GSO_SOFTWARE;//支持GSO时,sk_can_gso()返回非0。还需要对一些特殊场景判断是否真的可以使用GSOif (sk_can_gso(sk)) {//只有使用IPSec时,dst->header_len才不为0,这种情况下不能使用TSO特性if (dst->header_len)sk->sk_route_caps &= ~NETIF_F_GSO_MASK;else//支持GSO时,必须支持SG IO和校验功能,这是因为分段时需要单独设置每个//分段的校验和,这些工作L4是没有办法提前做的。此外,如果不支持SG IO,//那么延迟分段将失去意义,因为这时L4必须要保证skb中数据只保存在线性//区域,这就不可避免的在发送路径中必须做相应的数据拷贝操作sk->sk_route_caps |= NETIF_F_SG | NETIF_F_HW_CSUM;}
}

上述代码中涉及到的几个能力的含义如下表所示:

能力 描述
NETIF_F_GSO 0x0000 0800 如果软件实现的GSO打开,设置该标记。在高版本内核中,该值在register_netdevice()中强制打开的
NETIF_F_TSO 0x0001 0000 网络设备如果支持TSO over IP,设置该标记
NETIF_F_TSO_ECN 0x0008 0000 网络设备如果支持设置了ECE标记的TSO,设置该标记
NETIF_F_TSO6 0x0010 0000 网络设备如果支持TSO over IPv6,设置该标记

3. 整体结构

TSO的处理会影响整个数据包发送路径,不仅仅是TCP层,下面先看一个整体的结构图,然后分析下TCP层发送路径上对TSO的处理,其它协议层的处理待后续补充。

注:图片来源于:https://www.cnblogs.com/lvyilong316/p/6818231.html

如上图所示,在TCP的发送路径上,有如下几个点设计TSO的处理:

  1. tcp_sendmsg()中调用tcp_current_mss()确定一个skb最多可以容纳多少数据量,即确定tp->xmit_size_goal;
  2. tcp_write_xmit()中调用tcp_init_gso_segs()设置skb中GSO字段,底层软件或者网卡将根据这些信息进行分段处理;
  3. 用tso_fragment()对数据包进行分段。

第三点和数据发送关系很大,在笔记TCP数据发送之发送新数中分析,这篇笔记分析1和2。

4. TCP发送路径TSO处理

4.1 tcp_sendmsg()

首先是tcp_sendmsg(),该函数负责将用户空间的数据封装成一个个的skb,所以它需要要知道每个skb应该要容纳多少的数据量,这是通过tcp_current_mss()设定的,代码如下:

int tcp_sendmsg(struct kiocb *iocb, struct socket *sock, struct msghdr *msg,size_t size)
{...//tcp_current_mss()中会设置tp->xmit_size_goalmss_now = tcp_current_mss(sk, !(flags&MSG_OOB));//size_goal就是本次发送每个skb可以容纳的数据量,它是mss_now的整数倍,//后面tcp_sendmsg()在组织skb时,就以size_goal为上界填充数据size_goal = tp->xmit_size_goal;
...
}//在"TCP选项之MSS"笔记中已经分析过该函数确定发送MSS的部分,这里重点关注tp->xmit_size_goal的部分
unsigned int tcp_current_mss(struct sock *sk, int large_allowed)
{struct tcp_sock *tp = tcp_sk(sk);struct dst_entry *dst = __sk_dst_get(sk);u32 mss_now;u16 xmit_size_goal;int doing_tso = 0;mss_now = tp->mss_cache;//不考虑MSG_OOB相关,从前面的介绍中我们可以知道都是支持GSO的if (large_allowed && sk_can_gso(sk) && !tp->urg_mode)doing_tso = 1;//下面三个分支是MSS相关if (dst) {u32 mtu = dst_mtu(dst);if (mtu != inet_csk(sk)->icsk_pmtu_cookie)mss_now = tcp_sync_mss(sk, mtu);}if (tp->rx_opt.eff_sacks)mss_now -= (TCPOLEN_SACK_BASE_ALIGNED +(tp->rx_opt.eff_sacks * TCPOLEN_SACK_PERBLOCK));
#ifdef CONFIG_TCP_MD5SIGif (tp->af_specific->md5_lookup(sk, sk))mss_now -= TCPOLEN_MD5SIG_ALIGNED;
#endif//xmit_size_goal初始化为MSSxmit_size_goal = mss_now;//如果支持TSO,则xmit_size_goal可以更大if (doing_tso) {//65535减去协议层的头部,包括选项部分xmit_size_goal = (65535 -inet_csk(sk)->icsk_af_ops->net_header_len -inet_csk(sk)->icsk_ext_hdr_len -tp->tcp_header_len);//调整xmit_size_goal不能超过对端接收窗口的一半xmit_size_goal = tcp_bound_to_half_wnd(tp, xmit_size_goal);//调整xmit_size_goal为MSS的整数倍xmit_size_goal -= (xmit_size_goal % mss_now);}//将确定的xmit_size_goal记录到TCB中tp->xmit_size_goal = xmit_size_goal;return mss_now;
}/* Bound MSS / TSO packet size with the half of the window */
static int tcp_bound_to_half_wnd(struct tcp_sock *tp, int pktsize)
{//max_window为当前已知接收方所拥有的最大窗口值,这里如果参数pktsize超过//了接收窗口的一半,则调整其大小最大为接收窗口的一半if (tp->max_window && pktsize > (tp->max_window >> 1))return max(tp->max_window >> 1, 68U - tp->tcp_header_len);else//其余情况不做调整return pktsize;
}

4.2 tcp_write_xmit()

static int tcp_write_xmit(struct sock *sk, unsigned int mss_now, int nonagle)
{...unsigned int tso_segs;while ((skb = tcp_send_head(sk))) {...//用MSS初始化skb中的gso字段,返回本skb将会被分割成几个TSO段传输tso_segs = tcp_init_tso_segs(sk, skb, mss_now);BUG_ON(!tso_segs);
...if (tso_segs == 1) {//Nagle算法检测,如果已经有小数据段没有被确认,则本次发送尝试失败if (unlikely(!tcp_nagle_test(tp, skb, mss_now, (tcp_skb_is_last(sk, skb) ? nonagle : TCP_NAGLE_PUSH)))) {break;}} else {if (tcp_tso_should_defer(sk, skb))break;}//limit是本次能够发送的字节数,如果skb的大小超过了limit,那么需要将其切割limit = mss_now;if (tso_segs > 1)limit = tcp_mss_split_point(sk, skb, mss_now, cwnd_quota);if (skb->len > limit && unlikely(tso_fragment(sk, skb, limit, mss_now)))break;
...}
...
}

4.2.1 tcp_init_tso_segs()

该函数设置skb中的GSO相关字段信息,并且返回

/* This must be invoked the first time we consider transmitting* SKB onto the wire.*/
static int tcp_init_tso_segs(struct sock *sk, struct sk_buff *skb, unsigned int mss_now)
{int tso_segs = tcp_skb_pcount(skb);//cond1: tso_segs为0表示该skb的GSO信息还没有被初始化过//cond2: MSS发生了变化,需要重新计算GSO信息if (!tso_segs || (tso_segs > 1 && tcp_skb_mss(skb) != mss_now)) {tcp_set_skb_tso_segs(sk, skb, mss_now);tso_segs = tcp_skb_pcount(skb);}//返回需要分割的段数return tso_segs;
}/* Due to TSO, an SKB can be composed of multiple actual* packets.  To keep these tracked properly, we use this.*/
static inline int tcp_skb_pcount(const struct sk_buff *skb)
{//gso_segs记录了网卡在传输当前skb时应该将其分割成多少个包进行return skb_shinfo(skb)->gso_segs;
}/* This is valid iff tcp_skb_pcount() > 1. */
static inline int tcp_skb_mss(const struct sk_buff *skb)
{//gso_size记录了该skb应该按照多大的段被切割,即上次的MSSreturn skb_shinfo(skb)->gso_size;
}//设置skb中的GSO信息,所谓GSO信息,就是指skb_shared_info中的
//gso_segs、gso_size、gso_type三个字段
static void tcp_set_skb_tso_segs(struct sock *sk, struct sk_buff *skb, unsigned int mss_now)
{//如果该skb数据量不足一个MSS,或者根本就不支持GSO,那么就是一个段if (skb->len <= mss_now || !sk_can_gso(sk)) {/* Avoid the costly divide in the normal non-TSO case.*///只需设置gso_segs为1,另外两个字段在这种情况下无意义skb_shinfo(skb)->gso_segs = 1;skb_shinfo(skb)->gso_size = 0;skb_shinfo(skb)->gso_type = 0;} else {//计算要切割的段数,就是skb->len除以MSS,结果向上取整skb_shinfo(skb)->gso_segs = DIV_ROUND_UP(skb->len, mss_now);skb_shinfo(skb)->gso_size = mss_now;//gso_type来自于TCB,该字段的初始化见上文skb_shinfo(skb)->gso_type = sk->sk_gso_type;}
}

5. 参考

  1. TCP数据发送之发送新数
  2. TSO维基百科解释

TCP数据发送之TSO/GSO相关推荐

  1. linux内核协议栈 TCP层数据发送之TSO/GSO

    目录 1 基本概念 2 TCP延迟分段判定 2.1 客户端初始化 2.2 服务器端初始化 2.3 sk_setup_caps() 3 整体结构 4. TCP发送路径TSO处理 4.1 tcp_send ...

  2. linux 内核 发送数据,linux 内核tcp数据发送的实现

    在分析之前先来看下SO_RCVTIMEO和SO_SNDTIMEO套接口吧,前面分析代码时没太注意这两个.这里算是个补充. SO_RCVTIMEO和SO_SNDTIMEO套接口选项可以给套接口的读和写, ...

  3. linux内核协议栈 TCP数据发送之发送窗口

    目录 1 发送窗口概述 2 snd_una 和 snd_wnd 的更新 2.1 发送窗口初始化 2.1.1 客户端初始化 2.1.2 服务器端初始化 2.2 本地接收窗口 rcv_wnd 通告 2.2 ...

  4. TCP数据发送之发送窗口

    TCP的发送过程由滑动窗口控制,而滑动窗口的大小受限于发送窗口和拥塞窗口,拥塞窗口由拥塞控制算法的代表,而发送窗口是流量控制算法的代表,这篇笔记记录了发送窗口相关的内容,包括发送窗口的初始化.更新.以 ...

  5. TCP 数据收发过程抓包分析

    本文简单对 TCP 协议的三次握手.数据传输.四次挥手过程进行抓包分析. 一. 抓包准备 首先本地通过套接字实现一个 TCP 通信,然后通过 Wireshark 抓包,套接字通信代码如下: Serve ...

  6. TCP TSO/GSO初步探索

    参考:https://blog.csdn.net/quqi99/article/details/51066800            https://www.ibm.com/developerwor ...

  7. 【iOS】Socket/TCP 通信 发送 NSString 字符串格式数据

    Socket/TCP 原理这里就不阐述了,网上一搜一大堆,直接上关键代码. [注]iOS 目前有非常著名的第三方库 CocoaAsyncSocket 可以使用,但是我们项目当时做大数据上报要求直接发送 ...

  8. 串口发送tcp数据 源端口号_三分钟基础知识:用动画给面试官解释 TCP 三次握手过程...

    作者 |  小鹿 来源 |  小鹿动画学编程 写在前边 TCP 三次握手过程对于面试是必考的一个,所以不但要掌握 TCP 整个握手的过程,其中有些小细节也更受到面试官的青睐. 对于这部分掌握以及 TC ...

  9. linux进程退出所有tcp数据才发送,深入理解Node.js 进程与线程(8000长文彻底搞懂)...

    前言 进程与线程是一个程序员的必知概念,面试经常被问及,但是一些文章内容只是讲讲理论知识,可能一些小伙伴并没有真的理解,在实际开发中应用也比较少.本篇文章除了介绍概念,通过Node.js 的角度讲解进 ...

最新文章

  1. input 正则验证(摘)
  2. Delphi获取显卡和系统各种音频设备的代码实现
  3. Mac平台上的几款串口工具
  4. 面试官: 用css实现android系统的loading动画
  5. 大话oraclerac集群、高可用性、备份与恢复_数腾Oracle RAC数据库灾备解决方案
  6. extern关键字讲解
  7. python 中BeautifulSoup入门
  8. overflow鼠标拖拽显示_[翻译] 从零开始的 .Net Shell 扩展教程 (四) - Shell 拖拽处理程序
  9. 【转】MFC中用CFile读取和写入文件2
  10. excel公式编辑器_EXCEL从文件夹中提取符合条件的记录,其实很简单
  11. IceWarp邮件监控功能
  12. puppet单机模型
  13. linux视频对话框,抖音对话框视频怎么做?如何在视频画面上添加对话气泡框?视频加对话气泡的方法...
  14. React中文文档之Components and Props
  15. 三轴机械手结构化编程5轴伺服项目
  16. ROS rviz gazebo No transform from [left_leg] to [base_link]
  17. ESPIDF开发ESP32学习笔记【基本内容】
  18. Kubernetes K8S之Pod跨namespace名称空间访问Service服务
  19. spring aop切入点汇总
  20. stm32 --如何安装J-Link驱动软件

热门文章

  1. 7的意志 (数位DP)
  2. 纷享销客罗旭对话旷视唐文斌:数字化的AI革命之路
  3. web服务器、应用服务器、web应用框架的关系
  4. 全球及中国第五代移动通信技术(5G)产业建设现状与应用发展方向分析报告2022年
  5. 转:教大家如何用HBuilder将web项目打包成apk
  6. STM8L101 与 STM8S103区别
  7. Java程序优雅关闭
  8. [720全景图]三脚架外景拍摄
  9. 一款c#实现的实用好玩儿的背单词程序
  10. 记一个简单的Android计时器制作过程。