tcp 中使用的定时器
定时器的使用场景主要有两种。
(1)周期性任务
这是定时器最常用的一种场景,比如 tcp 中的 keepalive 定时器,起到 tcp 连接的两端保活的作用,周期性发送数据包,如果对端回复报文,说明对端还活着;如果对端不回复数据包,就会判定对端已经不存在了;再比如分布式系统中,各个组件之间的心跳报文也是定时发送来维护组件之间的状态。
(2)兜底功能
一些不立即执行的任务的时间底线。比如 tcp 中的延迟 ack 功能,说的就是在接收到一个报文的时候,并不会立即向对方回复 ack,而是会看看本端最近是不是会发送报文,如果是的话,那么 ack 就跟随这个报文一块发送, 这样可以减少链路上的报文数量,提高带宽利用率。如果本端很长时间内没有数据发向对端呢,当前这个线程不会一直在这里等待,而是使用一个定时器来完成后边的工作,也就是说最多可以等待多长时间,即等待的底线,如果超过这个底线之后还没有等到发送数据,那么这个定时器就会直接将 ack 发送出去。重传定时器,0 窗口探测定时器,也起到了兜底的作用。定时器通过异步的方式解放了线程,有了定时器就不需要线程在这里等待。
tcp 中使用的定时器有多个,本文主要有介绍以下 6 个。6 个定时器可以按照 tcp 连接的生命周期进行划分,划分结果如下表所示:
定时器分类 | 定时器 | 定时器成员 | 所在结构体 | 超时处理函数 |
建立连接过程 | syn + ack 定时器 | rsk_timer | struct request_sock | reqsk_timer_handler() |
数据传输过程 | 重传定时器 | icsk_retransmit_timer | struct inet_connection_sock | tcp_retransmit_timer() |
延时 ack 定时器 | icsk_delack_timer | struct inet_connection_sock | tcp_delack_timer() | |
保活定时器 | sk_timer | struct sock | tcp_keepalive_timer() | |
窗口探测定时器 | icsk_retransmit_timer | struct inet_connection_sock | tcp_probe_timer() | |
断开连接过程 | TIME_WAIT 定时器 | tw_timer | struct inet_timewait_sock | tw_timer_handler() |
不同的定时器,维护的 socket 是不一样的。syn + ack 定时器在 struct request_sock 中维护,TIME_WAIT 定时器在 struct inet_timewait_sock 中维护,这两个定时器也是只在建立连接阶段或断开连接阶段存在,并且前者是服务端需要使用的定时器,后者是主动断开连接的一方需要使用的定时器,并不是连接的每一端都需要。数据传输过程中使用的定时器是连接的两端都要使用到的定时器。
1 连接建立过程定时器
1.1 syn 定时器
在介绍 syn + ack 定时器之前,先介绍一下 syn 定时器。顾名思义,syn 定时器就是重传 syn 包的定时器。之所以上边表格中没有单独列出来 syn 定时器,是因为 syn 定时器就是重传定时器。
syn 定时器即发起连接的一方(客户端),发送 syn 包之后,会启动一个定时器,这个定时器和后边讲的连接建立完成之后的重传定时器是同一个定时器。作用也是一样的,即发送 syn 包之后,如果在超时时间之内没有收到 syn + ack 报文,便会重传 syn 包。
发送 syn 包和启动定时器的工作在 tcp_connect() 函数中完成。这个定时器只有客户端才需要使用,所以不是在 socket 的初始化函数中创建的,而是在 tcp_connect() 函数中创建的。
syn 包最大重传次数可通过 /proc/sys/net/ipv4/tcp_syn_retries 配置,默认是 6。
int tcp_connect(struct sock *sk)
{struct sk_buff *buff;// 构造一个 syn 报文tcp_init_nondata_skb(buff, tp->write_seq++, TCPHDR_SYN);// 将报文放入重传队列中,重传队列使用红黑树来维护tcp_rbtree_insert(&sk->tcp_rtx_queue, buff);// 发送 syn 包err = tp->fastopen_req ?tcp_send_syn_data(sk, buff) :tcp_transmit_skb(sk, buff, 1, sk->sk_allocation);// 启动重传定时器inet_csk_reset_xmit_timer(sk, ICSK_TIME_RETRANS, inet_csk(sk)->icsk_rto,TCP_RTO_MAX);return 0;
}
1.2 syn + ack 定时器
服务端收到 syn 之后,便会进行第二次握手,即发送 syn + ack 报文。syn + ack 定时器和 syn 定时器的作用类似,也是检测发送 syn + ack 报文之后,在一定时间内有没有收到第三次握手的 ack 报文,如果没有收到,该定时器超时之后便会重传 syn + ack。
syn + ack 定时器的创建调用栈是 :
tcp_conn_request()
调用
inet_csk_reqsk_queue_hash_add()
调用
reqsk_queue_hash_req()
当服务端收到 syn 报文时,说明有新的连接请求,该请求在函数 tcp_conn_request() 中处理,在该函数中的主要工作有三个:
① 申请一个 struct request_sock,然后将之加入到 ehash 中,便于第三次握手到来之后查找到这个套接字
② 向对端发送 syn + ack 报文,即第二次握手
③ 启动 syn + ack 定时器
syn + ack 报文,同样也有最大重传次数限制,可以通过配置 /proc/sys/net/ipv4/tcp_synack_retries 进行修改,默认是 5。
2 数据传输过程中的定时器
ESTABLISHED 状态下的定时器包括重传定时器,延迟 ack 定时器,窗口探测定时器(又叫坚持定时器)以及保活定时器。这四个定时器在函数 tcp_init_xmit_timers() 创建,该函数被 tcp_init_sock() 调用,也就是说不管是客户端还是服务端都会创建这四个定时器。
void tcp_init_xmit_timers(struct sock *sk)
{// 创建三个定时器,分别是重传定时器,延时 ack 定时器,保活定时器// 三个定时器的超时处理函数即后三个入参inet_csk_init_xmit_timers(sk, &tcp_write_timer, &tcp_delack_timer,&tcp_keepalive_timer);...
}
2.1 重传定时器
重传定时器,简单来说就是发送侧发送一个报文之后,就启动一个定时器等接收方的 ack,如果超时没有等到 ack,那么发送方就会认为发生了丢包,然后会重新发送这个报文;反之,如果在超时时间内收到了对端回应的 ack, 说明接收侧已经收到了这个报文,发送侧就可以放心地把这个报文从重传队列中取出,然后释放报文占用的资源了。
重传定时器示意图如下,发送方发送报文序列号 1000,长度为 200,发送之后便会启动重传定时器。正常情况下,在定时器超时之前,接收方会返回 ack,如果定时器超时的时候没有收到 ack,发送方便会认为这个报文丢失,从而会重传这个报文。
(1)什么时候启动重传定时器 ?
发包路径
// 函数 tcp_write_xmit() 中会调用 tcp_transmit_skb() 进行发包
// 如果 tcp_transmit_skb() 返回成功,则调用函数 tcp_event_new_data_sent()
// 在函数 tcp_event_new_data_sent() 中将报文放入重传队列中,同时启动重传定时器
static void tcp_event_new_data_sent(struct sock *sk, struct sk_buff *skb)
{ // packets_out 表示发送出去,但是还没有收到 ack 的报文// 在该函数的后边会更新这个变量,把刚发送的报文加上去// 当收到 ack 报文的时候会对这个变量做减法unsigned int prior_packets = tp->packets_out;// 更新 snd_nxtWRITE_ONCE(tp->snd_nxt, TCP_SKB_CB(skb)->end_seq);// 将 skb 从发送队列中移除,然后将 skb 放入重传队列// 报文发向 ip 层成功之后并不能立即释放 skb, 因为报文在链路上可能会丢失 // 所以先将报文移入重传队列,如果这个报文在链路上丢了的话还可重传// 只有收到这个报文的 ack 时,说明接收侧已经收到了这个报文// 这个时候才可以将报文从重传队列中移除,释放 skb 资源__skb_unlink(skb, &sk->sk_write_queue);tcp_rbtree_insert(&sk->tcp_rtx_queue, skb);// 更新 packets_outtp->packets_out += tcp_skb_pcount(skb);// prior_packets 即不包括这次发送的报文,之前发送出去但是还没有确认的报文// 如果都已经确认了,说明重传定时器这个时候没有工作,需要启动重传定时器// 如果还有没被确认的,说明上次发包的时候就已经启动了重传定时器,并且没有超时// 这种情况下就不需要再次启动重传定时器了// 具体启动重传定时器的工作在 tcp_rearm_rto() 中完成if (!prior_packets || icsk->icsk_pending == ICSK_TIME_LOSS_PROBE)tcp_rearm_rto(sk);
}// 函数 tcp_rearm_rto() 中首先计算 rto,即重传定时器的超时时间
// 由此可见重传定时器的超时时间不是固定不变的,而是和链路状态有关系
// 计算 rto 之后便会通过函数 tcp_reset_xmit_timer() 启动重传定时器
void tcp_rearm_rto(struct sock *sk)
{// 如果 packets_out 是 0,说明发送出去的报文已经全部确认,则可以停掉重传定时器if (!tp->packets_out) {inet_csk_clear_xmit_timer(sk, ICSK_TIME_RETRANS);} else {u32 rto = inet_csk(sk)->icsk_rto;/* Offset the time elapsed after installing regular RTO */if (icsk->icsk_pending == ICSK_TIME_REO_TIMEOUT ||icsk->icsk_pending == ICSK_TIME_LOSS_PROBE) {s64 delta_us = tcp_rto_delta_us(sk);/* delta_us may not be positive if the socket is locked* when the retrans timer fires and is rescheduled.*/rto = usecs_to_jiffies(max_t(int, delta_us, 1)); }tcp_reset_xmit_timer(sk, ICSK_TIME_RETRANS, rto,TCP_RTO_MAX);}
}
(2) 收到 ack 报文的时候如何改变重传定时器
收到 ack 报文之后,如果发现发送的报文都已经被确认,那么就会停掉重传定时器;否则,则会重启重传定时器。
// tcp_ack() 函数处理接收到的 ack 报文
// tcp_ack() 函数中调用 tcp_clean_rtx_queue() 来将已经 ack 的报文从重传队列中移除,
// 同时对 tp->packets_out 做减法
// tcp_clean_rtx_queue() 中会判断是不是有新的报文被确认,
// 如果是,则返回的 flag 中包含 FLAG_SET_XMIT_TIMER 标志
// 在 tcp_ack() 中就会重置重传定时器
static int tcp_ack(struct sock *sk, const struct sk_buff *skb, int flag)
{// 如果有新的数据被确认,则返回的 flag 中带有标志 FLAG_SET_XMIT_TIMERflag |= tcp_clean_rtx_queue(sk, skb, prior_fack, prior_snd_una,&sack_state, flag & FLAG_ECE);// FLAG_SET_XMIT_TIMER 这个标志说明有数据被确认,// 这种情况下就需要重新设置重传定时器// tcp_set_xmit_timer() 最终会调用到 tcp_rearm_rto()// 在 tcp_rearm_rto() 中判断,// 如果发送出去的报文都已经确认,则停止重传定时器,否则 reset 重传定时器if (flag & FLAG_SET_XMIT_TIMER)tcp_set_xmit_timer(sk);
}
(3) 重传定时器回调函数中如何重传 ?
重传定时器超时,最终会调用函数 tcp_retransmit_timer() 进行重传。在该函数中主要做的工作有三个:
① 从重传队列中取出第一个报文,进行重传。
② 重传之前要判断,重传次数是不是已经达到最大值,如果达到最大值,则放弃重传,设置套接字为错误状态。重传次数并不是无限的,而是有最大值限制。放弃重传的判断条件有两个,分别是时间维度和数量维度,函数 tcp_write_timeout() 中进行具体判断。
③ 发生重传说明存在丢包,这种情况下进入 loss 状态。
void tcp_retransmit_timer(struct sock *sk)
{struct tcp_sock *tp = tcp_sk(sk);struct net *net = sock_net(sk);struct inet_connection_sock *icsk = inet_csk(sk);struct request_sock *req;struct sk_buff *skb;// tp->packets_out 为 0,说明发送的报文都已经 ack 了// 没有报文需要重传,直接 returnif (!tp->packets_out)return;// 从重传队列中取出第一个报文skb = tcp_rtx_queue_head(sk);if (WARN_ON_ONCE(!skb))return;// 判断重传是否超时,如果超时,则将套接字设置为错误状态,然后退出// 将套接字设置为错误状态通过函数 tcp_write_err() 完成// 重传采用退避策略,重传定时器超时时间倍数增长// 最小重传时间是 0.5s,最大是 120s,由下边两个宏来定义// #define TCP_RTO_MAX ((unsigned)(120*HZ))// #define TCP_RTO_MIN ((unsigned)(HZ/5))if (tcp_write_timeout(sk))goto out;// 进入 loss 状态tcp_enter_loss(sk);// 重传报文icsk->icsk_retransmits++;if (tcp_retransmit_skb(sk, tcp_rtx_queue_head(sk), 1) > 0) {/* Retransmission failed because of local congestion,* Let senders fight for local resources conservatively.*/inet_csk_reset_xmit_timer(sk, ICSK_TIME_RETRANS,TCP_RESOURCE_PROBE_INTERVAL,TCP_RTO_MAX);goto out;}out_reset_timer:// 计算下次重传超时时间并重置重传定时器if (sk->sk_state == TCP_ESTABLISHED &&(tp->thin_lto || net->ipv4.sysctl_tcp_thin_linear_timeouts) &&tcp_stream_is_thin(tp) &&icsk->icsk_retransmits <= TCP_THIN_LINEAR_RETRIES) {icsk->icsk_backoff = 0;icsk->icsk_rto = min(__tcp_set_rto(tp), TCP_RTO_MAX);} else {/* Use normal (exponential) backoff */icsk->icsk_rto = min(icsk->icsk_rto << 1, TCP_RTO_MAX);}inet_csk_reset_xmit_timer(sk, ICSK_TIME_RETRANS,tcp_clamp_rto_to_user_timeout(sk),TCP_RTO_MAX);
out:;
}
2.2 延时 ack 定时器
当接收到数据之后,并不一定是立即发送 ack。而是等待一段时间,如果在这段时间之内,有发往对方的数据,则 ack 随着该数据一块发送;如果在超时时间之内,没有数据发往对方,则在定时器回调函数中单独发送 ack。
延时 ack 也叫捎带 ack,相比于收到一个报文之后就立即发送 ack,延时 ack 可以减少链路上纯 ack 报文的比例,提高网络带宽利用率。
接收侧收到数据之后会调用函数 __tcp_ack_snd_check(),在这个函数中判断是不是需要立即发送 ack,如果需要立即发送 ack,则立即发送;否则的话,如果满足发送延时 ack 的条件,则调用函数 tcp_send_delayed_ack() 进行发送延时 ack 的逻辑。延时 ack 定时器的最小超时时间是 40ms, 最大超时时间是 200ms,分别用宏 TCP_DELACK_MIN 和 TCP_DELACK_MAX 来定义。
static void __tcp_ack_snd_check(struct sock *sk, int ofo_possible)
{struct tcp_sock *tp = tcp_sk(sk);unsigned long rtt, delay;// 收到的报文大于 mss// 或者设置了 quick ack// 或者设置了 ICSK_ACK_NOW// 直接发送 ackif (((tp->rcv_nxt - tp->rcv_wup) > inet_csk(sk)->icsk_ack.rcv_mss &&(tp->rcv_nxt - tp->copied_seq < sk->sk_rcvlowat ||__tcp_select_window(sk) >= tp->rcv_wnd)) ||tcp_in_quickack_mode(sk) ||/* Protocol state mandates a one-time immediate ACK */inet_csk(sk)->icsk_ack.pending & ICSK_ACK_NOW) {send_now:tcp_send_ack(sk);return;}// 延时 ack 的逻辑在 tcp_send_delayed_ack() 中进行处理if (!ofo_possible || RB_EMPTY_ROOT(&tp->out_of_order_queue)) {tcp_send_delayed_ack(sk);return;}...
}
如果想要接收到报文之后就立即发送 ack,那么需要设置 socket 选项 TCP_QUICKACK。socket 选项中,有一些设置之后就会一直生效,比如 SO_RCVTIMEO 选项,可以设置接收数据的超时时间,如果阻塞这么长时间,数据还没有到来,那么 recv() 就会返回。还有一些选项,设置之后并不是一直生效,比如 TCP_QUICKACK,设置之后,就会立即回应 ack,但是这个选项并不一定一直生效,还会受到 tcp 协议栈内部判断的影响,所以需要每次收到数据之后都重新设置一次这个选项。
2.3 窗口探测定时器
在建立 tcp 连接时,两端会向对方通告自己的接收窗口大小。接收窗口用于流量控制,tcp 发送数据时不能超过对端接收窗口的大小。
如果出现发送方的发送速度大于接收方的接收速度,或者接收侧应用长时间没有从接收缓冲区接收数据的时候,接收窗口会变成 0,并将 0 窗口通知给发送方,发送方便会停止发送数据。
当接收方的窗口从 0 变为非 0 时,便会向对端发送 ack 报文,通告窗口的大小。对端收到该报文后,知道接收窗口不是 0 了,便会开始发送数据。
当通知报文在链路上丢失了, 会进行重传吗 ?不会重传。如果该报文丢失了,那么连接的两端就会死锁(发送方仍然认为接收窗口是 0,停止发送数据;接收方认为自己的通知报文已经发送出去了,已经通知了对方,自己责任已经完成),数据传输不会开启。
窗口探测定时器的作用就是应对死锁情况的补偿措施。发送方会定期发送探测报文,接收方收到探测报文之后便会回复 ack 报文,该 ack 报文同时也包含窗口信息。窗口探测定时器直到收到窗口非 0 的 ack 之后才会停止。这样就保证了即使两端发送死锁,定时器也能探测到窗口非 0 的情况,起到了兜底的作用。
窗口字段在 tcp 首部,接收侧收到报文之后,便会基于该字段更新本端发送窗口。
当 tcp 接收到 ack 报文之后,会通过函数 tcp_ack_update_window() 更新发送窗口,snd_wnd 是发送窗口,发送报文的时候会进行检查,发送的数据不会大于发送窗口。
static int tcp_ack_update_window(struct sock *sk, const struct sk_buff *skb,u32 ack, u32 ack_seq)
{struct tcp_sock *tp = tcp_sk(sk);int flag = 0;u32 nwin = ntohs(tcp_hdr(skb)->window);// 窗口扩展因子if (likely(!tcp_hdr(skb)->syn))nwin <<= tp->rx_opt.snd_wscale;if (tcp_may_update_window(tp, ack, ack_seq, nwin)) {flag |= FLAG_WIN_UPDATE;tcp_update_wl(tp, ack_seq);// 更新发送窗口if (tp->snd_wnd != nwin) {tp->snd_wnd = nwin;}}return flag;
}
2.3.1 定时器什么时候启动
窗口探测定时器在发送路径上启动。
void __tcp_push_pending_frames(struct sock *sk, unsigned int cur_mss,int nonagle)
{// tcp_write_xmit() 返回 true, 说明这次调用没有发送任何报文// 则调用 tcp_check_probe_timer() 进行判断,需不需要开启窗口探测定时器if (tcp_write_xmit(sk, cur_mss, nonagle, 0,sk_gfp_mask(sk, GFP_ATOMIC)))tcp_check_probe_timer(sk);
}// 判断两个条件,如果这两个条件均满足,则开启窗口探测定时器
// 条件一:所有发送的数据都 ack 了
// 只有这个条件满足,才会开启定时器,因为如果现在还有发送的数据没有被 ack,
// 那么不需要定时器来探测,因为 ack 很快就会来了,ack 中带有窗口信息
//
// 条件二:窗口探测定时器没有启动。
static inline void tcp_check_probe_timer(struct sock *sk)
{if (!tcp_sk(sk)->packets_out && !inet_csk(sk)->icsk_pending)tcp_reset_xmit_timer(sk, ICSK_TIME_PROBE0,tcp_probe0_base(sk), TCP_RTO_MAX);
}
2.3.2 定时器回调函数做什么工作 ?
窗口探测定时器超时之后调用函数 tcp_probe_timer(),在该函数中发送一个特殊的报文,对端收到该报文后便会回一个 ack,通过 ack 便可知道对端的窗口是不是已经变成非 0。
那么窗口探测报文有什么特殊之处呢 ?
特殊之处在序号,序号是已经 ack 的报文。假如本端收到的最后一个 ack 是 1000, 下一个要发送的字节序号是 1000,而窗口探测报文发送的序列号是 999。
tcp_probe_timer() 发送 0 窗口探测报文:
static void tcp_probe_timer(struct sock *sk)
{struct inet_connection_sock *icsk = inet_csk(sk);struct sk_buff *skb = tcp_send_head(sk);struct tcp_sock *tp = tcp_sk(sk);int max_probes;// tp->packets_out 是已发送,但是还没有 ack 的包的个数// 如果这个数不是 0,说明最近会收到 ack,或者收不到 ack 就会重传// 不需要窗口探测报文来探测// !skb 说明 skb 是空,当前没有要发送的数据// 这种情况下,也不需要探测窗口,直接返回if (tp->packets_out || !skb) {icsk->icsk_probes_out = 0;icsk->icsk_probes_tstamp = 0;return;}// 最大重传次数max_probes = sock_net(sk)->ipv4.sysctl_tcp_retries2;// 如果达到最大重传次数,则关闭连接if (icsk->icsk_probes_out >= max_probes) {abort:tcp_write_err(sk);} else {// 发送窗口探测报文tcp_send_probe0(sk);}
}void tcp_send_probe0(struct sock *sk)
{struct inet_connection_sock *icsk = inet_csk(sk);struct tcp_sock *tp = tcp_sk(sk);struct net *net = sock_net(sk);unsigned long timeout;int err;// 这个函数中完成窗口探测报文的发送err = tcp_write_wakeup(sk, LINUX_MIB_TCPWINPROBE);// 后边要重启窗口探测定时器,在重启之前,要判断一下需不需要重启// 如下两个条件满足,则不需要重启if (tp->packets_out || tcp_write_queue_empty(sk)) {icsk->icsk_probes_out = 0;icsk->icsk_backoff = 0;icsk->icsk_probes_tstamp = 0;return;}icsk->icsk_probes_out++;if (err <= 0) {if (icsk->icsk_backoff < net->ipv4.sysctl_tcp_retries2)icsk->icsk_backoff++;timeout = tcp_probe0_when(sk, TCP_RTO_MAX);} else {/* If packet was not sent due to local congestion,* Let senders fight for local resources conservatively.*/timeout = TCP_RESOURCE_PROBE_INTERVAL;}timeout = tcp_clamp_probe0_to_user_timeout(sk, timeout);tcp_reset_xmit_timer(sk, ICSK_TIME_PROBE0, timeout, TCP_RTO_MAX);
}// 这个函数用于 0 窗口探测定时器
// 同时也用于 keepalive 定时器
int tcp_write_wakeup(struct sock *sk, int mib)
{struct tcp_sock *tp = tcp_sk(sk);struct sk_buff *skb;if (sk->sk_state == TCP_CLOSE)return -1;skb = tcp_send_head(sk);// 如果当前发送队列中有报文了,并且接收窗口已经打开// 那么就不需要发送探测报文,直接发送用户数据if (skb && before(TCP_SKB_CB(skb)->seq, tcp_wnd_end(tp))) {int err;unsigned int mss = tcp_current_mss(sk);unsigned int seg_size = tcp_wnd_end(tp) - TCP_SKB_CB(skb)->seq;if (before(tp->pushed_seq, TCP_SKB_CB(skb)->end_seq))tp->pushed_seq = TCP_SKB_CB(skb)->end_seq;/* We are probing the opening of a window* but the window size is != 0* must have been a result SWS avoidance ( sender )*/if (seg_size <TCP_SKB_CB(skb)->end_seq - TCP_SKB_CB(skb)->seq ||skb->len > mss) {seg_size = min(seg_size, mss);TCP_SKB_CB(skb)->tcp_flags |= TCPHDR_PSH;if (tcp_fragment(sk, TCP_FRAG_IN_WRITE_QUEUE, skb,seg_size, mss, GFP_ATOMIC))return -1;} else if (!tcp_skb_pcount(skb))tcp_set_skb_tso_segs(skb, mss);TCP_SKB_CB(skb)->tcp_flags |= TCPHDR_PSH;err = tcp_transmit_skb(sk, skb, 1, GFP_ATOMIC);if (!err)tcp_event_new_data_sent(sk, skb);return err;} else {// 发送探测报文return tcp_xmit_probe_skb(sk, 0, mib);}
}
2.3.3 窗口探测定时器什么时候停止 ?
定时器停止的情况有以下几种:
① 发送一次探测报文之后判断当前链路上是不是有已发送但是还没有确认的报文,或者发送队列中是不是有数据。上边两个条件满足其一,则不再重启定时器,也就意味着定时器后边不会再触发了。参考函数 tcp_send_probe0()。
② 收到 ack 得知对端打开接收窗口
static int tcp_ack(struct sock *sk, const struct sk_buff *skb, int flag)
{// 已经发送但还没有确认的报文int prior_packets = tp->packets_out;// 如果发送的报文都已经确认了,那么就尝试停止探测定时器if (!prior_packets)goto no_queue;no_queue:// 这个函数中会进行判断,然后决定停止探测定时器还是重启探测定时器tcp_ack_probe(sk);return 0;
}static void tcp_ack_probe(struct sock *sk)
{struct inet_connection_sock *icsk = inet_csk(sk);struct sk_buff *head = tcp_send_head(sk);const struct tcp_sock *tp = tcp_sk(sk);// 如果发送队列是空的,不对探测定时器做操作if (!head)return;// 如果现在的窗口能把 skb 这个报文全部发送出去,则停掉探测定时器// 否则,重启探测定时器if (!after(TCP_SKB_CB(head)->end_seq, tcp_wnd_end(tp))) {icsk->icsk_backoff = 0;icsk->icsk_probes_tstamp = 0;inet_csk_clear_xmit_timer(sk, ICSK_TIME_PROBE0);/* Socket must be waked up by subsequent tcp_data_snd_check().* This function is not for random using!*/} else {unsigned long when = tcp_probe0_when(sk, TCP_RTO_MAX);when = tcp_clamp_probe0_to_user_timeout(sk, when);tcp_reset_xmit_timer(sk, ICSK_TIME_PROBE0, when, TCP_RTO_MAX);}
}
2.3.4 窗口探测定时器实验
为了测试 0 窗口的情况,tcp 连接建立之后,客户端向服务端发送数据,但是服务端不接收数据。这样的话,接收侧窗口很快就会变为 0。
伪码如下:
服务端:
socket()
bind()
listen()
accept_fd = accept()
// 服务端 accept 一个连接之后,不立即接收报文,而是 10 s 之后再接收报文
sleep(10)
recv()客户端:
connect()
// 客户端建立连接之后,就立即发送数据
send()
抓包,如下图所示,192.168.1.104 是客户端,192.168.1.103 是服务端,建立连接之后,客户端向服务端发数据。
① 序号 30 是发送的最后一个报文,序列号是 83313,长度是 6912,所以最后一个序列号是 83313 + 6912 - 1 = 90224。
② 序列号 31 是服务端给客户端的 ack, ack seq 是 90225,意思是客户端下一个要发的数据序号是 90225。
③ 序列号 34 是客户端发送的 0 窗口探测报文,可以看到序列号是 90224,而不是 90225。
④ 序列号 35 是服务端发送给客户端的 ack, 这个 ack 中包含窗口信息,是 0 说明现在窗口仍然是 0。
过了 10s 之后,服务端开始读数据,这个时候,接收侧的窗口就打开了。
① 43 和 44 是服务端向客户端发送的窗口打开通知。
② 45 是客户端向服务端开始发送数据。
2.4 保活定时器
保活定时器,顾名思义,就是当 tcp 连接上长时间没有数据传输时,用来判断对端是否还存在,如果一端给另外一端发送一个保活报文,然后得到回应报文,那么说明对端就是还存在的,这条连接继续保持;反之,如果收不到对端的回应,那么就会认为对端已经不存在了,则会关闭这条连接。
保活定时器和上边的窗口探测定时器,都是探测定时器,一个是窗口探测,一个存活性探测。
保活定时器,默认是没有开启的,用户如果想使能该功能话,需要通过函数 setsockopt() 来设置 SO_KEEPALIVE 选项。
// 用户设置 KEEALIVE
int val = 1;
setsockopt(sock_fd, SOL_SOCKET, SO_KEEPALIVE, (void *)&val, sizeof(val));// SO_KEEPALIVE 选项在内核中,通过函数 tcp_set_keepalive 来完成
// 可以看到,如果是打开选项,则启动定时器,关闭选项则停止定时器
void tcp_set_keepalive(struct sock *sk, int val)
{if ((1 << sk->sk_state) & (TCPF_CLOSE | TCPF_LISTEN))return;if (val && !sock_flag(sk, SOCK_KEEPOPEN))inet_csk_reset_keepalive_timer(sk,keepalive_time_when(tcp_sk(sk)));else if (!val)inet_csk_delete_keepalive_timer(sk);
}
保活定时器的超时处理函数为 tcp_keepalive_timer()。
static void tcp_keepalive_timer(struct timer_list *t)
{struct sock *sk = from_timer(sk, t, sk_timer);struct inet_connection_sock *icsk = inet_csk(sk);struct tcp_sock *tp = tcp_sk(sk);u32 elapsed;// 该函数首先判断了四种情况,在这几种情况下,不需要发送保活报文,函数直接退出// 1、套接字正在被使用,说明最近会有数据收发,所以不需要发送保活报文if (sock_owned_by_user(sk)) {inet_csk_reset_keepalive_timer(sk, HZ / 20);goto out;}// 2、套接字处于 LISTEN 状态,处于 LISTEN 状态的套接字,不是一条连接套接字,// 也不需要发送保活报文。可以看到下边的注释,非常有趣,类似于这样的注释,内核中不少if (sk->sk_state == TCP_LISTEN) {pr_err("Hmm... keepalive on a LISTEN ???\n");goto out;}// 3、这个链接即将关闭,也不需要发送保活报文// TCP_FIN_WAIT2 也会使用这个定时器if (sk->sk_state == TCP_FIN_WAIT2 && sock_flag(sk, SOCK_DEAD)) {if (tp->linger2 >= 0) {const int tmo = tcp_fin_time(sk) - TCP_TIMEWAIT_LEN;if (tmo > 0) {tcp_time_wait(sk, TCP_FIN_WAIT2, tmo);goto out;}}tcp_send_active_reset(sk, GFP_ATOMIC);goto death;}// 4、没有设置 SOCK_KEEPOPEN 标志,不发送保活报文,理论只要设置了 SO_KEEPALIVE 就会设置这个标志// 处于关闭状态或者还在连接建立过程中,也不发送保活报文if (!sock_flag(sk, SOCK_KEEPOPEN) ||((1 << sk->sk_state) & (TCPF_CLOSE | TCPF_SYN_SENT)))goto out;// 获取保活定时器超时时间,为了下一行代码直接 goto resched 做准备,// 如果这里不获取的话,下一句 goto resched 之后,定时器的超时时间是 0,// 很明显是不对的elapsed = keepalive_time_when(tp);// tp->packets_out 不为 0, 说明本端发出去的包还有包没有收到 ack// 这种情况下也不发送保活报文// write queue 不为空,说明现在连接还有数据需要传输,也不发送保活报文if (tp->packets_out || !tcp_write_queue_empty(sk))goto resched;// 这句代码是该函数很重要的一行代码// tp->rcv_tstamp 是上一次收到数据的时间// icsk->icsk_ack.lrcvtime 是上一次收到 ack 的时间// tcp_jiffies32 是当前时间// 该函数的返回结果就是连接上没有数据的时间,// 只有这个时间超过了 /proc/sys/net/ipv4/tcp_keepalive_time,才会发送保活报文// 否则不发送保活报文// static inline u32 keepalive_time_elapsed(const struct tcp_sock *tp)// {// const struct inet_connection_sock *icsk = &tp->inet_conn;// return min_t(u32, tcp_jiffies32 - icsk->icsk_ack.lrcvtime,// tcp_jiffies32 - tp->rcv_tstamp);// }elapsed = keepalive_time_elapsed(tp);if (elapsed >= keepalive_time_when(tp)) {// icsk->icsk_probes_out >= keepalive_probes(tp)// 这个条件即保活报文总数限制,默认是 9,如果超过这个数// 则关闭连接if ((icsk->icsk_user_timeout != 0 &&elapsed >= msecs_to_jiffies(icsk->icsk_user_timeout) &&icsk->icsk_probes_out > 0) ||(icsk->icsk_user_timeout == 0 &&icsk->icsk_probes_out >= keepalive_probes(tp))) {tcp_send_active_reset(sk, GFP_ATOMIC);tcp_write_err(sk);goto out;}if (tcp_write_wakeup(sk, LINUX_MIB_TCPKEEPALIVE) <= 0) {// 发送 keepalive 报文返回成功,增加计数// elapsed 重新赋值,默认是 75sicsk->icsk_probes_out++;elapsed = keepalive_intvl_when(tp);} else {elapsed = TCP_RESOURCE_PROBE_INTERVAL;}} else {elapsed = keepalive_time_when(tp) - elapsed;}sk_mem_reclaim(sk);resched:inet_csk_reset_keepalive_timer(sk, elapsed);goto out;death:tcp_done(sk);out:bh_unlock_sock(sk);sock_put(sk);
}
① 三个参数
tcp keepalive 功能,有三个参数可供用户配置
配置参数 | 默认值 | 作用 |
/proc/sys/net/ipv4/tcp_keepalive_time | 7200 | 多长时间没有数据传输就会发送保活探测报文,默认是 7200s,即 2 个小时; 这个时间对于实际应用来说太长,可以根据应用的具体场景做调整。 |
/proc/sys/net/ipv4/tcp_keepalive_intvl | 75 | 发送保活报文的时间间隔,默认是 75s;保活报文,不是发一次,收不到回应就立即认为对方不存在了,而是可以发送多次,最多可以发送的次数由下边的参数控制。 |
/proc/sys/net/ipv4/tcp_keepalive_probes | 9 | 发送保活报文的次数,默认是 9,也就是说如果发送了 9 个报文,都没有收到对端的响应,那么就会认为对端不存在了。 |
② 没有数据传输的时间判断
发送 keepalive 报文之前需要进行判断,其中一个条件是这条连接上多久没有数据传输了,只有没有数据传输的时间超过一定值之后,才会发送保活报文,也就是说当连接上有数据传输的时候,这条连接肯定是正常的,不需要发送保活报文。
上文中对函数 tcp_keepalive_timer(struct timer_list *t) 的注释中包括了对该时间的判断,在keepalive_time_elapsed(tp); 这行代码中获取到了没有数据活跃的持续时间。
函数 keepalive_time_elapsed() 中获取时间的方式,通过最后收到数据的时间以及最后收到的 ack 的时间来计算。乍一看是只考虑了接收方向的数据,其实不然,tp->rcv_tstamp 即最后接收到数据的时间,可以代表接收方向, icsk->icsk_ack.lrcvtime 表示最后接收到 ack 的时间,收到了 ack 说明之前肯定发送了数据,所以这个时间可以代表发送方向。
③ 发送保活报文
tcp 中并没有一个特殊的标志来标记这个报文是保活报文,tcp hdr flag 中没有 keepalive 相关的标志,tcp 选项中也没有 keepalive 相关的选项。
那么 tcp 报文有什么特点呢 ?
发送保活报文在函数 tcp_xmit_probe_skb() 中完成。
调用关系如下:
tcp_keepalive_timer()
调用
tcp_write_wakeup()
调用
tcp_xmit_probe_skb()
从函数 tcp_xmit_probe_skb() 的注释中也可以看到,这个报文的特殊之处在于序列号,序列号只一个已经发送过的序列号,并且已经 ack 过了。接收端还存在,收到这样的数据之后,会回应一个 ack 报文;如果接收端已经不存在了,那么就会发过来一个 rst 报文,本端收到 rst 报文之后便会关闭连接。
static int tcp_xmit_probe_skb(struct sock *sk, int urgent, int mib)
{struct tcp_sock *tp = tcp_sk(sk);struct sk_buff *skb;/* We don't queue it, tcp_transmit_skb() sets ownership. */skb = alloc_skb(MAX_TCP_HEADER,sk_gfp_mask(sk, GFP_ATOMIC | __GFP_NOWARN));if (!skb)return -1;/* Reserve space for headers and set control bits. */skb_reserve(skb, MAX_TCP_HEADER);/* Use a previous sequence. This should cause the other* end to send an ack. Don't queue or clone SKB, just* send it.*/tcp_init_nondata_skb(skb, tp->snd_una - !urgent, TCPHDR_ACK);NET_INC_STATS(sock_net(sk), mib);return tcp_transmit_skb(sk, skb, 0, (__force gfp_t)0);
}
为了方便测试,把 keepalive 时间改成了 10s(默认 7200s),进行测试,抓包结果如下,从抓包结果可以看到:
① keepalive 的时间变成了 10s
② 在发送 keepalive 报文之前,3025 + 1072 -1 = 4096,seq 为 4096 的字节已经发送出去了,并且得到了 ack;keepalive 的序列号是 4096,本来正常的数据应该是 4097,接收方想要接收的下一个字节的编号也是 4097。
③ 接收方收到报文之后立即回应了 ack 报文。
3 断开连接过程中的定时器
3.1 TIME_WAIT 定时器
主动发起关闭的一方,最后一个状态是 TIME_WAIT。发送最后一个 ack 之后便从 FIN_WAIT_2 状态进入到 TIME_WAIT 状态。
在函数 tcp_fin() 中处理 FIN 标志,主动断开连接的一方收到对端发送的 FIN 报文之后,返回一个 ack 之后便会进入到 TIME_WAIT 状态。tcp_time_wait() 函数中完成 TIME_WAIT 状态的处理,在这个函数中会启动 TIME_WAIT 定时器,定时器的超时处理函数 tw_timer_handler()。
void tcp_fin(struct sock *sk)
{switch (sk->sk_state) {...case TCP_FIN_WAIT2:/* Received a FIN -- send ACK and enter TIME_WAIT. */tcp_send_ack(sk);tcp_time_wait(sk, TCP_TIME_WAIT, 0);break;...}
}
相关文章:

tcp 中使用的定时器
定时器的使用场景主要有两种。 (1)周期性任务 这是定时器最常用的一种场景,比如 tcp 中的 keepalive 定时器,起到 tcp 连接的两端保活的作用,周期性发送数据包,如果对端回复报文,说明对端还活着…...

黑马Java——IO流
一、IO流的概述 IO流:存储和读取数据的解决方案 IO流和File是息息相关的 1、IO流的分类 1.1、纯文本文件 word、Excel不是纯文本文件 而txt或者md文件是纯文本文件 2、小结 二、IO流的体系结构 三、字节流 1、FileOutputStream(字节输出流ÿ…...
re:从0开始的CSS学习之路 11. 盒子垂直布局
1. 盒子的垂直布局的注意 若两个“相邻”垂直摆放的盒子,上面盒子的下外边距与下面盒子的上外边距会发生重叠,称为外边距合并 若合并后,外边距会选择重叠外边距的较大值 若两个盒子具有父子关系,则两个盒子的上外边距会发生重叠&…...

Kindling-OriginX 如何集成 DeepFlow 的数据增强网络故障的解释力
DeepFlow 是基于 eBPF 的可观测性开源项目,旨在为复杂的云基础设施及云原生应用提供深度可观测性。DeepFlow 基于 eBPF 采集了精细的链路追踪数据和网络、应用性能指标,其在网络路径上的全链路覆盖能力和丰富的 TCP 性能指标能够为专业用户和网络领域专家…...

轻松掌握Jenkins执行远程window的Jmeter接口脚本
Windows环境:10.1.2.78 新建与配置节点 【系统管理】—【管理节点】—【新建节点】输入节点名称,勾选“dumb slave”,点击ok 按如上配置: 说明: Name:定义slave的唯一名称标识,可以是任意字…...

UI文件原理
使用UI文件创建界面很轻松很便捷,他的原理就是每次我们保存UI文件的时候,QtCreator就自动帮我们将UI文件翻译成C的图形界面创建代码。可以通过以下步骤查看代码 到工程编译目录,一般就是工程同级目录下会生成另一个编译目录,会找到…...

OS设备管理
设备管理 操作系统作为系统资源的管理者,其提供的功能有:处理机管理、存储器管理、文件管理、设备管理。其中前三个管理都是在计算机的主机内部管理其相对应的硬件。 I/O设备 I/O即输入/输出。I/O设备即可以将数据输入到计算机,或者可以接收…...
Matlab绘图经典代码大全:条形图、极坐标图、玫瑰图、填充图、饼状图、三维网格云图、等高线图、透视图、消隐图、投影图、三维曲线图、函数图、彗星图
学会 MATLAB 中的绘图命令对初学者来说具有重要意义,主要体现在以下几个方面: 1. 数据可视化。绘图命令是 MATLAB 中最基本也是最重要的功能之一,它可以帮助初学者将数据可视化,更直观地理解数据的分布、变化规律和趋势。通过绘制图表,可以快速了解数据的特征,从而为后续…...

姿态传感器MPU6050模块之陀螺仪、加速度计、磁力计
MEMS技术 微机电系统(MEMS, Micro-Electro-Mechanical System),也叫做微电子机械系统、微系统、微机械等,指尺寸在几毫米乃至更小的高科技装置。微机电系统其内部结构一般在微米甚至纳米量级,是一个独立的智能系统。 微…...

MySQL 基础知识(一)之数据库和 SQL 概述
目录 1 数据库相关概念 2 数据库的结构 3 SQL 概要 4 SQL 的基本书写规则 1 数据库相关概念 数据库是将大量的数据保存起来,通过计算机加工而成的可以进行高效访问的数据集合数据库管理系统(DBMS)是用来管理数据库的计算机系统…...

挑战杯 wifi指纹室内定位系统
简介 今天来介绍一下室内定位相关的原理以及实现方法; WIFI全称WirelessFidelity,在中文里又称作“行动热点”,是Wi-Fi联盟制造商的商标做为产品的品牌认证,是一个创建于IEEE 802.11标准的无线局域网技术。基于两套系统的密切相关ÿ…...

Midjourney提示词风格调试测评
在Midjourney中提示词及风格参数的变化无疑会对最终的作品产生影响,那影响具体有多大?今天我我们将通过一个示例进行探究。 示例提示词: 计算机代码海洋中的黄色折纸船(图像下方)风格参考:金色长发的女人,…...
Codeforces Round 926 (Div. 2)(A~C)
A. Sasha and the Beautiful Array 分析:说实话,打比赛的时候看到这题没多想,过了一下样例发现将数组排序一下就行,交了就过了。刚刚写题解反应过来,a2-a1a3-a2.....an-a(n-1) an - a1,所以最后结果只取决…...

Godot 游戏引擎个人评价和2024年规划(无代码)
文章目录 前言Godot C# .net core 开发简单评价Godot相关网址可行性 Godot(GDScirpt) Vs CocosGodot VS UnityUnity 的裁员Unity的股票Unity的历史遗留问题:Mono和.net core.net core的开发者,微软 个人的独立游戏Steam平台分成说明独立游戏的选题美术风…...

Win11关闭Windows Defender实时保护,暂时关闭和永久关闭方法 | Win10怎么永久关闭Windows Defender实时保护
文章目录 1. 按2. 暂时关闭Windows Defender实时保护3. 永久关闭实时保护 1. 按 开启Windows Defender实时保护有时候会导致系统变得异常卡顿,严重影响系统的流畅度,并且由于会有几率错误拦截和查杀我们的正常操作,所以还会导致我们的程序无…...

C# CAD2016 宗地生成界址点,界址点编号及排序
1 、界址点起点位置C# CAD2016 多边形顶点按方向重新排序 2、 界址点顺时针逆时针走向 C# CAD2016 判断多边形的方向正时针或逆时针旋转 3、块文件插入 //已知块文件名称 GXGLQTC //块文件需要插入的坐标点 scaledPoint// 插入块到当前图纸中的指定位置ObjectId newBlockId;B…...

[ai笔记7] google浏览器ai学习提效定制优化+常用插件推荐
欢迎来到文思源想的ai空间,这是技术老兵重学ai以及成长思考的第7篇分享! 工欲善其事必先利其器,为了ai学习的效能提升,放假期间对google浏览器做了一次系统整改,添加了一些配置和插件,这里既有一些显示、主…...

联想thinkpad-E450双系统升级记
早期笔记本联想thinkpad-E450双系统 大约16年花4000多大洋,买了一台thinkpad-E450屏幕是16寸本,有AMD独立显卡,i5cpu,4G内存。 . 后来加了一个同型号4G内存组成双通道, . 加了一个三星固态500G, . 换了一个…...

Mysql运维篇(四) Xtarbackup--备份与恢复练习
一路走来,所有遇到的人,帮助过我的、伤害过我的都是朋友,没有一个是敌人。如有侵权,请留言,我及时删除! 前言 xtrabackup是Percona公司CTO Vadim参与开发的一款基于InnoDB的在线热备工具,具有…...
vue3 封装一个通用echarts组件
实现这个组件需要引入echarts和vue-echarts插件,使用vue-echarts是因为它帮我们封装了一些很常用的功能,比如监听页面resize后重新渲染功能,本次组件只使用到了autoresize配置,其它可以根据官方文档按需选配 https://github.com/…...

K8S认证|CKS题库+答案| 11. AppArmor
目录 11. AppArmor 免费获取并激活 CKA_v1.31_模拟系统 题目 开始操作: 1)、切换集群 2)、切换节点 3)、切换到 apparmor 的目录 4)、执行 apparmor 策略模块 5)、修改 pod 文件 6)、…...

对WWDC 2025 Keynote 内容的预测
借助我们以往对苹果公司发展路径的深入研究经验,以及大语言模型的分析能力,我们系统梳理了多年来苹果 WWDC 主题演讲的规律。在 WWDC 2025 即将揭幕之际,我们让 ChatGPT 对今年的 Keynote 内容进行了一个初步预测,聊作存档。等到明…...
Linux云原生安全:零信任架构与机密计算
Linux云原生安全:零信任架构与机密计算 构建坚不可摧的云原生防御体系 引言:云原生安全的范式革命 随着云原生技术的普及,安全边界正在从传统的网络边界向工作负载内部转移。Gartner预测,到2025年,零信任架构将成为超…...

学习STC51单片机32(芯片为STC89C52RCRC)OLED显示屏2
每日一言 今天的每一份坚持,都是在为未来积攒底气。 案例:OLED显示一个A 这边观察到一个点,怎么雪花了就是都是乱七八糟的占满了屏幕。。 解释 : 如果代码里信号切换太快(比如 SDA 刚变,SCL 立刻变&#…...

LINUX 69 FTP 客服管理系统 man 5 /etc/vsftpd/vsftpd.conf
FTP 客服管理系统 实现kefu123登录,不允许匿名访问,kefu只能访问/data/kefu目录,不能查看其他目录 创建账号密码 useradd kefu echo 123|passwd -stdin kefu [rootcode caozx26420]# echo 123|passwd --stdin kefu 更改用户 kefu 的密码…...

Linux 内存管理实战精讲:核心原理与面试常考点全解析
Linux 内存管理实战精讲:核心原理与面试常考点全解析 Linux 内核内存管理是系统设计中最复杂但也最核心的模块之一。它不仅支撑着虚拟内存机制、物理内存分配、进程隔离与资源复用,还直接决定系统运行的性能与稳定性。无论你是嵌入式开发者、内核调试工…...

Axure 下拉框联动
实现选省、选完省之后选对应省份下的市区...
人工智能 - 在Dify、Coze、n8n、FastGPT和RAGFlow之间做出技术选型
在Dify、Coze、n8n、FastGPT和RAGFlow之间做出技术选型。这些平台各有侧重,适用场景差异显著。下面我将从核心功能定位、典型应用场景、真实体验痛点、选型决策关键点进行拆解,并提供具体场景下的推荐方案。 一、核心功能定位速览 平台核心定位技术栈亮…...
书籍“之“字形打印矩阵(8)0609
题目 给定一个矩阵matrix,按照"之"字形的方式打印这个矩阵,例如: 1 2 3 4 5 6 7 8 9 10 11 12 ”之“字形打印的结果为:1,…...

AxureRP-Pro-Beta-Setup_114413.exe (6.0.0.2887)
Name:3ddown Serial:FiCGEezgdGoYILo8U/2MFyCWj0jZoJc/sziRRj2/ENvtEq7w1RH97k5MWctqVHA 注册用户名:Axure 序列号:8t3Yk/zu4cX601/seX6wBZgYRVj/lkC2PICCdO4sFKCCLx8mcCnccoylVb40lP...