关于TCP_options中的timestamps

关于TCP options 中的 timestamps

Linux内核版本:5.10.104

1. TCP timestamp简介

1.1 TCP timestamp的作用

TCP header最大为60字节,固定的20字节长度以及最大40字节的options。本次介绍 option字段中的timestamps。

它主要用于以下2个方面:

  • 超时重传时间RTO动态更新
    • TCP作为可靠的传输协议,一个重要的机制就是超时重传。因此如何计算一个准确(合适)的重传超时时间(RTO, Retransmission TimeOut)对于TCP性能有着重要的影响。
  • PAWS防止序号回绕
    • PAWS 的全称是 Protection Against Wrapped Sequences,即防止序号回绕。它的本质实际上是利用 TCP Timestamp 选项的单调递增特性来识别老旧的报文,防止这些老旧报文的干扰。

与此同时,我们还可以使用TCP timestam来精准的估算**报文往返时间(round-trip-time, RTT)**。

1.2 TCP timestamp的组成

TCP Timestamps Option 由四部分构成:

  • 类别(kind)
  • 长度(Length)
  • 发送方时间戳(TS value)
  • 回显时间戳(TS Echo Reply)

时间戳选项类别(kind)的值等于 8,用来与其它类型的选项区分。长度(length)等于 10。两个时间戳相关的选项都是 4 字节。

Linux内核:#define TCPOPT_TIMESTAMP 8 /* Better RTT estimations/PAWS */

1.3 TCP timestamp的开启

Linux内核中tcp_timestamp默认是开启的

1
2
3
4
5
6
7
8
9
10
11
12
// net/ipv4/tcp_ipv4.c
static int __net_init tcp_sk_init(struct net *net)
{
// ...
net->ipv4.sysctl_max_syn_backlog = max(128, cnt / 128);
net->ipv4.sysctl_tcp_sack = 1;
net->ipv4.sysctl_tcp_window_scaling = 1;
net->ipv4.sysctl_tcp_timestamps = 1; // 默认是开启的
net->ipv4.sysctl_tcp_early_retrans = 3;
net->ipv4.sysctl_tcp_recovery = TCP_RACK_LOSS_DETECTION;
// ...
}

需要注意的是,timestamps是一个双向的选项。当一方不开启时,两方都将停用timestamps。比如client端发送的SYN包中带有timestamp选项,但server端并没有开启该选项。则回复的SYN-ACK将不带timestamp选项,同时client后续回复的ACK也不会带有timestamp选项。当然,如果client发送的SYN包中就不带timestamp,双向都将停用timestamp。

1.4 使用wireshark看TCP timestamp的工作过程

如图所示,在TCP连接三次握手时,发送方发送数据时,将一个发送时间戳 1734581141 放在发送方时间戳TSval中,接收方收到数据包以后,将收到的时间戳 1734581141 原封不动的返回给发送方,放在TSecr字段中,同时把自己的时间戳 3303928779 放在TSval中。

img

接下来使用wireshark查看TCP三次握手时,TCP timestamp的变化。使用SSH连接远程服务器,此时会建立一条TCP连接,以下是三次握手时的报文。

  • 客户端向服务端发起连接请求(第一次)

此时记录了客户端的时间戳 TSval = 3810596399

1
2
3
4
5
6
7
8
9
10
11
Transmission Control Protocol, Src Port: 36358, Dst Port: 22, Seq: 0, Len: 0
Source Port: 36358
Destination Port: 22
Options: (20 bytes), Maximum segment size, SACK permitted, Timestamps, No-Operation (NOP), Window scale
TCP Option - Maximum segment size: 1460 bytes
TCP Option - SACK permitted
TCP Option - Timestamps: TSval 3810596399, TSecr 0
Kind: Time Stamp Option (8)
Length: 10
Timestamp value: 3810596399
Timestamp echo reply: 0
  • 服务端向客户端响应ACK(第二次)
    • TSval = 1878823122
    • TSecr = 3810596399, 客户端发起SYN的时间
1
2
3
4
5
6
7
8
9
10
11
Transmission Control Protocol, Src Port: 22, Dst Port: 36358, Seq: 0, Ack: 1, Len: 0
Source Port: 22
Destination Port: 36358
Options: (20 bytes), Maximum segment size, SACK permitted, Timestamps, No-Operation (NOP), Window scale
TCP Option - Maximum segment size: 1380 bytes
TCP Option - SACK permitted
TCP Option - Timestamps: TSval 1878823122, TSecr 3810596399
Kind: Time Stamp Option (8)
Length: 10
Timestamp value: 1878823122
Timestamp echo reply: 3810596399
  • 客户端影响服务端(第三次)
1
2
3
4
5
6
7
8
9
10
11
Transmission Control Protocol, Src Port: 36358, Dst Port: 22, Seq: 1, Ack: 1, Len: 0
Source Port: 36358
Destination Port: 22
Options: (12 bytes), No-Operation (NOP), No-Operation (NOP), Timestamps
TCP Option - No-Operation (NOP)
TCP Option - No-Operation (NOP)
TCP Option - Timestamps: TSval 3810596425, TSecr 1878823122
Kind: Time Stamp Option (8)
Length: 10
Timestamp value: 3810596425
Timestamp echo reply: 1878823122

2. TCP timestamp源码解析

2.1 TCP timestamp单位是什么

TCP timestamp的单位本身并不是常规的时间,如微秒、纳秒等等,而是一个与具体时间相关的成比例的相对的虚拟时间单位。

而在具体实现上,在Linux内核中,它与TCP_TS_HZ有关,其值为1000。因此,实际上,Linux内核中的TCP timestamp,即真实时间尺度(s)的1000倍。注意,是尺度,不是时刻。

在Linux内核中,以下为纳秒转换为TCP timestamp时间戳的函数。

div_u64将进行除法运算,并取整。

NSEC_PER_SEC / TCP_TS_HZ表示一个TCP timestamp时间戳计数单位为多少纳秒。

1
2
3
4
5
6
// include/net/tcp.h
/* Convert a nsec timestamp into TCP TSval timestamp (ms based currently) */
static inline u32 tcp_ns_to_ts(u64 ns)
{
return div_u64(ns, NSEC_PER_SEC / TCP_TS_HZ);
}

个人认为,值本身并没有意义,差值才有真实时间尺度下的意义。

2.2 TCP timestamp的发送构造

在TCP三次握手流程中,当客户端发送SYN包时, TCP timestamp将构造在SYN发送报文中,整个过程如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// net/ipv4/tcp_ipv4.c
/* This will initiate an outgoing connection. */
int tcp_v4_connect(struct sock *sk, struct sockaddr *uaddr, int addr_len)
{
// ...
if (likely(!tp->repair)) {
if (!tp->write_seq)
WRITE_ONCE(tp->write_seq,
secure_tcp_seq(inet->inet_saddr,
inet->inet_daddr,
inet->inet_sport,
usin->sin_port));
// 根据源地址与目标地址等输入计算偏移值
tp->tsoffset = secure_tcp_ts_off(sock_net(sk),
inet->inet_saddr,
inet->inet_daddr);
}
// ...
err = tcp_connect(sk); // 构建完整的syn报文,并发送
// ...
}

tcp_connect构建完整的SYN报文,并发送。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// net/ipv4/tcp_output.c

/* Build a SYN and send it off. */
int tcp_connect(struct sock *sk)
{
// ...
// 申请 skb,并构造为一个SYN包
buff = sk_stream_alloc_skb(sk, 0, sk->sk_allocation, true);
if (unlikely(!buff))
return -ENOBUFS;

tcp_init_nondata_skb(buff, tp->write_seq++, TCPHDR_SYN);
// 记录发送时间 tcp_mstamp, 单位 us, 同时记录 tcp_clock_cache
tcp_mstamp_refresh(tp);
tp->retrans_stamp = tcp_time_stamp(tp); // 记录重传时间,单位 TCP timestamp

/* Send off SYN; include data in Fast Open. */
err = tp->fastopen_req ? tcp_send_syn_data(sk, buff) :
tcp_transmit_skb(sk, buff, 1, sk->sk_allocation); // 包的发送
if (err == -ECONNREFUSED)
return err;
// ...
return 0;
}
EXPORT_SYMBOL(tcp_connect);

/* Refresh clocks of a TCP socket,
* ensuring monotically increasing values.
*/
void tcp_mstamp_refresh(struct tcp_sock *tp)
{
u64 val = tcp_clock_ns();

tp->tcp_clock_cache = val; // 将当前时间写入tcp_clock_cache, 单位 ns
tp->tcp_mstamp = div_u64(val, NSEC_PER_USEC);
}

tcp_transmit_skb调用到__tcp_transmit_skb,完成TCP header的构建后,调用ip_queue_xmit,移交IP层处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// net/ipv4/tcp_output.c
static int __tcp_transmit_skb(struct sock *sk, struct sk_buff *skb,
int clone_it, gfp_t gfp_mask, u32 rcv_nxt)
{
// ...
tp = tcp_sk(sk);
prior_wstamp = tp->tcp_wstamp_ns; // 保存之前的时间
tp->tcp_wstamp_ns = max(tp->tcp_wstamp_ns, tp->tcp_clock_cache);
skb->skb_mstamp_ns = tp->tcp_wstamp_ns; // 造包时间,单位 ns
// ...
if (unlikely(tcb->tcp_flags & TCPHDR_SYN)) {
// 建立连接时的SYN包, TCP options构造
tcp_options_size = tcp_syn_options(sk, skb, &opts, &md5);
} else {
// 已连接的TCP连接, TCP options构造
tcp_options_size = tcp_established_options(sk, skb, &opts,
&md5);
// ...
/* BPF prog is the last one writing header option */
// 用BPF技术也可以造TCP options包(开启相关配置)
bpf_skops_write_hdr_opt(sk, skb, NULL, NULL, 0, &opts);
// ...
err = INDIRECT_CALL_INET(icsk->icsk_af_ops->queue_xmit,
inet6_csk_xmit, ip_queue_xmit, // 发送到IP层
sk, skb, &inet->cork.fl);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/* Compute TCP options for SYN packets. This is not the final
* network wire format yet.
*/
static unsigned int tcp_syn_options(struct sock *sk, struct sk_buff *skb,
struct tcp_out_options *opts,
struct tcp_md5sig_key **md5)
{
// ...
if (likely(sock_net(sk)->ipv4.sysctl_tcp_timestamps && !*md5)) {
opts->options |= OPTION_TS;
// 当前时间的TCP timestamp时间单位
// 补偿tsoffset在解析TCP timestamp时会减掉
// tp->tsoffset 下文会阐述
opts->tsval = tcp_skb_timestamp(skb) + tp->tsoffset;
opts->tsecr = tp->rx_opt.ts_recent;
remaining -= TCPOLEN_TSTAMP_ALIGNED;
}
}
static unsigned int tcp_established_options(struct sock *sk, struct sk_buff *skb,
struct tcp_out_options *opts,
struct tcp_md5sig_key **md5)
{
//...
if (likely(tp->rx_opt.tstamp_ok)) {
opts->options |= OPTION_TS;
opts->tsval = skb ? tcp_skb_timestamp(skb) + tp->tsoffset : 0; // 2
opts->tsecr = tp->rx_opt.ts_recent;
size += TCPOLEN_TSTAMP_ALIGNED;
}
//...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// include/net/tcp.h
// 返回TCP timestamp时间单位
static inline u32 tcp_skb_timestamp(const struct sk_buff *skb)
{
return tcp_ns_to_ts(skb->skb_mstamp_ns); // 这个实际就是造包时间,单位 ns
}

static inline u32 tcp_ns_to_ts(u64 ns)
{
return div_u64(ns, NSEC_PER_SEC / TCP_TS_HZ);
}

#define NSEC_PER_SEC 1000000000L
/*
* Deliver a 32bit value for TCP timestamp option (RFC 7323)
* It is no longer tied to jiffies, but to 1 ms clock.
* Note: double check if you want to use tcp_jiffies32 instead of this.
*/
#define TCP_TS_HZ 1000

2.3 TCP timestamp的接收回显

在2.2节中阐述了TCP三次握手中的SYN发送的全过程,因本文主要讲述TCP timestamp,为避免篇幅过程,以下仅描述SYN-ACK与ACK中涉及TCP timestamp的部分。

  • SYN-ACK
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// net/ipv4/tcp_ipv4.c
/*
* Send a SYN-ACK after having received a SYN.
* This still operates on a request_sock only, not on a big
* socket.
*/
static int tcp_v4_send_synack(const struct sock *sk, struct dst_entry *dst,
struct flowi *fl,
struct request_sock *req,
struct tcp_fastopen_cookie *foc,
enum tcp_synack_type synack_type,
struct sk_buff *syn_skb)
{
// ...
// 构造 SYN-ACK包
skb = tcp_make_synack(sk, dst, req, foc, synack_type, syn_skb);
// ...
err = ip_build_and_send_pkt(skb, sk, ireq->ir_loc_addr,
ireq->ir_rmt_addr,
rcu_dereference(ireq->ireq_opt),
tos);
// ...
}

tcp_make_synack中的tcp_synack_options函数中,将构造TCP timestamp。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// net/ipv4/tcp_output.c
/**
* tcp_make_synack - Allocate one skb and build a SYNACK packet.
* @sk: listener socket
* @dst: dst entry attached to the SYNACK. It is consumed and caller
* should not use it again.
* @req: request_sock pointer
* @foc: cookie for tcp fast open
* @synack_type: Type of synack to prepare
* @syn_skb: SYN packet just received. It could be NULL for rtx case.
*/
struct sk_buff *tcp_make_synack(const struct sock *sk, struct dst_entry *dst,
struct request_sock *req,
struct tcp_fastopen_cookie *foc,
enum tcp_synack_type synack_type,
struct sk_buff *syn_skb)
{
// ...
/* bpf program will be interested in the tcp_flags */
TCP_SKB_CB(skb)->tcp_flags = TCPHDR_SYN | TCPHDR_ACK;
// TCP options 构造
tcp_header_size = tcp_synack_options(sk, req, mss, skb, &opts, md5,
foc, synack_type,
syn_skb) + sizeof(*th);
// ...
}

/* Set up TCP options for SYN-ACKs. */
static unsigned int tcp_synack_options(const struct sock *sk,
struct request_sock *req,
unsigned int mss, struct sk_buff *skb,
struct tcp_out_options *opts,
const struct tcp_md5sig_key *md5,
struct tcp_fastopen_cookie *foc,
enum tcp_synack_type synack_type,
struct sk_buff *syn_skb)
{
// ...
if (likely(ireq->tstamp_ok)) {
opts->options |= OPTION_TS;
// 构造 TCP timestamp
opts->tsval = tcp_skb_timestamp(skb) + tcp_rsk(req)->ts_off;
opts->tsecr = req->ts_recent;
remaining -= TCPOLEN_TSTAMP_ALIGNED;
}
// ...
}
  • ACK
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// net/ipv4/tcp_ipv4.c
static void tcp_v4_reqsk_send_ack(const struct sock *sk, struct sk_buff *skb,
struct request_sock *req)
{
tcp_v4_send_ack(sk, skb, seq,
tcp_rsk(req)->rcv_nxt,
req->rsk_rcv_wnd >> inet_rsk(req)->rcv_wscale,
tcp_time_stamp_raw() + tcp_rsk(req)->ts_off, // 构造TCP timestamp
req->ts_recent,
0,
tcp_md5_do_lookup(sk, l3index, addr, AF_INET),
inet_rsk(req)->no_srccheck ? IP_REPLY_ARG_NOSRCCHECK : 0,
ip_hdr(skb)->tos);
}

static void tcp_v4_send_ack(const struct sock *sk,
struct sk_buff *skb, u32 seq, u32 ack,
u32 win, u32 tsval, u32 tsecr, int oif,
struct tcp_md5sig_key *key,
int reply_flags, u8 tos)
{
const struct tcphdr *th = tcp_hdr(skb);
struct {
struct tcphdr th;
__be32 opt[(TCPOLEN_TSTAMP_ALIGNED >> 2)
#ifdef CONFIG_TCP_MD5SIG
+ (TCPOLEN_MD5SIG_ALIGNED >> 2)
#endif
];
} rep;
// ...
// 如果发送者带时间戳,接受者回ack时,也会把数据写到 opt中
if (tsecr) {
rep.opt[0] = htonl((TCPOPT_NOP << 24) | (TCPOPT_NOP << 16) |
(TCPOPT_TIMESTAMP << 8) |
TCPOLEN_TIMESTAMP);
rep.opt[1] = htonl(tsval); // timestamp value
rep.opt[2] = htonl(tsecr); // timestamp echo value
arg.iov[0].iov_len += TCPOLEN_TSTAMP_ALIGNED;
}
// ...
}

3. 如何提取TCP timestamp

3.1 根据TCP head偏移计算

思路:找到tcp header,计算出偏移地址找到options的起始位置。依次遍历option的类型,找到TCPOPT_TIMESTAMP,再计算出tsval和tsecr的偏移以便取出值。

程序基于libbpf开发,源码如下:

查看消息:root用户 cat /sys/kernel/debug/tracing/trace_pipe

关于下述程序源码中的TCPOPT_NOP:

在TCP头部中,nop选项表示为一个字节的值0x01,这个值被称为”无操作码”(no-op code)。当它出现在TCP选项字段中时,它不会执行任何实际的操作,只是被用来占位。如果有多个nop选项,它们连续地填充了TCP头部中的字节,以便满足对齐要求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
SEC("kprobe/tcp_v4_rcv") 
int BPF_KPROBE(tcp_v4_rcv,struct sk_buff *skb,struct sock *sk){
struct tcphdr tcph ;
struct tcphdr *tcph_ptr = skb_to_tcphdr(skb);
bpf_probe_read(&tcph,sizeof(tcph),tcph_ptr);
u16 doff = tcph.doff;
// doff * 4 即 TCP 头的长度
if (tcph_ptr != NULL && doff > 5) {
unsigned char options ;
unsigned char *opt_base = (unsigned char *)(tcph_ptr + 1);
u32 offset = 0;
bpf_probe_read(&options,sizeof(unsigned char),opt_base);
int optlen = (doff * 4) - sizeof(struct tcphdr); // 获取 TCP option 长度
struct tcp_sock *ts;
ts = (struct tcp_sock *)(sk);
// FIXME:目前仅遍历option字段中的前12字节
for (int i = 0; i < 12 && optlen > 0; i++) {
if (options == TCPOPT_NOP) {
offset++;
bpf_probe_read(&options,sizeof(unsigned char),opt_base+offset);
optlen--;
continue;
}
if (options == TCPOPT_TIMESTAMP) {
u32 *tsval_ptr = (u32 *)(opt_base+offset+2);
u32 *tsecr_ptr = NULL;
if (optlen >= TCPOLEN_TIMESTAMP_ALIGNED) {
tsecr_ptr = (u32 *)(opt_base+offset+6);
}
u32 tsval,tsecr;
bpf_probe_read(&tsval,sizeof(u32),tsval_ptr);
bpf_probe_read(&tsecr,sizeof(u32),tsecr_ptr);
tsval = __bpf_ntohl(tsval);
tsecr = __bpf_ntohl(tsecr);
struct tcp_sock *tsock;
tsock = (struct tcp_sock *)BPF_CORE_READ(skb, sk);
bpf_printk("tsval = %u,tsecr = %u",tsval,tsecr);
break;
}
unsigned char tmp;
bpf_probe_read(&tmp,sizeof(unsigned char),opt_base+1);
offset += tmp;
bpf_probe_read(&options,sizeof(unsigned char),opt_base+offset);
optlen -= tmp;
}
}

return 0;
}

程序输出如下:

3.2 直接从tcp_options_received结构体中提取

原理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// include/linux/tcp.h
struct tcp_sock {
// ...
/* RTT measurement */
u64 tcp_mstamp; /* most recent packet received/sent */ // 单位是us
u32 srtt_us; /* smoothed round trip time << 3 in usecs */
u32 mdev_us; /* medium deviation */
u32 mdev_max_us; /* maximal mdev for the last rtt period */
u32 rttvar_us; /* smoothed mdev_max */
u32 rtt_seq; /* sequence number to update rttvar */
struct minmax rtt_min;
// ...
/*
* Options received (usually on last packet, some only on SYN packets).
*/
struct tcp_options_received rx_opt;
// ...
}

tcp_sock结构体中tcp_options_received

  • rcv_tsval(Timestamp value)
  • rcv_tsecr(Timestamp Echo Reply)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// include/linux/tcp.h
struct tcp_options_received {
/* PAWS/RTTM data */
int ts_recent_stamp;/* Time we stored ts_recent (for aging) */
u32 ts_recent; /* Time stamp to echo next */
u32 rcv_tsval; /* Time stamp value */
u32 rcv_tsecr; /* Time stamp echo reply */
u16 saw_tstamp : 1, /* Saw TIMESTAMP on last packet */
tstamp_ok : 1, /* TIMESTAMP seen on SYN packet */
dsack : 1, /* D-SACK is scheduled */
wscale_ok : 1, /* Wscale seen on SYN packet */
sack_ok : 3, /* SACK seen on SYN packet */
smc_ok : 1, /* SMC seen on SYN packet */
snd_wscale : 4, /* Window scaling received from sender */
rcv_wscale : 4; /* Window scaling to send to receiver */
u8 saw_unknown:1, /* Received unknown option */
unused:7;
u8 num_sacks; /* Number of SACK blocks */
u16 user_mss; /* mss requested by user in ioctl */
u16 mss_clamp; /* Maximal mss, negotiated at connection setup */
};

程序基于libbpf开发,源码如下:

查看消息:root用户 cat /sys/kernel/debug/tracing/trace_pipe

1
2
3
4
5
6
7
8
SEC("kprobe/tcp_rcv_established") 
int BPF_KPROBE(tcp_rcv_established,struct sock *sk, struct sk_buff *skb){
struct tcp_sock *tp = tcp_sk(sk);
u32 tsval = BPF_CORE_READ(tp, rx_opt.rcv_tsval);
u32 tsecr = BPF_CORE_READ(tp, rx_opt.rcv_tsecr);
bpf_printk("tsval = %u, tsecr = %u",tsval, tsecr);
return 0;
}

程序输出结果如图所示。

4. 如何计算TCP往返时间(RTT)

4.1 内核是怎么算的

如前所述,TCP timestamp的其中一个作用是为了动态调整RTO,而RTO调整的依据之一就是RTT值,因此Linux内核中也有计算RTT的函数,分别是 tcp_rcv_rtt_measuretcp_rcv_rtt_measure_ts。其中,tcp_rcv_rtt_measure_ts在函数tcp_rcv_established中调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
//net/ipv4/tcp_input.c
static inline void tcp_rcv_rtt_measure(struct tcp_sock *tp)
{
u32 delta_us;

if (tp->rcv_rtt_est.time == 0)
goto new_measure;
if (before(tp->rcv_nxt, tp->rcv_rtt_est.seq))
return;
delta_us = tcp_stamp_us_delta(tp->tcp_mstamp, tp->rcv_rtt_est.time);
if (!delta_us)
delta_us = 1;
tcp_rcv_rtt_update(tp, delta_us, 1);

new_measure:
tp->rcv_rtt_est.seq = tp->rcv_nxt + tp->rcv_wnd;
tp->rcv_rtt_est.time = tp->tcp_mstamp;
}

static inline void tcp_rcv_rtt_measure_ts(struct sock *sk,
const struct sk_buff *skb)
{
struct tcp_sock *tp = tcp_sk(sk);

if (tp->rx_opt.rcv_tsecr == tp->rcv_rtt_last_tsecr)
return;
tp->rcv_rtt_last_tsecr = tp->rx_opt.rcv_tsecr;

if (TCP_SKB_CB(skb)->end_seq -
TCP_SKB_CB(skb)->seq >= inet_csk(sk)->icsk_ack.rcv_mss) {
// step 1 TCP timestamp 尺度间隔时间
u32 delta = tcp_time_stamp(tp) - tp->rx_opt.rcv_tsecr;
u32 delta_us;

if (likely(delta < INT_MAX / (USEC_PER_SEC / TCP_TS_HZ))) {
if (!delta)
delta = 1;
// step 2, 真实时间 us: delta / TCP_TS_HZ * USEC_PER_SEC
delta_us = delta * (USEC_PER_SEC / TCP_TS_HZ);
tcp_rcv_rtt_update(tp, delta_us, 0);
}
}
}

step 1:

  • USEC_PER_SEC = 1000000L
  • TCP_TS_HZ = 1000
  • u64 tcp_mstamp;

tcp_time_stamp返回的是:tp->tcp_mstamp / (USEC_PER_SEC / TCP_TS_HZ)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// include/net/tcp.h
/* This should only be used in contexts where tp->tcp_mstamp is up to date */
static inline u32 tcp_time_stamp(const struct tcp_sock *tp) {
return div_u64(tp->tcp_mstamp, USEC_PER_SEC / TCP_TS_HZ);
}
// include/linux/math64.h
#ifndef div_u64
static inline u64 div_u64(u64 dividend, u32 divisor)
{
u32 remainder;
return div_u64_rem(dividend, divisor, &remainder);
}
#endif
// include/linux/math64.h
static inline u64 div_u64_rem(u64 dividend, u32 divisor, u32 *remainder)
{
*remainder = dividend % divisor;
return dividend / divisor; // 取整
}

4.2 用户态直接计算

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// include/uapi/linux/tcp.h
// 内核
struct tcp_info {
// ...
__u32 tcpi_rto;
// ...
/* Times. */
__u32 tcpi_last_data_sent;
__u32 tcpi_last_ack_sent; /* Not remembered, sorry. */
__u32 tcpi_last_data_recv;
__u32 tcpi_last_ack_recv;
// ...
__u32 tcpi_rcv_rtt; // RTT值
__u32 tcpi_rcv_space;
// ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// /usr/include/netinet/tcp.h
// 系统,GLIBC
struct tcp_info
{ /* Metrics. */
uint32_t tcpi_pmtu;
uint32_t tcpi_rcv_ssthresh;
uint32_t tcpi_rtt;
uint32_t tcpi_rttvar;
uint32_t tcpi_snd_ssthresh;
uint32_t tcpi_snd_cwnd;
uint32_t tcpi_advmss;
uint32_t tcpi_reordering;

uint32_t tcpi_rcv_rtt;
};

例如,在C++网络库muduo中,有以下代码,可以打印出上述tcp_Info中的tcpi_rtt值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// muduo/net/Socket.cc

#include <netinet/in.h>
#include <netinet/tcp.h>
#include <stdio.h> // snprintf

bool Socket::getTcpInfo(struct tcp_info* tcpi) const
{
socklen_t len = sizeof(*tcpi);
memZero(tcpi, len);
return ::getsockopt(sockfd_, SOL_TCP, TCP_INFO, tcpi, &len) == 0;
}

bool Socket::getTcpInfoString(char* buf, int len) const
{
struct tcp_info tcpi;
bool ok = getTcpInfo(&tcpi);
if (ok)
{
snprintf(buf, len, "unrecovered=%u "
"rto=%u ato=%u snd_mss=%u rcv_mss=%u "
"lost=%u retrans=%u rtt=%u rttvar=%u "
"sshthresh=%u cwnd=%u total_retrans=%u",
tcpi.tcpi_retransmits, // Number of unrecovered [RTO] timeouts
tcpi.tcpi_rto, // Retransmit timeout in usec
tcpi.tcpi_ato, // Predicted tick of soft clock in usec
tcpi.tcpi_snd_mss,
tcpi.tcpi_rcv_mss,
tcpi.tcpi_lost, // Lost packets
tcpi.tcpi_retrans, // Retransmitted packets out
tcpi.tcpi_rtt, // Smoothed round trip time in usec
tcpi.tcpi_rttvar, // Medium deviation
tcpi.tcpi_snd_ssthresh,
tcpi.tcpi_snd_cwnd,
tcpi.tcpi_total_retrans); // Total retransmits for entire connection
}
return ok;
}

4.3 使用ss工具

使用ss -ti

1
2
3
4
5
6
7
8
fzy@fzy-Lenovo:~$ ss -ti
State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
ESTAB 0 0 127.0.0.1:59164 127.0.0.1:7890
cubic wscale:7,7
rto:204
rtt:0.881/1.622 # RTT平均值及中位数
minrtt:0.019
# ...

4.4 使用BCC tcprtt工具

BCC提供了tcprtt工具,并有python与基于libbpf两个版本的程序。其中基于libbpf的tcprtt工具在libbpf-tools文件夹下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
fzy@fzy-Lenovo:~/Downloads/04_bcc_ebpf/bcc/libbpf-tools$ sudo ./tcprtt 
[sudo] password for fzy:
Tracing TCP RTT... Hit Ctrl-C to end.
^C
All Addresses = ******
usecs : count distribution
0 -> 1 : 0 | |
2 -> 3 : 0 | |
4 -> 7 : 0 | |
8 -> 15 : 0 | |
16 -> 31 : 0 | |
32 -> 63 : 3 |********* |
64 -> 127 : 5 |*************** |
128 -> 255 : 1 |*** |
256 -> 511 : 0 | |
512 -> 1023 : 9 |*************************** |
1024 -> 2047 : 1 |*** |
2048 -> 4095 : 8 |************************ |
4096 -> 8191 : 4 |************ |
8192 -> 16383 : 5 |*************** |
16384 -> 32767 : 0 | |
32768 -> 65535 : 13 |****************************************|

原理:

tcp_rcv_established结构体中(结构体定义见3.2节),采集tcp_sock中的srtt_us

1
2
3
4
5
6
7
8
9
10
11
12
// bcc/.../tcprtt.bpf.c
SEC("kprobe/tcp_rcv_established")
int BPF_KPROBE(tcp_rcv_kprobe, struct sock *sk)
{
// ...
u32 srtt, saddr, daddr;
// ...
ts = (struct tcp_sock *)(sk);
bpf_probe_read_kernel(&srtt, sizeof(srtt), &ts->srtt_us);
srtt >>= 3; // 单位:us
// ...
}

4.5 eBPF提取srtt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
SEC("kprobe/tcp_rcv_established")
int BPF_KPROBE(tcp_rcv_established, struct sock *sk,struct sk_buff *skb)
{
if (!_is_send) {
if (!_is_ipv6) {
if (skb == NULL)
return 0;
struct iphdr *ip = skb_to_iphdr(skb);
struct tcphdr *tcp = skb_to_tcphdr(skb);
struct packet_tuple pkt_tuple = {};
get_pkt_tuple(&pkt_tuple, ip, tcp);

SAMPLING
FILTER_DPORT
FILTER_SPORT

struct ktime_info *tinfo;
if ((tinfo = bpf_map_lookup_elem(&in_timestamps,&pkt_tuple)) == NULL){
return 0;
}
struct tcp_sock *ts;
u32 srtt;
ts = (struct tcp_sock *)(sk);
srtt = BPF_CORE_READ(ts, srtt_us);
tinfo->srtt = srtt;
}
}
return 0;

}

运行结果:

5. 参考资料

  1. 深入理解TCP协议(下):RTT、滑动窗口、拥塞处理
  2. TCP中RTT时延的理解
  3. TCP timestamp
  4. Measuring TCP Congestion Windows
  5. tcp 协议小结
  6. TCP timestamp 选项那点事

6.关于一些TCP知识的补充

  • 关于 DOFF

在TCP协议中,DOFF是一个代表TCP头部长度的字段。TCP报文头部的长度是可变长度的,因为TCP头部中可以包括选项,而选项的长度是不固定的。因此,通过DOFF字段来指示TCP头部中实际的长度,以便接收方向正确位置解析TCP数据包。

TCP头部总长度是由DOFF和TCP选项共同决定的,DOFF字段的长度是一个4位二进制数字,可以最多表示15个单位的长度。因此,TCP头部的最大长度为60个字节(15*4)。在TCP报文中,DOFF值代表TCP头部的长度,而TCP数据则紧随TCP头部之后。

该字段占据TCP头部的第一个字节的高4位,表示TCP头部的长度,也就是TCP头部的首部长度(Header length),由前四位二进制数值决定,用于解析TCP数据报。

例如,DOFF=5,表示TCP头部的长度为20个字节(5 x 4),而TCP头部中实际长度为20个字节,加上选项长度等于TCP头部的总长度。

  • 关于TCP option的常见选项
options 释义
TCPOLEN_MSS MSS(最大数据段长度)选项长度为4个字节
TCPOLEN_WINDOW 窗口选项长度为3个字节
TCPOLEN_SACK_PERM SACK_PERM(选择性确认)选项长度为2个字节
TCPOLEN_TIMESTAMP 时间戳选项长度为10个字节
TCPOLEN_MD5SIG 使用MD5对TCP报文进行数字签名时选项长度为18个字节
TCPOLEN_FASTOPEN_BASE Fast Open选项长度为2个字节
TCPOLEN_EXP_FASTOPEN_BASE 扩展的Fast Open选项长度为4个字节
TCPOLEN_EXP_SMC_BASE 扩展的SMC(Server Message Channel)选项长度为6个字节
TCPOLEN_TSTAMP_ALIGNED 时间戳选项(timestamp option)按照四字节对齐后的长度为12个字节
TCPOLEN_WSCALE_ALIGNED 窗口比例选项(window scale option)按照四字节对齐后的长度为4个字节
TCPOLEN_SACKPERM_ALIGNED 选择性确认允许选项(SACK permit option)按照四字节对齐后的长度为4个字节
TCPOLEN_SACK_BASE 每个SACK块(selective acknowledgment block)的长度为2个字节
TCPOLEN_SACK_BASE_ALIGNED 按照四字节对齐后的每个SACK块的长度为4个字节
TCPOLEN_SACK_PERBLOCK 每个SACK块中最多可以包含八个块(selective acknowledgment per-block option),按照八字节对齐后的长度为8个字节
TCPOLEN_MD5SIG_ALIGNED 使用MD5对TCP报文进行数字签名时选项按照四字节对齐后的长度为20个字节
TCPOLEN_MSS_ALIGNED 最大报文长度(maximum segment size)选项按照四字节对齐后的长度为4个字节
TCPOLEN_EXP_SMC_BASE_ALIGNED 扩展服务器消息通道选项(extended server message channel option)按照四字节对齐后的长度为8个字节

关于TCP_options中的timestamps
http://ziyangfu.github.io/2023/05/05/关于TCP-options中的timestamps/
作者
FZY
发布于
2023年5月5日
许可协议