> For the complete documentation index, see [llms.txt](https://man.bsdcn.org/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://man.bsdcn.org/man4/siftr.4.md).

# siftr(4)

`SIFTR` — 用于 TCP 研究的统计信息

## 名称

`SIFTR`

## 概要

`要在运行时以模块形式加载此驱动，请以 root 身份执行以下命令：`

```sh
kldload siftr
```

`或者，要在引导时以模块形式加载此驱动，请将以下行添加到 loader.conf(5) 文件中：`

```sh
siftr_load="YES"
```

## 描述

`SIFTR`（*S*tatistical *I*nformation *F*or *T*CP *R*esearch）内核模块将活动 TCP 连接的一系列统计信息记录到日志文件中。它提供了对 TCP 连接状态进行高度细粒度测量的能力，面向系统管理员、开发者和研究人员。

### 编译时配置

`SIFTR` 的默认操作是捕获 IPv4 TCP/IP 数据包。可通过取消注释以下行来配置 `SIFTR` 以支持 IPv4 和 IPv6：

```sh
CFLAGS+=-DSIFTR_IPV6
```

在 \<sys/modules/siftr/Makefile> 中并重新编译。

在仅 IPv4 模式（默认）下，使用标准的点分十进制表示法（如 "136.186.229.95"）格式化 IPv4 地址以进行日志记录。在 IPv6 模式下，使用标准的点分十进制表示法格式化 IPv4 地址，使用标准的冒号分隔十六进制表示法（参见 RFC 4291）格式化 IPv6 地址（如 "fd00::2"）以进行日志记录。

### 运行时配置

`SIFTR` 利用 [sysctl(8)](/man8/sysctl.8.md) 接口将其配置变量导出到用户空间。以下变量可用：

**`net.inet.siftr.enabled`** 控制模块是否执行测量。默认值为 0，表示模块不会进行任何测量。在 `net.inet.siftr.enabled` 设为 0 的情况下加载模块不会影响网络栈性能，因为数据包过滤钩子仅在 `net.inet.siftr.enabled` 设为 1 时才会插入。

**`net.inet.siftr.ppl`** 控制给定 TCP 连接的多少个入站/出站数据包会触发为该连接生成一条日志消息。默认值为 1，表示模块会为每个 TCP 连接的每个数据包记录一条消息。该值可设为 \[1,2^32] 范围内的任意整数，并且可随时更改，即使模块已启用。

**`net.inet.siftr.logfile`** 控制模块写入日志消息的文件路径。默认使用 /var/log/siftr.log 文件。该路径可随时更改，即使模块已启用。

**`net.inet.siftr.port_filter`** 控制 `SIFTR` 应捕获哪个源或目标端口。默认值为 0，表示所有端口都符合日志记录条件。设为任何其他值时，仅记录源或目标端口等于此数值的数据包。

### 日志格式

典型的 `SIFTR` 日志文件包含 3 种不同类型的日志消息。所有消息均以纯 ASCII 文本写入。

注意：本节示例日志消息中出现的 "e" 表示行续行，并非实际日志消息的一部分。

当模块启用并开始从运行中的内核收集数据时，第一种类型的日志消息会写入文件。以下文本显示了一个示例模块启用日志。字段为制表符分隔的键值对，描述了有关系统的一些基本信息。

```sh
enable_time_secs=1685191807    enable_time_usecs=160752 \
siftrver=1.3.0    sysname=FreeBSD    sysver=1400089    ipmode=4
```

字段说明如下：

**`enable_time_secs`** 模块启用的时间，以自 UNIX 纪元以来的秒数表示。

**`enable_time_usecs`** 模块启用的时间，以自 enable\_time\_secs 起的微秒数表示。

**`siftrver`** `SIFTR` 的版本。

**`sysname`** 操作系统名称。

**`sysver`** 操作系统版本。

**`ipmode`** 编译时定义的 IP 模式。ipmode 为 "4" 表示不支持 IPv6，IP 地址以常规点分四元组格式记录。ipmode 为 "6" 表示支持 IPv6，IP 地址以点分四元组或十六进制格式记录，如"编译时配置"小节所述。

当生成数据日志消息时，第二种类型的日志消息会写入文件。以下文本显示了一个由 IPv4 TCP/IP 数据包触发的示例数据日志。数据采用 CSV 格式。

```sh
o,1685191814.185109,10.1.1.2,32291,10.1.1.3,5001,1073725440, \
14480,2,65160,65700,7,9,4,1460,1000,1,16778209,230000,33580,0, \
65700,0,0,0,86707916,130
```

字段说明如下：

**`1`** 触发日志消息的数据包方向。"i" 表示入站，"o" 表示出站。

**`2`** 触发日志消息的数据包被 [pfil(9)](/man9/pfil.9.md) 钩子函数处理的时间，以自 UNIX 纪元以来的秒和微秒表示。

**`3`** 本地主机的 IPv4 或 IPv6 地址，采用点分四元组（IPv4 数据包）或冒号分隔十六进制（IPv6 数据包）表示法。

**`4`** 本地主机通信所用的 TCP 端口。

**`5`** 远程主机的 IPv4 或 IPv6 地址，采用点分四元组（IPv4 数据包）或冒号分隔十六进制（IPv6 数据包）表示法。

**`6`** 远程主机通信所用的 TCP 端口。

**`7`** 该流的慢启动阈值，以字节为单位。

**`8`** 该流的当前拥塞窗口，以字节为单位。

**`9`** 该流 t\_flags2 字段的当前状态。

**`10`** 该流的当前发送窗口，以字节为单位。报告缩放后的值。

**`11`** 该流的当前接收窗口，以字节为单位。始终报告缩放后的值。

**`12`** 发送窗口的当前窗口缩放因子。

**`13`** 接收窗口的当前窗口缩放因子。

**`14`** TCP 有限状态机的当前状态，定义于 <`netinet/tcp_fsm.h`>。

**`15`** 该流的最大段大小，以字节为单位。

**`16`** 该流的当前平滑 RTT 估计值，以微秒为单位。

**`17`** SACK 启用指示符。1 表示启用 SACK，0 表示未启用。

**`18`** 该流 TCP 标志的当前状态。有关各标志的信息，请参见 <`netinet/tcp_var.h`>。

**`19`** 该流的当前重传超时时长，以微秒为单位。

**`20`** 套接字发送缓冲区的当前大小（字节）。

**`21`** 套接字发送缓冲区中的当前字节数。

**`22`** 套接字接收缓冲区的当前大小（字节）。

**`23`** 套接字接收缓冲区中的当前字节数。

**`24`** 当前在途未确认的字节数。通过 SACK 确认的字节不从此计数中排除。

**`25`** 重装队列中的当前段数。

**`26`** 连接的 flowid。注意事项：零 '0' 可能表示有效的 flowid，也可能是未设置时的默认值。如果不查看实际使用的网络接口卡和驱动程序，没有简单的方法来区分。

**`27`** 连接的流类型。flowtype 定义了哪些协议字段被哈希以生成 flowid。完整列表可在 `sys/mbuf.h` 中的 `M_HASHTYPE_*` 下找到。

当模块禁用并停止从运行中的内核收集数据时，第三种类型的日志消息会写入文件。以下文本显示了一个示例模块禁用日志。字段为制表符分隔的键值对，提供自模块最近一次启用以来的操作统计信息。

```sh
disable_time_secs=1685191816    disable_time_usecs=629397 \
num_inbound_tcp_pkts=10    num_outbound_tcp_pkts=10 \
total_tcp_pkts=20    num_inbound_skipped_pkts_malloc=0 \
num_outbound_skipped_pkts_malloc=0    num_inbound_skipped_pkts_tcpcb=2 \
num_outbound_skipped_pkts_tcpcb=2    num_inbound_skipped_pkts_inpcb=0 \
num_outbound_skipped_pkts_inpcb=0    total_skipped_tcp_pkts=4 \
flow_list=10.1.1.2;32291-10.1.1.3;5001,10.1.1.2;58544-10.1.1.3;5001,
```

字段说明如下：

**`disable_time_secs`** 模块禁用的时间，以自 UNIX 纪元以来的秒数表示。

**`disable_time_usecs`** 模块禁用的时间，以自 disable\_time\_secs 起的微秒数表示。

**`num_inbound_tcp_pkts`** 向上穿过网络栈的 TCP 数据包数。仅包含 `SIFTR` 启用期间的入站 TCP 数据包。

**`num_outbound_tcp_pkts`** 向下穿过网络栈的 TCP 数据包数。仅包含 `SIFTR` 启用期间的出站 TCP 数据包。

**`total_tcp_pkts`** num\_inbound\_tcp\_pkts 和 num\_outbound\_tcp\_pkts 的总和。

**`num_inbound_skipped_pkts_malloc`** 因 malloc 调用失败而未处理的入站数据包数。

**`num_outbound_skipped_pkts_malloc`** 因 malloc 调用失败而未处理的出站数据包数。

**`num_inbound_skipped_pkts_tcpcb`** 因无法找到与数据包关联的 TCP 控制块而未处理的入站数据包数。

**`num_outbound_skipped_pkts_tcpcb`** 因无法找到与数据包关联的 TCP 控制块而未处理的出站数据包数。

**`num_inbound_skipped_pkts_inpcb`** 因无法找到与数据包关联的 IP 控制块而未处理的入站数据包数。

**`num_outbound_skipped_pkts_inpcb`** 因无法找到与数据包关联的 IP 控制块而未处理的出站数据包数。

**`total_skipped_tcp_pkts`** 所有跳过数据包计数器的总和。

**`flow_list`** 自模块加载以来触发数据日志消息生成的 TCP 流的 CSV 列表。CSV 列表中的每个流条目格式为 "local\_ip;local\_port-foreign\_ip;foreign\_port"。如果列表中没有条目（即未生成数据日志消息），该值为空白。如果列表中至少有一个条目，则始终会有一个尾随逗号。

模块启用/禁用周期内日志文件中找到的数据日志消息总数应等于 total\_tcp\_pkts - total\_skipped\_tcp\_pkts。

## 实现说明

`SIFTR` 使用 [pfil(9)](/man9/pfil.9.md) 接口挂入网络栈。在当前实现中，它挂入 AF\_INET/AF\_INET6（IPv4/IPv6）[pfil(9)](/man9/pfil.9.md) 过滤点，这意味着它在网络栈的 IP 层看到数据包。这意味着入栈的 TCP 数据包在被 TCP 层处理之前就被拦截。出栈的数据包在被 TCP 层处理之后被拦截。

下图说明了 `SIFTR` 如何将自身插入到栈中。

```sh
----------------------------------
           上层
----------------------------------
    ^                       |
    |                       |
    |                       |
    |                       v
 TCP 入                 TCP 出
----------------------------------
    ^                      |
    |________     _________|
            |     |
            |     v
           ---------
           | SIFTR |
           ---------
            ^     |
    ________|     |__________
    |                       |
    |                       v
IPv{4/6} 入            IPv{4/6} 出
----------------------------------
    ^                       |
    |                       |
    |                       v
二层 入                二层 出
----------------------------------
          物理层
----------------------------------
```

`SIFTR` 使用 [alq(9)](/man9/alq.9.md) 接口来管理将数据写入磁盘。

乍一看，你可能误以为 `SIFTR` 从单个 TCP 数据包中提取信息。事实并非如此。`SIFTR` 使用系统发起的每个 TCP 流的 TCP 数据包事件（入站和出站）来触发该流 TCP 控制块状态的转储。当 PPL 设为 1 时，我们实际上是在以流数据包进入/离开系统的频率对每个 TCP 流的控制块状态进行采样。例如，将 PPL 设为 2 会将采样率减半，即每第二个流数据包（入站或出站）触发一次控制块状态转储。

区分查询单个数据包与查询控制块很重要，因为 `SIFTR` 并未消除对 [tcpdump(1)](/man1/tcpdump.1.md) 等数据包捕获工具的需求。`SIFTR` 允许你将在线上看到的内容（使用 [tcpdump(1)](/man1/tcpdump.1.md) 等工具捕获）与所关注流的 TCP 控制块中的变化之间的因果关系进行关联和观察。因此，同时使用 `SIFTR` 和 [tcpdump(1)](/man1/tcpdump.1.md) 等工具来收集必要的数据以拼凑出完整图景是很有用的。单独使用任一工具都无法提供所有必要的数据。

由于需要查询 TCP 控制块，连接生命周期中的某些数据包无法触发 `SIFTR` 日志消息。初始握手在控制块不存在或控制块尚未完全初始化时发生，而最终的 ACK 在连接处于 TIMEWAIT 状态时交换。

`SIFTR` 旨在最大限度地减少对穿过网络栈的数据包引入的延迟。这种设计要求一个高度优化且最简的钩子函数，在挂起数据包时提取所需的最少细节，并将这些细节传递给另一个线程进行实际处理和日志记录。

这种多线程设计在访问操作线程间共享的数据结构时确实会引入一些争用问题。当钩子函数尝试将细节放入结构体时，它必须首先获取独占锁。同样，当处理线程尝试从结构体中读取细节时，它也必须获取独占锁。如果一个线程持有锁，另一个线程必须等待才能获取它。这确实会在内核的数据包处理代码路径中引入一些额外的有界延迟。

在某些情况下（如内存不足、连接终止），进入 `SIFTR` [pfil(9)](/man9/pfil.9.md) 钩子函数的 TCP 数据包不会触发日志消息的生成。`SIFTR` 将此结果称为"跳过的数据包"。注意，`SIFTR` 始终确保数据包能够继续穿过栈，即使它们无法成功触发数据日志消息。因此，`SIFTR` 不会为穿过网络栈的 TCP/IP 数据包引入任何丢包。

### 重要行为

模块启用时日志文件路径更改的行为如下：

* 将所有待处理日志消息刷新到旧文件路径。
* 关闭旧文件路径。
* 将活动日志文件指针切换到新文件路径。
* 开始记录到新文件。
* 尝试打开新文件路径以进行写入。如果失败，路径更改将失败，现有路径将继续使用。
* 假设新路径有效并成功打开：

在将待处理日志消息刷新到旧文件和开始记录到新文件之间的时间内，新的日志消息仍会生成并缓冲。一旦新文件路径准备好写入，积累的日志消息将被写入文件。

## 实例

要启用模块操作，请以 root 身份执行以下命令：sysctl net.inet.siftr.enabled=1

要将日志消息的粒度更改为每 10 个 TCP 数据包（每个连接）生成 1 条日志消息，请以 root 身份执行以下命令：sysctl net.inet.siftr.ppl=10

要将日志文件位置更改为 /tmp/siftr.log，请以 root 身份执行以下命令：sysctl net.inet.siftr.logfile=/tmp/siftr.log

## 参见

[tcpdump(1)](/man1/tcpdump.1.md), [tcp(4)](/man4/tcp.4.md), [sysctl(8)](/man8/sysctl.8.md), [alq(9)](/man9/alq.9.md), [pfil(9)](/man9/pfil.9.md)

## 致谢

本软件的开发部分得益于 Cisco University Research Program Fund at Community Foundation Silicon Valley 以及 FreeBSD Foundation 的资助。

## 历史

`SIFTR` 最早出现于 FreeBSD 7.4 和 FreeBSD 8.2。

`SIFTR` 于 2007 年由 Lawrence Stewart 和 James Healy 首次发布，当时他们在澳大利亚墨尔本斯威本科技大学高级互联网架构中心从事 NewTCP 研究项目，该项目部分得益于 Cisco University Research Program Fund at Community Foundation Silicon Valley 的资助。更多详情请参见：

<http://caia.swin.edu.au/urp/newtcp/>

`SIFTR` v1.2.x 的工作由 FreeBSD Foundation 作为"Enhancing the FreeBSD TCP Implementation"项目（2008-2009）的一部分赞助。更多详情请参见：

<https://www.freebsdfoundation.org/>

<http://caia.swin.edu.au/freebsd/etcp09/>

## 作者

`SIFTR` 由 Lawrence Stewart <lstewart@FreeBSD.org> 和 James Healy <jimmy@deefa.com> 编写。

本手册页由 Lawrence Stewart <lstewart@FreeBSD.org> 编写。

## 缺陷

当前已知的限制及任何相关变通方法如下：

* 用于在操作线程间传递信息的内部队列目前是无界的。这使 `SIFTR` 能够应对突发网络流量，但如果处理线程跟不上数据包速率，持续的高每秒数据包流量可能导致内核内存耗尽。
* 如果在使用 `SIFTR` 的机器上同时运行使用 [pfil(9)](/man9/pfil.9.md) 框架的其他模块（如 [dummynet(4)](/man4/dummynet.4.md)、[ipfw(8)](/man8/ipfw.8.md)、[pf(4)](/man4/pf.4.md)），加载模块的顺序很重要。应先 kldload 其他模块，以确保 TCP 数据包在 `SIFTR` "看到"并处理它们之前经过任何必要的操作。
* 在使用 [witness(4)](/man4/witness.4.md) 支持编译的内核中启用 `SIFTR` 时，[witness(4)](/man4/witness.4.md) 会报告 [pfil(9)](/man9/pfil.9.md) 互斥锁与 tcbinfo TCP 锁之间存在已知的无害锁顺序反转警告。
* 无法过滤你希望捕获数据的 TCP 流。需要后处理来分离属于特定感兴趣流的数据。
* 模块不会检测日志文件路径的删除。如果在模块设置为使用文件时删除了 `SIFTR` 使用的日志文件，新的日志消息将直接丢失。使用 *net.inet.siftr.logfile* 变量切换到新的日志文件会创建新文件，并允许日志消息再次开始写入磁盘。新的日志文件路径必须与已删除文件的路径不同。
* 代码中使用的哈希表大小为可容纳 65536 个流。这不是硬性限制，因为链式法用于处理哈希表结构中的冲突。然而，我们怀疑（基于与其他哈希表性能数据的类比），随着模块启用/禁用周期内处理的唯一流数接近并超过 65536，哈希表查找性能（因此模块的数据包处理性能）将以指数方式下降。
* 流哈希表不执行垃圾回收。目前刷新它的唯一方法是禁用 `SIFTR`。
* PPL 变量适用于进入处理线程的数据包，而非钩子函数中接收的数据包总数。数据包在应用 PPL 变量之前被跳过，这意味着日志消息的触发可能存在轻微差异。例如，如果 PPL 设为 10，并且自上一条日志消息以来的第 8 个数据包被跳过，则第 11 个数据包将实际触发日志消息的生成。这在 CAIA 技术报告 070824A 中有更深入的讨论。
* 在撰写本文时，尚无简单的方法挂入 TCP 层来拦截数据包。使用 IP 层钩子点意味着所有 IP 流量都将由 [pfil(9)](/man9/pfil.9.md) 钩子函数处理，这会对非 TCP 数据包也引入轻微但不必要的延迟和处理开销。从数据收集的角度来看，在 IP 层挂钩也不理想。向上穿过栈的数据包将在被 TCP 层处理之前被拦截并触发日志消息生成，这意味着我们无法尽可能精确地观察入站事件与相应 TCP 控制块之间的因果关系。理想情况下，应在数据包被 TCP 层处理后拦截它们，即在 `tcp_input` 处理后拦截向上穿越栈的数据包，在 `tcp_output` 处理后拦截向下穿越栈的数据包。不过，当前代码仍给出了令人满意的粒度，因为入站事件往往会触发出站事件，允许通过同样捕获出站事件的状态来间接观察因果关系。
* `SIFTR` 记录的"inflight bytes"值未考虑接收主机已 SACK 的字节数。
