struct sk_buff

sk_buff 是表示数据包的主要网络结构。

基本sk_buff几何结构

struct sk_buff 本身是一个元数据结构,不包含任何数据包数据。 所有数据都保存在关联的缓冲区中。

sk_buff.head 指向主“头”缓冲区。 头缓冲区分为两部分

  • 数据缓冲区,包含标头,有时还包含有效负载; 这是 skb 中由常用辅助函数(例如 skb_put()skb_pull())操作的部分;

  • 共享信息 (struct skb_shared_info),其中包含指向 (page, offset, length) 格式的只读数据的指针数组。

可选地,skb_shared_info.frag_list 可能会指向另一个 skb。

基本图可能如下所示

                                ---------------
                               | sk_buff       |
                                ---------------
   ,---------------------------  + head
  /          ,-----------------  + data
 /          /      ,-----------  + tail
|          |      |            , + end
|          |      |           |
v          v      v           v
 -----------------------------------------------
| headroom | data |  tailroom | skb_shared_info |
 -----------------------------------------------
                               + [page frag]
                               + [page frag]
                               + [page frag]
                               + [page frag]       ---------
                               + frag_list    --> | sk_buff |
                                                   ---------

共享skbs和skb克隆

sk_buff.users 是一个简单的引用计数,允许多个实体保持 struct sk_buff 处于活动状态。 sk_buff.users != 1 的 skb 被称为共享 skb(参见 skb_shared())。

skb_clone() 允许快速复制 skb。 没有复制任何数据缓冲区,但调用者会获得一个新的元数据结构 (struct sk_buff)。 &skb_shared_info.refcount 指示指向同一数据包数据的 skb 数量(即,克隆)。

dataref和无标头的skbs

传输层发送出它们持有的有效负载 skb 的克隆,以进行重传。 为了允许堆栈的较低层添加它们的标头,我们将 skb_shared_info.dataref 分成两半。 较低的 16 位计数总的引用次数。 较高的 16 位指示有多少引用是仅有效负载的。 skb_header_cloned() 检查是否允许 skb 添加/写入标头。

skb 的创建者(例如,TCP)将其 skb 标记为 sk_buff.nohdr (通过 __skb_header_release())。 从标记的 skb 创建的任何克隆都将使用可用的预留空间填充 sk_buff.hdr_len。 如果只存在一个克隆,则它能够随意修改预留空间。 传输层中的调用顺序是

<alloc skb>
skb_reserve()
__skb_header_release()
skb_clone()
// send the clone down the stack

这不是一个非常通用的结构,它取决于传输层是否做正确的事情。 实际上,通常只有一个仅有效负载的 skb。 拥有具有不同 hdr_len 长度的多个仅有效负载的 skb 是不可能的。 仅有效负载的 skb 绝不应该离开它们的拥有者。

校验和信息

堆栈和网络驱动程序之间的校验和卸载的接口如下...

设备接收的数据包的校验和

校验和验证的指示在 sk_buff.ip_summed 中设置。 可能的值是

  • CHECKSUM_NONE

    设备没有校验此数据包的校验和,例如,由于缺乏能力。 数据包包含完整(但未验证)的校验和,但 skb->csum 中没有。 因此,在这种情况下,skb->csum 是未定义的。

  • CHECKSUM_UNNECESSARY

    您正在处理的硬件不计算完整的校验和(如 CHECKSUM_COMPLETE 中),但它会解析标头并验证特定协议的校验和。 对于此类数据包,如果它们的校验和没问题,则它将设置 CHECKSUM_UNNECESSARY。 在这种情况下,sk_buff.csum 仍然是未定义的。 驱动程序或设备绝不能修改数据包中的校验和字段,即使校验和已验证。

    CHECKSUM_UNNECESSARY 适用于以下协议

    • TCP:IPv6 和 IPv4。

    • UDP:IPv4 和 IPv6。 设备可以将 CHECKSUM_UNNECESSARY 应用于 IPv4 或 IPv6 的零 UDP 校验和,在这种情况下,网络堆栈可能会执行进一步的验证。

    • GRE:仅当标头中存在校验和时。

    • SCTP:指示 SCTP 标头中的 CRC 已验证。

    • FCOE:指示 FC 帧中的 CRC 已验证。

    sk_buff.csum_level 指示在数据包中找到的连续校验和的数量(减 1),这些校验和已验证为 CHECKSUM_UNNECESSARY。 例如,如果设备接收到 IPv6->UDP->GRE->IPv4->TCP 数据包,并且设备能够验证 UDP(可能为零)、GRE(设置了校验和标志)和 TCP 的校验和,则 sk_buff.csum_level 将设置为 2。 如果设备只能验证 UDP 校验和,而不能验证 GRE,可能是因为它不支持 GRE 校验和或因为 GRE 校验和错误,则 skb->csum_level 将设置为 0(在这种情况下不考虑 TCP 校验和)。

  • CHECKSUM_COMPLETE

    这是最通用的方法。 设备提供 netif_rx() 看到的 _整个_ 数据包的校验和,并填写 sk_buff.csum。 这意味着硬件不需要解析 L3/L4 标头来实现此目的。

    注意事项

    • 即使设备仅支持某些协议,但能够生成 skb->csum,它也必须使用 CHECKSUM_COMPLETE,而不是 CHECKSUM_UNNECESSARY。

    • CHECKSUM_COMPLETE 不适用于 SCTP 和 FCoE 协议。

  • CHECKSUM_PARTIAL

    设置校验和以卸载到设备,如 CHECKSUM_PARTIAL 的输出描述中所述。 这可能发生在直接从另一个 Linux OS 接收的数据包上,例如,同一主机上的虚拟化 Linux 内核,或者它可能在 GRO 或远程校验和卸载的输入路径中设置。 为了校验和验证的目的,skb->csum_start + skb->csum_offset 引用的校验和以及数据包中任何前面的校验和都被认为是已验证的。 数据包中位于要卸载的校验和之后的任何校验和都不被认为是已验证的。

非GSO的传输校验和

堆栈在数据包的 sk_buff.ip_summed 中请求校验和卸载。 值是

  • CHECKSUM_PARTIAL

    驱动程序需要校验 hard_start_xmit() 从 sk_buff.csum_start 到结尾看到的数据包的校验和,并在偏移量 sk_buff.csum_start + sk_buff.csum_offset 处记录/写入校验和。 驱动程序可能会验证 csum_start 和 csum_offset 值是否是给定数据包的长度和偏移量的有效值,但它不应尝试验证校验和是否引用合法的传输层校验和 - 验证 csum_start 和 csum_offset 是否正确设置是堆栈的职责。

    当堆栈请求数据包的校验和卸载时,驱动程序必须确保正确设置校验和。 驱动程序可以将校验和计算卸载到设备,或者调用 skb_checksum_help(如果设备不支持特定校验和的卸载)。

    NETIF_F_IP_CSUMNETIF_F_IPV6_CSUM 正被弃用,取而代之的是 NETIF_F_HW_CSUM。 新设备应使用 NETIF_F_HW_CSUM 来指示校验和卸载能力。 可以调用 skb_csum_hwoffload_help() 以基于网络设备校验和能力解析 CHECKSUM_PARTIAL:如果数据包不匹配它们,则调用 skb_checksum_help() 或 skb_crc32c_help()(取决于 sk_buff.csum_not_inet 的值,请参见 非IP校验和 (CRC) 卸载)来解析校验和。

  • CHECKSUM_NONE

    skb 已被协议校验和,或者不需要校验和。

  • CHECKSUM_UNNECESSARY

    这与输出上的校验和卸载的 CHECKSUM_NONE 含义相同。

  • CHECKSUM_COMPLETE

    未用于校验和输出。 如果驱动程序观察到 skbuff 中设置了此值的數據包,则应将数据包视为设置了 CHECKSUM_NONE

非IP校验和(CRC)卸载

NETIF_F_SCTP_CRC

此功能指示设备能够卸载数据包中的 SCTP CRC。 为了执行此卸载,堆栈将相应地设置 csum_start 和 csum_offset,将 ip_summed 设置为 CHECKSUM_PARTIAL,并将 csum_not_inet 设置为 1,以在 skbuff 中指示 CHECKSUM_PARTIAL 引用 CRC32c。 支持 IP 校验和卸载和 SCTP CRC32c 卸载的驱动程序必须通过测试 sk_buff.csum_not_inet 的值来验证为数据包配置了哪个卸载; 提供 skb_crc32c_csum_help() 来解析 csum_not_inet 设置为 1 的 skb 上的 CHECKSUM_PARTIAL

NETIF_F_FCOE_CRC

此功能指示设备能够卸载数据包中的 FCOE CRC。 为了执行此卸载,堆栈会将 ip_summed 设置为 CHECKSUM_PARTIAL,并相应地设置 csum_start 和 csum_offset。 请注意,skbuff 中没有指示 CHECKSUM_PARTIAL 引用 FCOE 校验和,因此支持 IP 校验和卸载和 FCOE CRC 卸载的驱动程序必须通过检查数据包标头来验证为数据包配置了哪个卸载。

带有GSO的输出校验和

在 GSO 数据包的情况下(skb_is_gso() 为真),校验和卸载由 gso_type 中的 SKB_GSO_* 标志暗示。 最明显的是,如果 gso_type 是 SKB_GSO_TCPV4SKB_GSO_TCPV6,则意味着作为 GSO 操作一部分的 TCP 校验和卸载。 如果正在使用 GSO 卸载校验和,则 ip_summed 是 CHECKSUM_PARTIAL,并且 csum_start 和 csum_offset 都设置为引用要卸载的最外层校验和(使用 UDP 封装可以卸载两个校验和)。