时间戳¶
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 格式的时间戳。
在 32 位计算机上,SO_TIMESTAMP_OLD 在 2038 年之后返回不正确的时间戳。
1.2 SO_TIMESTAMPNS(也是 SO_TIMESTAMPNS_OLD 和 SO_TIMESTAMPNS_NEW)¶
此选项与 SO_TIMESTAMP 相同,只是返回的数据类型不同。它的 struct timespec 允许比 SO_TIMESTAMP 的 timeval (ms) 更高分辨率 (ns) 的时间戳。
始终使用 SO_TIMESTAMPNS_NEW 时间戳,以始终获得 struct __kernel_timespec 格式的时间戳。
在 32 位计算机上,SO_TIMESTAMPNS_OLD 在 2038 年之后返回不正确的时间戳。
1.3 SO_TIMESTAMPING(也是 SO_TIMESTAMPING_OLD 和 SO_TIMESTAMPING_NEW)¶
支持多种类型的时间戳请求。因此,此套接字选项采用标志的位图,而不是布尔值。在
err = setsockopt(fd, SOL_SOCKET, SO_TIMESTAMPING, &val, sizeof(val));
val 是一个整数,其中设置了以下任意位。设置其他位返回 EINVAL 且不更改当前状态。
套接字选项配置单个 sk_buff (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 实现。对于该协议,它可能会过度报告测量结果,因为当确认发送() 处的所有数据(包括缓冲区)时会生成时间戳:累积确认。该机制忽略 SACK 和 FACK。可以通过套接字选项和控制消息启用此标志。
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 在该套接字的所有可能并发的未完成时间戳请求中是唯一的。
进程可以选择覆盖默认生成的 ID,方法是通过控制消息 SCM_TS_OPT_ID(TCP 套接字不支持)传递一个特定的 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 节)。使用此功能,应用程序可以在不承担通过 setsockopt 启用和禁用时间戳的开销的情况下,对每个 sendmsg() 的时间戳进行采样。
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 格式的时间戳。
在 32 位机器上,SO_TIMESTAMPING_OLD 会在 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 的发送时间戳¶
对于发送时间戳,传出的数据包将循环回到套接字的错误队列,并附带发送时间戳。进程通过调用带有 MSG_ERRQUEUE 标志的 recvmsg() 并使用足够大的 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. 硬件时间戳配置:SIOCSHWTSTAMP 和 SIOCGHWTSTAMP¶
还必须为每个预期执行硬件时间戳的设备驱动程序初始化硬件时间戳。该参数在 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_* */
};
期望的行为通过调用 ioctl(SIOCSHWTSTAMP) 并传递指向 struct ifreq 的指针来传递到内核并传递到特定设备,其中 struct ifreq 的 ifr_data 指向 struct hwtstamp_config。 tx_type 和 rx_filter 是对驱动程序预期行为的提示。 如果不支持对传入数据包的精细过滤,则驱动程序可能会对超出请求类型的数据包进行时间戳标记。
驱动程序可以自由使用比请求的配置更宽松的配置。预期驱动程序应该只直接实现可以支持的最通用的模式。例如,如果硬件可以支持 HWTSTAMP_FILTER_PTP_V2_EVENT,那么它通常应该总是向上扩展 HWTSTAMP_FILTER_PTP_V2_L2_SYNC,依此类推,因为 HWTSTAMP_FILTER_PTP_V2_EVENT 更通用(对应用程序更有用)。
支持硬件时间戳的驱动程序应使用实际的、可能更宽松的配置更新 struct。如果无法对请求的数据包进行时间戳标记,则不应进行任何更改,并且应返回 ERANGE(与 EINVAL 相反,后者表示完全不支持 SIOCSHWTSTAMP)。
只有具有管理员权限的进程才能更改配置。用户空间负责确保多个进程不会互相干扰,并且设置已重置。
任何进程都可以通过以相同方式将此结构传递给 ioctl(SIOCGHWTSTAMP) 来读取实际配置。但是,并非所有驱动程序都已实现此功能。
/* 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 硬件时间戳实现:设备驱动程序¶
支持硬件时间戳的驱动程序必须支持 SIOCSHWTSTAMP ioctl,并使用 SIOCSHWTSTAMP 部分中所述的实际值更新提供的 struct hwtstamp_config。它还应该支持 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 上,DSA 运行 BPF 分类器来识别 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 没有网络接口表示,因此需要由它们各自的 MAC 驱动程序来调解它们的时间戳和 ethtool ioctl 操作。因此,与 DSA 交换机相反,需要对每个单独的 MAC 驱动程序进行修改,以支持 PHY 时间戳。 这需要
在
.ndo_eth_ioctl
中,检查phy_has_hwtstamp(netdev->phydev)
是否为 true。如果是,则 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()
钩子,以使用与 DSA 非常相似的逻辑来确定是否需要延迟 RX 时间戳。同样像 DSA 一样,当时间戳可用时,将数据包发送到堆栈的责任将由 PHY 驱动程序承担。对于其他 skb 接收函数,例如
napi_gro_receive
和netif_receive_skb
,堆栈会自动检查是否需要skb_defer_rx_timestamp()
,因此驱动程序内部不需要此检查。在 TX (发送) 方面,同样,可能需要也可能不需要特殊干预。调用
mii_ts->txtstamp()
钩子的函数名为skb_clone_tx_timestamp()
。此函数可以直接调用(在这种情况下,确实需要显式的 MAC 驱动程序支持),但是该函数也从skb_tx_timestamp()
调用中“捎带”,许多 MAC 驱动程序已经出于软件时间戳的目的执行此调用。因此,如果 MAC 支持软件时间戳,则在此阶段无需执行任何其他操作。
3.2.3 MII 总线侦听设备¶
这些设备执行与时间戳以太网 PHY 相同的作用,但区别在于它们是独立的设备,因此可以与任何 PHY 结合使用,即使该 PHY 不支持时间戳。在 Linux 中,它们可以通过设备树被发现并附加到 struct phy_device
,其余部分,它们使用与那些设备相同的 mii_ts 基础设施。有关更多详细信息,请参见 Documentation/devicetree/bindings/ptp/timestamper.txt。
3.2.4 MAC 驱动程序的其他注意事项¶
堆叠的 PHC,尤其是 DSA(但不仅限于此) - 因为这不需要对 MAC 驱动程序进行任何修改,因此更难确保所有可能代码路径的正确性 - 是它们暴露了在堆叠 PTP 时钟存在之前不可能触发的错误。一个例子与之前已经出现过的这行代码有关
skb_shinfo(skb)->tx_flags |= SKBTX_IN_PROGRESS;
任何 TX 时间戳逻辑,无论是普通的 MAC 驱动程序、DSA 交换机驱动程序、PHY 驱动程序还是 MII 总线侦听设备驱动程序,都应设置此标志。但是,不了解 PHC 堆叠的 MAC 驱动程序可能会因自身之外的其他人设置此标志而被绊倒,并传递重复的时间戳。例如,TX 时间戳的典型驱动程序设计可能是将传输部分分为两个部分
“TX”:检查是否先前已通过
.ndo_eth_ioctl
( “priv->hwtstamp_tx_enabled == true
”) 启用了 PTP 时间戳,并且当前 skb 需要 TX 时间戳 ( “skb_shinfo(skb)->tx_flags & SKBTX_HW_TSTAMP
”)。如果为 true,则设置 “skb_shinfo(skb)->tx_flags |= SKBTX_IN_PROGRESS
” 标志。注意:如上所述,在堆叠的 PHC 系统的情况下,此条件永远不应触发,因为此 MAC 肯定不是最外层的 PHC。但这并非典型问题所在。传输继续进行此数据包。“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
”。因为系统的其余部分确保仅为最外层的 PHC 启用 PTP 时间戳,所以此增强的检查将避免向用户空间传递重复的 TX 时间戳。