网站开发定制合同范本,建一个网站需要购买域名 虚拟主机,哪个平台打广告效果好,本周时政新闻热点10条定时器的使用场景主要有两种。 #xff08;1#xff09;周期性任务
这是定时器最常用的一种场景#xff0c;比如 tcp 中的 keepalive 定时器#xff0c;起到 tcp 连接的两端保活的作用#xff0c;周期性发送数据包#xff0c;如果对端回复报文#xff0c;说明对端还活着…定时器的使用场景主要有两种。 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 dont 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. Dont 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 4096seq 为 4096 的字节已经发送出去了并且得到了 ackkeepalive 的序列号是 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;...}
}