软件时间戳实现原理

一、内核软件时间戳的整体架构

Linux 网络栈的时间戳分为硬件时间戳软件时间戳两条路径:

用户态 ptp4l / sock
    ↓ SO_TIMESTAMPING 标志
内核协议栈
    ↓
┌─────────────────────────────────────┐
│ TX 路径:skb_tx_timestamp()         │  ← 软件TX时间戳
│ RX 路径:net_timestamp()            │  ← 软件RX时间戳
├─────────────────────────────────────┤
│ 硬件时间戳:通过 PHC 驱动获取        │
│ (CPTS/AM65-CPTS)                    │
└─────────────────────────────────────┘
    ↓
sock 错误队列 / 控制消息
    ↓
用户态读取

二、关键数据结构

ethtool_ts_info — 声明网卡的时间戳能力:

struct ethtool_ts_info {
    u32 so_timestamping;   // 支持的时间戳类型位掩码
    int phc_index;         // 关联的 PHC 设备索引,-1 表示无
    ...
};

SOF_TIMESTAMPING_* 标志位(定义在 linux/net_tstamp.h):

标志含义
SOF_TIMESTAMPING_TX_SOFTWARE支持 TX 软件时间戳
SOF_TIMESTAMPING_RX_SOFTWARE支持 RX 软件时间戳
SOF_TIMESTAMPING_SOFTWARE系统软件时间戳引擎可用

这三者是软件时间戳的最小声明集合。如果还支持硬件时间戳,则会额外设置 SOF_TIMESTAMPING_TX_HARDWARESOF_TIMESTAMPING_RX_HARDWARESOF_TIMESTAMPING_RAW_HARDWARE 等。

三、TX 软件时间戳的工作流程

1. 用户态请求时间戳

应用程序通过 setsockopt 启用时间戳:

int val = SOF_TIMESTAMPING_TX_SOFTWARE | SOF_TIMESTAMPING_SOFTWARE;
setsockopt(fd, SOL_SOCKET, SO_TIMESTAMPING, &val, sizeof(val));

内核在 sock 结构中记录此标志。

2. 发送路径记录时间戳

vport_ndo_xmit 中(第1954行):

done_tx:
    skb_tx_timestamp(skb);    // ← 这里
    netdev_tx_sent_queue(netif_txq, pkt_len);
    ...

skb_tx_timestamp() 的内部逻辑(net/core/timestamp.c):

skb_tx_timestamp(skb)
  ├── 检查 sock->tsflags 是否包含 SOF_TIMESTAMPING_TX_SOFTWARE
  │     ↓ 否则直接返回,零开销
  ├── 检查 skb->sk 是否存在
  │     ↓ 无关联sock则返回
  └── __skb_tstamp_tx(skb, SKBTX_SW_TSTAMP)
        ├── 调用 ktime_get_real() 获取 CLOCK_REALTIME 当前时间
        ├── 构造 scm_timestamping 结构(3个时间戳槽位,slot[0]填软件时间戳)
        ├── 通过 sock_queue_err_skb() 将时间戳放入 sock 错误队列
        └── 触发 SKB_FCLONE_TIMEFLAG 标记,防止 skb 在完成前被释放

关键点

  • 软件时间戳在 skb_tx_timestamp() 调用点取的是 CLOCK_REALTIME,不是 PHC
  • 调用位置在数据推入 DMA 之前,记录的是"即将发送"的时刻
  • 如果 skb->sk 为空或未设置 SOF_TIMESTAMPING_TX_SOFTWARE,函数立即返回,开销极小
3. 用户态读取时间戳

发送后,应用程序通过 recvmsg() 从 sock 错误队列读取:

struct msghdr msg;
struct iovec iov;
char ctrl[512];
struct cmsghdr *cmsg;

msg.msg_control = ctrl;
msg.msg_controllen = sizeof(ctrl);
recvmsg(fd, &msg, MSG_ERRQUEUE);  // 从错误队列读取

for (cmsg = CMSG_FIRSTHDR(&msg); cmsg; cmsg = CMSG_NXTHDR(&msg, cmsg)) {
    if (cmsg->cmsg_level == SOL_SOCKET &&
        cmsg->cmsg_type == SO_TIMESTAMPING) {
        struct scm_timestamping *ts = (void *)CMSG_DATA(cmsg);
        // ts->ts[0] = 软件时间戳 (CLOCK_REALTIME)
        // ts->ts[1] = 硬件时间戳 (若支持)
        // ts->ts[2] = 原始硬件时间戳 (若支持)
    }
}

四、RX 软件时间戳的工作流程

RX 软件时间戳由内核网络栈在接收路径自动处理,不需要驱动显式调用

网卡接收 → DMA完成 → NAPI poll → vport_rx_packets()
                                       ↓
                              napi_gro_receive()
                                       ↓
                              netif_receive_skb() / netif_receive_skb_list()
                                       ↓
                              __netif_receive_skb_core()
                                       ↓
                              net_timestamp(skb)   ← 内核自动调用
                                ├── 检查 NETIF_F_RXHASH 等特征
                                ├── 如果 sock 需要 RX 软件时间戳
                                └── ktime_get_real() → skb->tstamp
                                       ↓
                              送往协议栈 → sock 控制消息

驱动本身不需要在 RX 路径做任何额外工作。只要 ethtool_ts_info 声明了 SOF_TIMESTAMPING_RX_SOFTWARE,内核协议栈会自动处理。

五、get_ts_info 回调的作用

static int vport_get_ts_info(struct net_device *ndev,
                             struct ethtool_ts_info *info)
{
    info->so_timestamping = SOF_TIMESTAMPING_TX_SOFTWARE |
                           SOF_TIMESTAMPING_RX_SOFTWARE |
                           SOF_TIMESTAMPING_SOFTWARE;
    info->phc_index = -1;
    return 0;
}

这个回调的作用是向用户态声明能力。没有它,ethtool -T eth2 会显示不支持任何时间戳,ptp4l 也不会尝试使用软件时间戳模式:

# 修改前
$ ethtool -T eth2
Time stamping parameters for eth2:
Capabilities:
    (无)
# 修改后
$ ethtool -T eth2
Time stamping parameters for eth2:
Capabilities:
    software-transmit
    software-receive
    software

phc_index = -1 表示没有关联的 PTP 硬件时钟(PHC),这是 proxy client 驱动的正确设置——它无法直接访问 CPTS 硬件,因为 CPSW 硬件由 R5F EthFw 服务器管控。

六、软件时间戳 vs 硬件时间戳对比

                软件时间戳                    硬件时间戳
时间源          CLOCK_REALTIME (系统时钟)      PHC (精确硬件时钟)
取戳位置        skb_tx_timestamp() (驱动层)    硬件在帧离开MAC时打戳
取戳时刻        "即将提交DMA"                  "帧实际发送到线路"
误差来源        系统调度延迟、中断延迟           仅PHY/MAC内部延迟
典型精度        ~10-100 μs                    ~10-100 ns
需要PHC         否                            是
需要phc2sys     否                            是(同步PHC与系统时钟)
驱动修改量      约10行(get_ts_info + 头文件)  需要CPTS驱动集成

七、本驱动的特殊性

cpsw-proxy-client 是代理客户端,CPSW 硬件由 R5F EthFw 服务器控制。这意味着:

  1. 无法直接获取硬件时间戳 — CPTS 寄存器在 R5F 侧,A72 Linux 无法访问
  2. skb_tx_timestamp() 已存在 — TI 主线代码已在第1954行调用,说明原始设计就预留了软件时间戳的支持
  3. 缺少的只是能力声明 — get_ts_info 未注册,导致用户态工具无法发现这个能力。本次修改补上了这个缺口

如果将来 EthFw 服务器支持通过 RPMsg 返回硬件时间戳,理论上可以实现硬件时间戳路径,但需要:

  • EthFw 在发送完成通知中附带 CPTS 时间戳
  • 驱动在 TX 完成回调中解析并调用 skb_hwtstamp_tx()
  • 声明 SOF_TIMESTAMPING_TX_HARDWARE 并绑定 PHC 设备
Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐