关于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 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中。
接下来使用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 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 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); }
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 int tcp_connect (struct sock *sk) { 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_refresh(tp); tp->retrans_stamp = tcp_time_stamp(tp); 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);void tcp_mstamp_refresh (struct tcp_sock *tp) { u64 val = tcp_clock_ns(); tp->tcp_clock_cache = val; 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 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; if (unlikely(tcb->tcp_flags & TCPHDR_SYN)) { tcp_options_size = tcp_syn_options(sk, skb, &opts, &md5); } else { tcp_options_size = tcp_established_options(sk, skb, &opts, &md5); 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, 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 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; 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 ; 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 static inline u32 tcp_skb_timestamp (const struct sk_buff *skb) { return tcp_ns_to_ts(skb->skb_mstamp_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 #define TCP_TS_HZ 1000
2.3 TCP timestamp的接收回显 在2.2节中阐述了TCP三次握手中的SYN发送的全过程,因本文主要讲述TCP timestamp,为避免篇幅过程,以下仅描述SYN-ACK与ACK中涉及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 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) { 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 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) { TCP_SKB_CB(skb)->tcp_flags = TCPHDR_SYN | TCPHDR_ACK; tcp_header_size = tcp_synack_options(sk, req, mss, skb, &opts, md5, foc, synack_type, syn_skb) + sizeof (*th); }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; opts->tsval = tcp_skb_timestamp(skb) + tcp_rsk(req)->ts_off; opts->tsecr = req->ts_recent; remaining -= TCPOLEN_TSTAMP_ALIGNED; } }
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 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, 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; if (tsecr) { rep.opt[0 ] = htonl((TCPOPT_NOP << 24 ) | (TCPOPT_NOP << 16 ) | (TCPOPT_TIMESTAMP << 8 ) | TCPOLEN_TIMESTAMP); rep.opt[1 ] = htonl(tsval); rep.opt[2 ] = htonl(tsecr); 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; 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); struct tcp_sock *ts ; ts = (struct tcp_sock *)(sk); 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 struct tcp_sock { u64 tcp_mstamp; u32 srtt_us; u32 mdev_us; u32 mdev_max_us; u32 rttvar_us; u32 rtt_seq; struct minmax rtt_min ; 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 struct tcp_options_received { int ts_recent_stamp; u32 ts_recent; u32 rcv_tsval; u32 rcv_tsecr; u16 saw_tstamp : 1 , tstamp_ok : 1 , dsack : 1 , wscale_ok : 1 , sack_ok : 3 , smc_ok : 1 , snd_wscale : 4 , rcv_wscale : 4 ; u8 saw_unknown:1 , unused:7 ; u8 num_sacks; u16 user_mss; u16 mss_clamp; };
程序基于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_measure与tcp_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 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) { 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 ; 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 static inline u32 tcp_time_stamp (const struct tcp_sock *tp) { return div_u64(tp->tcp_mstamp, USEC_PER_SEC / TCP_TS_HZ); } #ifndef div_u64 static inline u64 div_u64 (u64 dividend, u32 divisor) { u32 remainder; return div_u64_rem(dividend, divisor, &remainder); }#endif 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 struct tcp_info { __u32 tcpi_rto; __u32 tcpi_last_data_sent; __u32 tcpi_last_ack_sent; __u32 tcpi_last_data_recv; __u32 tcpi_last_ack_recv; __u32 tcpi_rcv_rtt; __u32 tcpi_rcv_space; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 struct tcp_info { 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 #include <netinet/in.h> #include <netinet/tcp.h> #include <stdio.h> 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, tcpi.tcpi_rto, tcpi.tcpi_ato, tcpi.tcpi_snd_mss, tcpi.tcpi_rcv_mss, tcpi.tcpi_lost, tcpi.tcpi_retrans, tcpi.tcpi_rtt, tcpi.tcpi_rttvar, tcpi.tcpi_snd_ssthresh, tcpi.tcpi_snd_cwnd, tcpi.tcpi_total_retrans); } 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 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 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 ; }
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. 参考资料
深入理解TCP协议(下):RTT、滑动窗口、拥塞处理
TCP中RTT时延的理解
TCP timestamp
Measuring TCP Congestion Windows
tcp 协议小结
TCP timestamp 选项那点事
6.关于一些TCP知识的补充
在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头部的总长度。
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个字节