本章将讨论 L3 的数据包发送流程。发送是指数据包从本地主机发往其他主机;它既可以由传输层(L4)发起,也可以作为转发流程的最后一步被调用。

如第 18 章图 18‑1 所示,负责投递数据包的核心函数是 dst_output;本章所介绍的函数均在它之前执行,为其做好数据包的准备工作。

内核在此阶段的任务包括:

  • 查找下一跳

    • 网络层(IP 层)需要知道出站设备以及用于下一跳的下一台路由器。路由信息通过在三层(L3)或四层(L4)调用的 ip_route_output_flow 函数获取。
  • 初始化 IP 头部

    • 在此阶段会填充 packet ID 等多个字段。如果该数据包是转发而来的,此前已经对头部做过少量处理(例如更新 TTL、校验和与选项字段)。但要使数据包能够发送,此时还需要完成更多工作。
  • 处理选项

    • 软件必须遵从那些需要在头部中添加地址或时间戳的选项要求。
  • 分片 Fragmentation

    • 如果 IP 数据包尺寸过大,无法在出站设备上进行传输,则必须对其进行分片(除非明确禁止分片)。
  • 校验和 checksum

    • 必须在完成 IP 头部的所有其他处理之后,再计算 IP 校验和。
    • 我们将会看到,网络层(L3)除了处理自身校验和之外,还可能负责处理传输层(L4)校验和。
    • 在这两种情况下,校验和既可以一次性计算完成,也可以增量计算。
    • 虽然校验和是必需的,但三层协议栈并非总是需要自行计算,因为部分设备的硬件可以完成这项工作(由 CHECKSUM_HW 标志标识)。
  • 与 Netfilter 进行检查

    • 如第 18 章的图 18‑1 所示,Linux 防火墙系统(Netfilter)有机会在数据包处理的各个阶段(包括传输阶段)对每个数据包进行丢弃或修改操作。
  • 更新统计数据

    • 根据传输结果(成功或失败)以及分片等操作,必须更新对应的 SNMP 计数器。

过去,内核中有两个不同的传输函数:一个用于无需分片、也无需处理 IP 选项、可以快速发送的数据包;另一个则提供完整的服务功能。如今内核不再显式区分这两种情况。

21.1 执行传输的关键函数

图 21‑1 展示了位于传输层(L4)发送与网络层(L3)最后一步之间的主要函数,L3 的最后一步会调用第 27 章介绍的邻居子系统函数。

按照调用它们的传输层(L4)协议进行了分类:

  • 左侧列出的原始 IP 及 UDP 和 ICMP 协议将所有分片工作都交由 IP 层完成

  • 右侧的传输层协议(TCP 与流控制传输协议 SCTP)会为分片做大量准备工作,从而减轻 IP 层的负担。

本章所描述的三层函数在完成工作后,会将数据包传递给 dst_output。

  • 对于原始 IP(raw IP)来说,如果使用了 IP_HDRINCL 选项,它将完全负责构造 IP 头部,因此会绕过本章介绍的函数,直接调用 dst_output。更多细节参见「原始套接字」一节。

  • 互联网组管理协议(IGMP)同样会直接调用 dst_output(自行完成 IP 头部初始化之后)。

这两组函数对分片的处理方式如下:

ip_queue_xmit

  • 传输层协议已经依据第 18 章所述的路径 MTU(PMTU),将数据划分成大小合适的分片(如需分片)。IP 层的工作仅需为已生成的数据分片添加 IP 首部即可。

ip_push_pending_frames 及相关函数

  • 调用该函数的传输层协议不会考虑分片,也不会协助完成分片操作。
  • 为提升效率,传输层协议可通过多次调用 ip_append_data 缓存多个发送请求,而不实际执行传输。
  • ip_append_data 并不只是简单地缓存发送请求,还会透明地生成最优大小的数据分片,以便后续 IP 层更轻松地处理分片。这避免了 IP 层在生成分片时将数据从一个缓冲区拷贝到另一个缓冲区,从而带来显著的性能提升。
  • 当传输层(L4)协议需要刷新由 ip_append_data 创建的输出队列时,会调用 ip_push_pending_frames。该函数会完成所有必要的分片操作,并将生成的数据包向下传递给 dst_output。
  • UDP 目前使用了 ip_append_data 的一个变体,名为 ip_append_page。

在特定场景下的传输过程中,还会使用到其他函数:

ip_build_and_send_pkt

  • 由 TCP 用于发送 SYN ACK 报文。

ip_send_reply

  • 由 TCP 用于发送 ACK 和 RST 报文。

图 21‑1 的分类仅涵盖最常见的情况:由于 ip_send_reply 内部使用了 ip_append_data 和 ip_push_pending_frame,这说明 TCP 并非只使用 ip_queue_xmit。

21.1.1 多播流量

21.1.2 本地流量相关的套接字数据结构

21.1.3 ip_queue_xmit 函数

21.1.5 ip_addpend_page 函数

21.1.6 ip_push_pending_frames 函数