时间戳

1. 控制接口

用于接收网络包时间戳的接口包括

SO_TIMESTAMP

为每个传入数据包生成一个时间戳,该时间戳以(不一定是单调的)系统时间表示。通过 recvmsg() 在控制消息中以微秒分辨率报告时间戳。SO_TIMESTAMP 根据架构类型和 libc 的 time_t 表示定义为 SO_TIMESTAMP_NEW 或 SO_TIMESTAMP_OLD。对于 SO_TIMESTAMP_OLD,控制消息格式为 struct __kernel_old_timeval,对于 SO_TIMESTAMP_NEW 选项,控制消息格式为 struct __kernel_sock_timeval。

SO_TIMESTAMPNS

与 SO_TIMESTAMP 相同的时间戳机制,但以 struct timespec 形式,以纳秒分辨率报告时间戳。SO_TIMESTAMPNS 根据架构类型和 libc 的 time_t 表示定义为 SO_TIMESTAMPNS_NEW 或 SO_TIMESTAMPNS_OLD。对于 SO_TIMESTAMPNS_OLD,控制消息格式为 struct timespec,对于 SO_TIMESTAMPNS_NEW 选项,控制消息格式为 struct __kernel_timespec。

IP_MULTICAST_LOOP + SO_TIMESTAMP[NS]

仅用于多播:通过读取环回数据包接收时间戳获得的近似传输时间戳。

SO_TIMESTAMPING

在接收、传输或两者上生成时间戳。支持多个时间戳源,包括硬件。支持为流套接字生成时间戳。

1.1 SO_TIMESTAMP(也包括 SO_TIMESTAMP_OLD 和 SO_TIMESTAMP_NEW)

此套接字选项启用数据报在接收路径上的时间戳。由于目标套接字(如果有)在网络堆栈中无法提前知晓,因此必须为所有数据包启用此功能。所有早期接收时间戳选项也是如此。

有关接口详细信息,请参阅 man 7 socket

始终使用 SO_TIMESTAMP_NEW 时间戳,以始终获取 struct __kernel_sock_timeval 格式的时间戳。

SO_TIMESTAMP_OLD 在 32 位机器上 2038 年后返回不正确的时间戳。

1.2 SO_TIMESTAMPNS(也包括 SO_TIMESTAMPNS_OLD 和 SO_TIMESTAMPNS_NEW)

此选项与 SO_TIMESTAMP 相同,只是返回的数据类型不同。其 struct timespec 允许比 SO_TIMESTAMP (ms) 的 timeval 更高的分辨率 (ns) 时间戳。

始终使用 SO_TIMESTAMPNS_NEW 时间戳,以始终获取 struct __kernel_timespec 格式的时间戳。

SO_TIMESTAMPNS_OLD 在 32 位机器上 2038 年后返回不正确的时间戳。

1.3 SO_TIMESTAMPING(也包括 SO_TIMESTAMPING_OLD 和 SO_TIMESTAMPING_NEW)

支持多种类型的时间戳请求。因此,此套接字选项采用标志位图,而不是布尔值。在

err = setsockopt(fd, SOL_SOCKET, SO_TIMESTAMPING, &val, sizeof(val));

val 是一个整数,其中设置了以下任何位。设置其他位将返回 EINVAL,并且不会更改当前状态。

套接字选项配置单个 sk_buffs (1.3.1) 的时间戳生成、到套接字错误队列的时间戳报告 (1.3.2) 和选项 (1.3.3)。还可以使用 cmsg (1.3.4) 为单个 sendmsg 调用启用时间戳生成。

1.3.1 时间戳生成

某些位是堆栈尝试生成时间戳的请求。它们的任何组合都是有效的。对这些位的更改适用于新创建的数据包,而不适用于堆栈中已有的数据包。因此,可以通过在两个 setsockopt 调用中嵌入一个 send() 调用,一个用于启用时间戳生成,另一个用于禁用时间戳生成,从而有选择地请求数据包子集的时间戳(例如,用于采样)。时间戳的生成也可能是出于特定套接字请求之外的原因,例如,如前所述,系统范围启用了接收时间戳时。

SOF_TIMESTAMPING_RX_HARDWARE

请求由网络适配器生成的 rx 时间戳。

SOF_TIMESTAMPING_RX_SOFTWARE

请求数据进入内核时生成的 rx 时间戳。这些时间戳是在设备驱动程序将数据包交给内核接收堆栈后立即生成的。

SOF_TIMESTAMPING_TX_HARDWARE

请求由网络适配器生成的 tx 时间戳。可以通过套接字选项和控制消息启用此标志。

SOF_TIMESTAMPING_TX_SOFTWARE

请求数据离开内核时生成的 tx 时间戳。这些时间戳是在设备驱动程序中尽可能接近网络接口的情况下生成的,但始终在其之前。因此,它们需要驱动程序支持,并且可能并非适用于所有设备。可以通过套接字选项和控制消息启用此标志。

SOF_TIMESTAMPING_TX_SCHED

请求在进入数据包调度程序之前生成的 tx 时间戳。内核传输延迟(如果时间很长)通常由排队延迟决定。此时间戳与在 SOF_TIMESTAMPING_TX_SOFTWARE 处获取的时间戳之间的差异将显示此延迟,而与协议处理无关。协议处理中产生的任何延迟都可以通过从此时间戳中减去在 send() 之前立即获取的用户空间时间戳来计算。在具有虚拟设备的机器上,传输的数据包会通过多个设备,从而通过多个数据包调度程序,每个层都会生成一个时间戳。这允许对排队延迟进行细粒度测量。可以通过套接字选项和控制消息启用此标志。

SOF_TIMESTAMPING_TX_ACK

请求发送缓冲区中的所有数据都已确认时生成的 tx 时间戳。这仅对可靠协议有意义。目前仅针对 TCP 实施。对于该协议,它可能会过度报告测量值,因为时间戳是在确认 send() 时的缓冲区及其之前的所有数据时生成的:累计确认。该机制忽略 SACK 和 FACK。可以通过套接字选项和控制消息启用此标志。

SOF_TIMESTAMPING_TX_COMPLETION

请求数据包 tx 完成时生成的 tx 时间戳。完成时间戳由内核在收到来自硬件的数据包完成报告时生成。硬件可能会一次报告多个数据包,并且完成时间戳反映报告的计时,而不是实际的 tx 时间。可以通过套接字选项和控制消息启用此标志。

1.3.2 时间戳报告

其他三个位控制将在生成的控制消息中报告哪些时间戳。对这些位的更改会在堆栈中的时间戳报告位置立即生效。时间戳仅针对也设置了相关时间戳生成请求的数据包报告。

SOF_TIMESTAMPING_SOFTWARE

在可用时报告任何软件时间戳。

SOF_TIMESTAMPING_SYS_HARDWARE

此选项已弃用且被忽略。

SOF_TIMESTAMPING_RAW_HARDWARE

在可用时,报告由 SOF_TIMESTAMPING_TX_HARDWARE 或 SOF_TIMESTAMPING_RX_HARDWARE 生成的硬件时间戳。

1.3.3 时间戳选项

该接口支持以下选项

SOF_TIMESTAMPING_OPT_ID

为每个数据包生成一个唯一标识符。一个进程可以有多个并发的待处理时间戳请求。数据包可以在传输路径中重新排序,例如在数据包调度程序中。在这种情况下,时间戳将以与原始 send() 调用不同的顺序排队到错误队列中。并非总是可以仅基于时间戳顺序或有效负载检查将时间戳唯一地匹配到原始 send() 调用。

此选项将 send() 处的每个数据包与唯一标识符相关联,并将其与时间戳一起返回。标识符是从每个套接字的 u32 计数器(循环)派生的。对于数据报套接字,计数器随着每个发送的数据包而递增。对于流套接字,它随着每个字节而递增。对于流套接字,还请设置 SOF_TIMESTAMPING_OPT_ID_TCP,请参阅下面的部分。

计数器从零开始。它在首次启用套接字选项时初始化。每次在禁用该选项后启用该选项时,都会重置它。重置计数器不会更改系统中现有数据包的标识符。

此选项仅针对传输时间戳实施。在那里,时间戳始终与 struct sock_extended_err 一起循环。该选项修改字段 ee_data 以传递一个 ID,该 ID 在该套接字的所有可能并发的待处理时间戳请求中是唯一的。

进程可以选择通过控制消息 SCM_TS_OPT_ID(TCP 套接字不支持)传递特定 ID 来覆盖默认生成的 ID。

struct msghdr *msg;
...
cmsg                         = CMSG_FIRSTHDR(msg);
cmsg->cmsg_level             = SOL_SOCKET;
cmsg->cmsg_type              = SCM_TS_OPT_ID;
cmsg->cmsg_len               = CMSG_LEN(sizeof(__u32));
*((__u32 *) CMSG_DATA(cmsg)) = opt_id;
err = sendmsg(fd, msg, 0);
SOF_TIMESTAMPING_OPT_ID_TCP

将此修饰符与新的 TCP 时间戳应用程序的 SOF_TIMESTAMPING_OPT_ID 一起传递。SOF_TIMESTAMPING_OPT_ID 定义了流套接字的计数器如何递增,但其起点并非完全微不足道。此选项修复了该问题。

对于流套接字,如果设置了 SOF_TIMESTAMPING_OPT_ID,则应始终设置此项。在数据报套接字上,该选项不起作用。

一个合理的预期是,计数器通过系统调用重置为零,因此后续 N 字节的 write() 生成一个时间戳,计数器为 N-1。SOF_TIMESTAMPING_OPT_ID_TCP 在所有条件下都实施此行为。

SOF_TIMESTAMPING_OPT_ID 在没有修饰符的情况下通常会报告相同的内容,尤其是在没有数据传输时设置套接字选项时。如果正在传输数据,则可能会因输出队列 (SIOCOUTQ) 的长度而有所偏差。

差异是由于基于 snd_una 而不是 write_seq。snd_una 是对等方确认的流中的偏移量。这取决于进程控制之外的因素,例如网络 RTT。write_seq 是进程写入的最后一个字节。此偏移量不受外部输入的影响。

当在初始套接字创建时配置时,这种差异是微妙的,不太可能被注意到,此时没有数据排队或发送。但 SOF_TIMESTAMPING_OPT_ID_TCP 行为无论何时设置套接字选项都更强大。

SOF_TIMESTAMPING_OPT_CMSG

支持 recv() cmsg 以用于所有带有时间戳的数据包。控制消息已无条件地在所有具有接收时间戳的数据包和具有传输时间戳的 IPv6 数据包上支持。此选项将它们扩展到具有传输时间戳的 IPv4 数据包。一个用例是通过同时启用套接字选项 IP_PKTINFO,将数据包与其出口设备相关联。

SOF_TIMESTAMPING_OPT_TSONLY

仅适用于传输时间戳。使内核将时间戳作为 cmsg 与空数据包一起返回,而不是与原始数据包一起返回。这减少了向套接字的接收预算 (SO_RCVBUF) 收取的内存量,并且即使 sysctl net.core.tstamp_allow_data 为 0,也会传递时间戳。此选项禁用 SOF_TIMESTAMPING_OPT_CMSG。

SOF_TIMESTAMPING_OPT_STATS

与传输时间戳一起获取的可选统计信息。它必须与 SOF_TIMESTAMPING_OPT_TSONLY 一起使用。当传输时间戳可用时,统计信息可在单独的 SCM_TIMESTAMPING_OPT_STATS 类型的控制消息中获得,作为 TLV(struct nlattr)类型的列表。这些统计信息允许应用程序将各种传输层统计信息与传输时间戳相关联,例如,某个数据块被对等方接收窗口限制的时间。

SOF_TIMESTAMPING_OPT_PKTINFO

为带有硬件时间戳的传入数据包启用 SCM_TIMESTAMPING_PKTINFO 控制消息。该消息包含 struct scm_ts_pktinfo,它提供接收数据包的真实接口的索引及其在第 2 层的长度。仅当启用 CONFIG_NET_RX_BUSY_POLL 且驱动程序使用 NAPI 时,才会返回有效的(非零)接口索引。该结构还包含其他两个字段,但它们是保留的且未定义。

SOF_TIMESTAMPING_OPT_TX_SWHW

当 SOF_TIMESTAMPING_TX_HARDWARE 和 SOF_TIMESTAMPING_TX_SOFTWARE 同时启用时,请求传出数据包的硬件和软件时间戳。如果生成了两个时间戳,则会将两个单独的消息循环到套接字的错误队列,每个消息仅包含一个时间戳。

SOF_TIMESTAMPING_OPT_RX_FILTER

过滤掉虚假的接收时间戳:仅当启用了匹配的时间戳生成标志时,才报告接收时间戳。

接收时间戳在入口路径的早期生成,在知晓数据包的目标套接字之前。如果任何套接字启用了接收时间戳,则所有套接字的数据包都将收到带有时间戳的数据包。包括那些使用 SOF_TIMESTAMPING_SOFTWARE 和/或 SOF_TIMESTAMPING_RAW_HARDWARE 请求时间戳报告,但不请求接收时间戳生成的套接字。当仅请求传输时间戳时,可能会发生这种情况。

接收虚假时间戳通常是良性的。进程可以忽略意外的非零值。但这使得行为微妙地依赖于其他套接字。此标志隔离套接字以获得更具确定性的行为。

鼓励新应用程序传递 SOF_TIMESTAMPING_OPT_ID 以消除时间戳的歧义,并传递 SOF_TIMESTAMPING_OPT_TSONLY 以便与 sysctl net.core.tstamp_allow_data 的设置无关地运行。

例外情况是当进程需要额外的 cmsg 数据时,例如 SOL_IP/IP_PKTINFO 以检测出口网络接口。然后传递选项 SOF_TIMESTAMPING_OPT_CMSG。此选项取决于对原始数据包内容的访问权限,因此无法与 SOF_TIMESTAMPING_OPT_TSONLY 组合使用。

1.3.4. 通过控制消息启用时间戳

除了套接字选项外,还可以通过 cmsg 为每次写入请求时间戳生成,仅适用于 SOF_TIMESTAMPING_TX_*(请参阅第 1.3.1 节)。使用此功能,应用程序可以按 sendmsg() 对时间戳进行采样,而无需支付通过 setsockopt 启用和禁用时间戳的开销

struct msghdr *msg;
...
cmsg                         = CMSG_FIRSTHDR(msg);
cmsg->cmsg_level             = SOL_SOCKET;
cmsg->cmsg_type              = SO_TIMESTAMPING;
cmsg->cmsg_len               = CMSG_LEN(sizeof(__u32));
*((__u32 *) CMSG_DATA(cmsg)) = SOF_TIMESTAMPING_TX_SCHED |
                               SOF_TIMESTAMPING_TX_SOFTWARE |
                               SOF_TIMESTAMPING_TX_ACK;
err = sendmsg(fd, msg, 0);

通过 cmsg 设置的 SOF_TIMESTAMPING_TX_* 标志将覆盖通过 setsockopt 设置的 SOF_TIMESTAMPING_TX_* 标志。

此外,应用程序仍必须通过 setsockopt 启用时间戳报告才能接收时间戳

__u32 val = SOF_TIMESTAMPING_SOFTWARE |
            SOF_TIMESTAMPING_OPT_ID /* or any other flag */;
err = setsockopt(fd, SOL_SOCKET, SO_TIMESTAMPING, &val, sizeof(val));

1.4 字节流时间戳

SO_TIMESTAMPING 接口支持字节流中的字节时间戳。每个请求都解释为请求缓冲区的全部内容何时通过时间戳点。也就是说,对于流选项 SOF_TIMESTAMPING_TX_SOFTWARE 将记录所有字节何时到达设备驱动程序,而不管数据已转换为多少个数据包。

通常,字节流没有自然的定界符,因此将时间戳与数据相关联并非易事。一定范围的字节可能会在段之间拆分,任何段都可能会合并(可能会合并与独立 send() 调用关联的先前分段缓冲区的各个部分)。段可以重新排序,并且相同的字节范围可以在多个段中共存,以用于实施重传的协议。

至关重要的是,所有时间戳都应实施相同的语义,而不管这些可能的转换如何,否则它们将无法比较。以不同于简单情况(从缓冲区到 skb 的 1:1 映射)的方式处理“罕见”的极端情况是不够的,因为性能调试通常需要关注此类异常值。

实际上,如果正确选择时间戳的语义和测量时间,则时间戳可以始终如一地与字节流的段相关联。此挑战与决定 IP 分片策略没有什么不同。在那里,定义是仅对第一个片段进行时间戳。对于字节流,我们选择仅当所有字节都通过一个点时才生成时间戳。如定义的 SOF_TIMESTAMPING_TX_ACK 易于实施和推理。由于可能的传输漏洞和乱序到达,必须考虑 SACK 的实施将更加复杂。

在主机上,由于 Nagle、cork、autocork、分段和 GSO,TCP 也可能会破坏从缓冲区到 skbuff 的简单 1:1 映射。该实施通过跟踪传递给 send() 的每个最后一个字节来确保在所有情况下都正确,即使它在 skbuff 扩展或合并操作后不再是最后一个字节。它将相关序列号存储在 skb_shinfo(skb)->tskey 中。由于 skbuff 只有一个此类字段,因此只能生成一个时间戳。

在极少数情况下,如果两个请求折叠到同一个 skb 上,则可能会错过时间戳请求。进程可以通过启用 SOF_TIMESTAMPING_OPT_ID 并比较发送时的字节偏移量与为每个时间戳返回的值来检测这种情况。它可以通过始终刷新请求之间的 TCP 堆栈来防止这种情况,例如,通过启用 TCP_NODELAY 并禁用 TCP_CORK 和 autocork。在 linux-4.7 之后,防止合并的更好方法是在 sendmsg() 时使用 MSG_EOR 标志。

这些预防措施确保仅当所有字节都通过时间戳点时才生成时间戳,前提是网络堆栈本身不重新排序段。堆栈确实尝试避免重新排序。唯一例外情况是在管理员控制下:可以构建一个数据包调度程序配置,以不同方式延迟来自同一流的段。这样的设置是不寻常的。

2 数据接口

时间戳是使用 recvmsg() 的辅助数据功能读取的。有关此接口的详细信息,请参阅 man 3 cmsg。套接字手册页 (man 7 socket) 描述了如何检索使用 SO_TIMESTAMP 和 SO_TIMESTAMPNS 记录生成的时间戳。

2.1 SCM_TIMESTAMPING 记录

这些时间戳在控制消息中返回,其中 cmsg_level 为 SOL_SOCKET,cmsg_type 为 SCM_TIMESTAMPING,有效负载类型为

对于 SO_TIMESTAMPING_OLD

struct scm_timestamping {
        struct timespec ts[3];
};

对于 SO_TIMESTAMPING_NEW

struct scm_timestamping64 {
        struct __kernel_timespec ts[3];

始终使用 SO_TIMESTAMPING_NEW 时间戳,以始终获取 struct scm_timestamping64 格式的时间戳。

SO_TIMESTAMPING_OLD 在 32 位机器上 2038 年后返回不正确的时间戳。

该结构最多可以返回三个时间戳。这是一个遗留功能。任何时候至少一个字段为非零。大多数时间戳在 ts[0] 中传递。硬件时间戳在 ts[2] 中传递。

ts[1] 过去用于保存转换为系统时间的硬件时间戳。相反,直接在 NIC 上公开硬件时钟设备作为 HW PTP 时钟源,以允许用户空间中的时间转换并可选择地将系统时间与用户空间 PTP 堆栈(例如 linuxptp)同步。对于 PTP 时钟 API,请参阅 Linux 的 PTP 硬件时钟基础设施

请注意,如果在启用 SOF_TIMESTAMPING_SOFTWARE 的情况下同时启用 SO_TIMESTAMP 或 SO_TIMESTAMPNS 选项以及 SO_TIMESTAMPING,则在 recvmsg() 调用中将生成一个虚假的软件时间戳,并在缺少真正的软件时间戳时在 ts[0] 中传递。硬件传输时间戳也会发生这种情况。

2.1.1 带有 MSG_ERRQUEUE 的传输时间戳

对于传输时间戳,传出的数据包将循环回到套接字的错误队列,并附加发送时间戳。进程通过调用 recvmsg() 并设置标志 MSG_ERRQUEUE,以及具有足够大的 msg_control 缓冲区以接收相关元数据结构来接收时间戳。recvmsg 调用返回带有两个附加辅助消息的原始传出数据包。

cm_level SOL_IP(V6) 和 cm_type IP(V6)_RECVERR 的消息嵌入 struct sock_extended_err。这定义了错误类型。对于时间戳,ee_errno 字段为 ENOMSG。另一个辅助消息将具有 cm_level SOL_SOCKET 和 cm_type SCM_TIMESTAMPING。这嵌入了 struct scm_timestamping。

2.1.1.2 时间戳类型

三个 struct timespec 的语义由扩展错误结构中的字段 ee_info 定义。它包含 SCM_TSTAMP_* 类型的值,以定义 scm_timestamping 中传递的实际时间戳。

SCM_TSTAMP_* 类型与前面讨论的 SOF_TIMESTAMPING_* 控制字段 1:1 匹配,但有一个例外。由于遗留原因,SCM_TSTAMP_SND 等于零,并且可以为 SOF_TIMESTAMPING_TX_HARDWARE 和 SOF_TIMESTAMPING_TX_SOFTWARE 设置。如果 ts[2] 非零,则为第一个;否则为第二个,在这种情况下,时间戳存储在 ts[0] 中。

2.1.1.3 分片

传出数据报的分片很少见,但有可能,例如,通过显式禁用 PMTU 发现。如果传出数据包被分片,则只有第一个片段被加上时间戳并返回到发送套接字。

2.1.1.4 数据包有效负载

调用应用程序通常不感兴趣接收它最初传递给堆栈的整个数据包有效负载:套接字错误队列机制只是一种在时间戳上搭载的方法。在这种情况下,应用程序可以选择使用较小的缓冲区读取数据报,甚至可能长度为 0。有效负载会相应地被截断。但是,在进程在错误队列上调用 recvmsg() 之前,完整的数据包会被排队,占用 SO_RCVBUF 的预算。

2.1.1.5 阻塞读取

从错误队列读取始终是非阻塞操作。要阻塞等待时间戳,请使用 poll 或 select。如果在错误队列上有任何数据准备就绪,poll() 将在 pollfd.revents 中返回 POLLERR。无需在 pollfd.events 中传递此标志。此标志在请求时被忽略。另请参阅 man 2 poll

2.1.2 接收时间戳

在接收时,没有理由从套接字错误队列读取。SCM_TIMESTAMPING 辅助数据与数据包数据一起在正常的 recvmsg() 上发送。由于这不是套接字错误,因此它没有伴随消息 SOL_IP(V6)/IP(V6)_RECVERROR。在这种情况下,隐式定义了 struct scm_timestamping 中三个字段的含义。如果设置了 ts[0],则它保存一个软件时间戳,ts[1] 再次被弃用,如果设置了 ts[2],则它保存一个硬件时间戳。

3. 硬件时间戳配置:ETHTOOL_MSG_TSCONFIG_SET/GET

还必须为预计进行硬件时间戳的每个设备驱动程序初始化硬件时间戳。该参数在 include/uapi/linux/net_tstamp.h 中定义为

struct hwtstamp_config {
        int flags;      /* no flags defined right now, must be zero */
        int tx_type;    /* HWTSTAMP_TX_* */
        int rx_filter;  /* HWTSTAMP_FILTER_* */
};

所需的行为通过调用 tsconfig netlink 套接字 ETHTOOL_MSG_TSCONFIG_SET 传递到内核和特定设备。然后使用 ETHTOOL_A_TSCONFIG_TX_TYPESETHTOOL_A_TSCONFIG_RX_FILTERSETHTOOL_A_TSCONFIG_HWTSTAMP_FLAGS netlink 属性相应地设置 struct hwtstamp_config。

ETHTOOL_A_TSCONFIG_HWTSTAMP_PROVIDER netlink 嵌套属性用于选择硬件时间戳的源。它由设备源的索引和时间戳类型的限定符组成。

驱动程序可以自由使用比请求的配置更宽松的配置。预计驱动程序应仅直接实施可以支持的最通用的模式。例如,如果硬件可以支持 HWTSTAMP_FILTER_PTP_V2_EVENT,则它通常应始终升级 HWTSTAMP_FILTER_PTP_V2_L2_SYNC 等等,因为 HWTSTAMP_FILTER_PTP_V2_EVENT 更通用(并且对应用程序更有用)。

支持硬件时间戳的驱动程序应使用实际的(可能更宽松的)配置更新该结构。如果无法对请求的数据包进行时间戳,则不应更改任何内容,并且应返回 ERANGE(与 EINVAL 形成对比,后者表示根本不支持 SIOCSHWTSTAMP)。

只有具有管理员权限的进程才能更改配置。用户空间负责确保多个进程不会相互干扰并且设置已重置。

任何进程都可以通过请求 tsconfig netlink 套接字 ETHTOOL_MSG_TSCONFIG_GET 来读取实际配置。

遗留配置是使用 ioctl(SIOCSHWTSTAMP) 并指向 struct ifreq 的指针,其 ifr_data 指向 struct hwtstamp_config。tx_type 和 rx_filter 提示驱动程序它应该做什么。如果不支持对传入数据包的请求的细粒度过滤,则驱动程序可能会对不仅仅是请求的数据包类型进行时间戳。ioctl(SIOCGHWTSTAMP) 的使用方式与 ioctl(SIOCSHWTSTAMP) 相同。但是,并非所有驱动程序都已实施此功能。

/* possible values for hwtstamp_config->tx_type */
enum {
        /*
        * no outgoing packet will need hardware time stamping;
        * should a packet arrive which asks for it, no hardware
        * time stamping will be done
        */
        HWTSTAMP_TX_OFF,

        /*
        * enables hardware time stamping for outgoing packets;
        * the sender of the packet decides which are to be
        * time stamped by setting SOF_TIMESTAMPING_TX_SOFTWARE
        * before sending the packet
        */
        HWTSTAMP_TX_ON,
};

/* possible values for hwtstamp_config->rx_filter */
enum {
        /* time stamp no incoming packet at all */
        HWTSTAMP_FILTER_NONE,

        /* time stamp any incoming packet */
        HWTSTAMP_FILTER_ALL,

        /* return value: time stamp all packets requested plus some others */
        HWTSTAMP_FILTER_SOME,

        /* PTP v1, UDP, any kind of event packet */
        HWTSTAMP_FILTER_PTP_V1_L4_EVENT,

        /* for the complete list of values, please check
        * the include file include/uapi/linux/net_tstamp.h
        */
};

3.1 硬件时间戳实施:设备驱动程序

支持硬件时间戳的驱动程序必须支持 ndo_hwtstamp_set NDO 或遗留 SIOCSHWTSTAMP ioctl,并按照 SIOCSHWTSTAMP 部分中的描述使用实际值更新提供的 struct hwtstamp_config。它还应支持 ndo_hwtstamp_get 或遗留 SIOCGHWTSTAMP。

必须将接收数据包的时间戳存储在 skb 中。要获取指向 skb 的共享时间戳结构的指针,请调用 skb_hwtstamps()。然后在结构中设置时间戳

struct skb_shared_hwtstamps {
        /* hardware time stamp transformed into duration
        * since arbitrary point in time
        */
        ktime_t     hwtstamp;
};

传出数据包的时间戳生成方式如下

  • 在 hard_start_xmit() 中,检查是否设置了 (skb_shinfo(skb)->tx_flags & SKBTX_HW_TSTAMP) 非零。如果是,则驱动程序预计会进行硬件时间戳。

  • 如果这对于 skb 是可能的并且被请求,则通过在 skb_shinfo(skb)->tx_flags 中设置标志 SKBTX_IN_PROGRESS 来声明驱动程序正在进行时间戳,例如

    skb_shinfo(skb)->tx_flags |= SKBTX_IN_PROGRESS;
    

    您可能想要保留指向关联 skb 的指针以进行下一步,而不是释放 skb。不支持硬件时间戳的驱动程序不会这样做。驱动程序绝不能触摸 sk_buff::tstamp!它用于存储网络子系统生成的软件时间戳。

  • 驱动程序应尽可能接近地将 sk_buff 传递给硬件时调用 skb_tx_timestamp()skb_tx_timestamp() 提供软件时间戳(如果请求且硬件时间戳不可用(未设置 SKBTX_IN_PROGRESS))。

  • 一旦驱动程序发送了数据包和/或获得了数据包的硬件时间戳,它就会通过调用 skb_tstamp_tx() 将时间戳传递回去,其中包含原始 skb 和原始硬件时间戳。skb_tstamp_tx() 克隆原始 skb 并添加时间戳,因此现在必须释放原始 skb。如果获取硬件时间戳以某种方式失败,则驱动程序不应回退到软件时间戳。原因是这会在处理管道中的较晚时间发生,因此可能会导致时间戳之间的意外增量。

3.2 堆叠 PTP 硬件时钟的特殊注意事项

在数据包的数据路径中可能存在多个 PHC(PTP 硬件时钟)的情况下。内核没有显式机制来允许用户选择哪个 PHC 用于时间戳以太网帧。相反,假设最外面的 PHC 始终是最可取的,并且内核驱动程序协作以实现该目标。目前有 3 种堆叠 PHC 的情况,详述如下

3.2.1 DSA(分布式交换机架构)交换机

这些是以太网交换机,其一个端口连接到(否则完全不知情的)主机以太网接口,并执行具有可选转发加速功能的端口倍增器的角色。每个 DSA 交换机端口对用户来说都可见,就像一个独立的(虚拟)网络接口,并且在底层,它的网络 I/O 是通过主机接口间接执行的(重定向到 TX 上的主机端口,并拦截 RX 上的帧)。

当 DSA 交换机连接到主机端口时,PTP 同步必须受到影响,因为交换机的可变排队延迟会在主机端口及其 PTP 伙伴之间引入路径延迟抖动。因此,某些 DSA 交换机包括它们自己的时间戳时钟,并且能够在它们自己的 MAC 上执行网络时间戳,这样路径延迟仅测量线路和 PHY 传播延迟。Linux 支持时间戳 DSA 交换机,并公开与其他任何网络接口相同的 ABI(除了 DSA 接口在网络 I/O 方面实际上是虚拟的事实外,它们确实有自己的 PHC)。DSA 交换机的所有接口共享同一个 PHC 是典型的,但不是强制性的。

通过设计,使用 DSA 交换机进行 PTP 时间戳不需要在附加到的主机端口的驱动程序中进行任何特殊处理。但是,当主机端口也支持 PTP 时间戳时,DSA 将负责拦截 .ndo_eth_ioctl 对主机端口的调用,并阻止尝试在其上启用硬件时间戳。这是因为 SO_TIMESTAMPING API 不允许为同一个数据包传递多个硬件时间戳,因此必须防止 DSA 交换机端口以外的任何其他人这样做。

在通用层中,DSA 为 PTP 时间戳提供以下基础设施

  • .port_txtstamp():在从用户空间传输带有硬件 TX 时间戳请求的数据包之前调用的挂钩。这是两步时间戳所必需的,因为硬件时间戳在实际 MAC 传输之后变为可用,因此驱动程序必须准备好将时间戳与原始数据包相关联,以便它可以将数据包重新排队到套接字的错误队列中。为了在时间戳可用时保存数据包,驱动程序可以调用 skb_clone_sk,将克隆指针保存在 skb->cb 中,并将 tx skb 队列排队。通常,交换机将具有 PTP TX 时间戳寄存器(或有时是 FIFO),其中时间戳变为可用。对于 FIFO,硬件可能会存储 PTP 序列 ID/消息类型/域号的键值对和实际时间戳。为了在等待时间戳的数据包队列和实际时间戳之间正确执行关联,驱动程序可以使用 BPF 分类器 (ptp_classify_raw) 来识别 PTP 传输类型,并使用 ptp_parse_header 来解释 PTP 标头字段。可能有一个 IRQ 在此时间戳可用时引发,或者驱动程序可能需要在调用主机接口的 dev_queue_xmit() 之后进行轮询。一步 TX 时间戳不需要数据包克隆,因为 PTP 协议不需要后续消息(因为 TX 时间戳是由 MAC 嵌入到数据包中的),因此用户空间不希望带有 TX 时间戳注释的数据包重新排队到其套接字的错误队列中。

  • .port_rxtstamp():在 RX 上,BPF 分类器由 DSA 运行以识别 PTP 事件消息(任何其他数据包,包括 PTP 常规消息,都不会被时间戳)。原始(也是唯一的)可时间戳 skb 提供给驱动程序,以便它可以使用时间戳对其进行注释(如果该时间戳立即可用),或推迟到以后。在接收时,时间戳可能以带内方式(通过 DSA 标头中的元数据或以其他方式附加到数据包)或以带外方式(通过另一个 RX 时间戳 FIFO)提供。当检索时间戳需要可睡眠上下文时,通常需要 RX 上的延迟。在这种情况下,DSA 驱动程序有责任在新时间戳的 skb 上调用 netif_rx()

3.2.2 以太网 PHY

这些设备通常在网络堆栈中扮演第 1 层角色,因此不像 DSA 交换机那样具有网络接口表示。但是,由于性能原因,PHY 可能能够检测和时间戳 PTP 数据包:尽可能靠近线路获取的时间戳有可能产生更稳定和精确的同步。

支持 PTP 时间戳的 PHY 驱动程序必须创建一个 struct mii_timestamper 并在 phydev->mii_ts 中添加一个指向它的指针。网络堆栈将检查此指针是否存在。

由于 PHY 没有网络接口表示,因此它们的时间戳和 ethtool ioctl 操作需要由它们各自的 MAC 驱动程序来协调。因此,与 DSA 交换机相反,需要对每个 MAC 驱动程序进行修改才能支持 PHY 时间戳。这包括

  • .ndo_eth_ioctl 中检查 phy_has_hwtstamp(netdev->phydev) 是否为真。如果是,则 MAC 驱动程序不应处理此请求,而是应使用 phy_mii_ioctl() 将其传递给 PHY。

  • 在 RX 上,可能需要也可能不需要特殊干预,具体取决于用于将 skb 传递到网络堆栈的函数。在普通 netif_rx() 和类似函数的情况下,MAC 驱动程序必须检查 skb_defer_rx_timestamp(skb) 是否是必要的 - 如果是,则根本不调用 netif_rx()。如果启用了 CONFIG_NETWORK_PHY_TIMESTAMPING 并且 skb->dev->phydev->mii_ts 存在,则其 .rxtstamp() hook 将立即被调用,以使用与 DSA 非常相似的逻辑来确定 RX 时间戳延迟是否是必要的。同样像 DSA 一样,当时间戳可用时,PHY 驱动程序有责任将数据包发送到堆栈。

    对于其他 skb 接收函数,例如 napi_gro_receivenetif_receive_skb,堆栈会自动检查 skb_defer_rx_timestamp() 是否必要,因此驱动程序内部不需要此检查。

  • 在 TX 上,同样,可能需要也可能不需要特殊干预。调用 mii_ts->txtstamp() hook 的函数名为 skb_clone_tx_timestamp()。可以直接调用此函数(在这种情况下,确实需要显式的 MAC 驱动程序支持),但是该函数也从 skb_tx_timestamp() 调用中借用,许多 MAC 驱动程序已经出于软件时间戳目的执行此调用。因此,如果 MAC 支持软件时间戳,则在此阶段无需执行任何进一步操作。

3.2.3 MII 总线侦听设备

这些设备执行与时间戳 Ethernet PHY 相同的作用,不同之处在于它们是离散设备,因此可以与任何 PHY 结合使用,即使它不支持时间戳。在 Linux 中,它们可以通过设备树发现并附加到 struct phy_device,其余部分与那些设备使用相同的 mii_ts 基础设施。有关更多详细信息,请参见 Documentation/devicetree/bindings/ptp/timestamper.txt。

3.2.4 MAC 驱动程序的其他注意事项

堆叠 PHC 的使用可能会暴露在没有它们的情况下无法触发的 MAC 驱动程序错误。一个例子与这行代码有关,已经在前面介绍过

skb_shinfo(skb)->tx_flags |= SKBTX_IN_PROGRESS;

任何 TX 时间戳逻辑,无论是普通的 MAC 驱动程序、DSA 交换机驱动程序、PHY 驱动程序还是 MII 总线侦听设备驱动程序,都应设置此标志。但是,不了解 PHC 堆叠的 MAC 驱动程序可能会被其他人而不是自己设置此标志所绊倒,并传递重复的时间戳。例如,TX 时间戳的典型驱动程序设计可能是将传输部分分为 2 个部分

  1. “TX”:检查是否已通过 .ndo_eth_ioctl(“priv->hwtstamp_tx_enabled == true”)先前启用了 PTP 时间戳,以及当前 skb 是否需要 TX 时间戳(“skb_shinfo(skb)->tx_flags & SKBTX_HW_TSTAMP”)。如果为真,则设置 “skb_shinfo(skb)->tx_flags |= SKBTX_IN_PROGRESS” 标志。注意:如上所述,在堆叠的 PHC 系统的情况下,此条件永远不应触发,因为此 MAC 肯定不是最外层的 PHC。但这并不是典型问题所在。传输继续进行此数据包。

  2. “TX 确认”:传输已完成。驱动程序检查是否有必要为其收集任何 TX 时间戳。这是典型问题所在:MAC 驱动程序走捷径,仅检查是否设置了 “skb_shinfo(skb)->tx_flags & SKBTX_IN_PROGRESS”。使用堆叠的 PHC 系统,这是不正确的,因为此 MAC 驱动程序并不是 TX 数据路径中唯一可以首先启用 SKBTX_IN_PROGRESS 的实体。

此问题的正确解决方案是 MAC 驱动程序在其 “TX 确认” 部分中进行复合检查,不仅要检查 “skb_shinfo(skb)->tx_flags & SKBTX_IN_PROGRESS”,还要检查 “priv->hwtstamp_tx_enabled == true”。因为系统的其余部分确保 PTP 时间戳仅为最外层的 PHC 启用,所以此增强的检查将避免将重复的 TX 时间戳传递给用户空间。