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/…...
安装 Windows Server 2003
1.镜像安装 镜像安装:Windows Server 2003 2.安装过程(直接以图的形式呈现) 按Enter(继续),继续后F8继续 直接Enter安装 下一步 秘钥:GM34K-RCRKY-CRY4R-TMCMW-DMDHM 等待安装成功即可...
在STM32中使用DMA进行SD卡读写操作的实现方法
在STM32中,使用DMA进行SD卡的读写操作可以提高数据传输的速度和效率。下面是在STM32中使用DMA进行SD卡读写操作的实现方法: ✅作者简介:热爱科研的嵌入式开发者,修心和技术同步精进 ❤欢迎关注我的知乎:对error视而不见…...
StringBuilder/StringBuffer类(Java)
StringBuilder/StringBuffer类 当对字符串进行修改的时候,使用 StringBuffer / StringBuilder 类更方便。和 String 类不同的是,StringBuffer 和 StringBuilder 类的对象能够被多次的修改,并且不产生新的未使用对象。方法类似 public class…...
SQL的1999语法
目录 交叉连接 实现交叉连接 自然连接 实现自然连接(实际上就是内连接) ON和USING 使用自然连接时要求两张表的字段名称相同,但是如果不相同或者两张表中有两组字段是重名,这时就要利用 ON 子句指定关联条件,利用 USING 子句…...
【AIGC】Stable Diffusion安装包
Stable Diffusion 的安装教程通常分为以下几个步骤: 一、安装 Python: 确保您的系统中已经安装了 Python,并且版本符合 Stable Diffusion 的要求。通常情况下,Python 版本应为 3.6 或更高版本。您可以从 Python 官方网站下载并安…...
C++:迭代器的封装思想
C:迭代器的封装思想 list迭代器实现反向迭代器实现 本博客将通过实现list的迭代器,以及它的反向迭代器,来帮助大家理解迭代器的底层逻辑,以及封装思想。 list迭代器实现 迭代器是一个遍历容器的工具,其可以通过自增自…...
飞天使-k8s知识点17-kubernetes实操2-pod探针的使用
文章目录 探针的使用容器探针启动实验1-启动探针的使用-startupprobeLiveness Probes 和 Readiness Probes演示若存在started.html 则进行 探针的使用 kubectl edit deploy -n kube-system corednslivenessprobe 的使用 livenessProbe:failureThreshold: 5httpGet:path: /heal…...
tee漏洞学习-翻译-3:TrustZone exploit for MSM8974
原文:http://bits-please.blogspot.com/2015/08/full-trustzone-exploit-for-msm8974.html 在这篇博文中,我们将介绍利用上一篇文章中描述的 TrustZone 漏洞的完整过程。 在开发此漏洞时,我只使用了我值得信赖的(个人࿰…...
rust递归遍历磁盘目录及文件
Std库实现 //遍历dir目录,找出修改日期距离当前超过age天的文件名称,存入file_list中 fn visit_dir(dir: &Path, file_list: &mut Vec<String>, age: u64) -> io::Result<()> {if dir.is_dir() {for entry in fs::read_dir(dir)…...
C语言每日一题(56)平衡二叉树
力扣网 110 平衡二叉树 题目描述 给定一个二叉树,判断它是否是高度平衡的二叉树。 本题中,一棵高度平衡二叉树定义为: 一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过 1 。 示例 1: 输入:root [3,9,20,…...
Flutter Android开发 梳理Google Material Design颜色体系
前言 做安卓开发(Kotlin语言),Flutter开发的人员应该都听说过谷歌一直推崇的Material Design,而Material Design Color是其推崇的颜色体系,具体来说,Material Design Color是一套旨在帮助设计师和开发者创…...
每日五道java面试题之java基础篇(六)
目录: 第一题:Java 创建对象有哪⼏种⽅式?第二题 .Integer a 127,Integer b 127;Integer c 128,Integer d 128;相等吗?第三题.Object 类的常⻅⽅法?第四题 List和Set的区别第五题 ArrayList和…...
c++ STL系列——(五)map
目录 引言 特点 包含头文件 基本特性 基本操作 插入元素 访问元素 移除元素 检查是否包含某个键 获取元素数量 高级特性 迭代器 自定义比较函数 实际应用 统计字符出现次数 缓存最近访问的元素 总结 引言 在C中,标准模板库(STL…...
Huggingface 文档翻译完毕
Accelerate 0.27 中文文档音频课程文档AutoTrain 中文文档AWS 中文文档竞赛中文文档Diffusers 0.26 中文文档深度强化学习课程文档数据集服务器中文文档Datasets 2.17 中文文档 Evaluate 0.4 中文文档Huggingface.js 中文文档Hub 中文文档Hub 客户端库 JS 0.20 中文文档推理 AP…...
C++中类的6个默认成员函数 【拷贝构造函数】
文章目录 拷贝构造函数的使用拷贝构造对于自定义类型【浅拷贝】深拷贝拷贝构造函数典型调用场景 拷贝构造函数的使用 在前几章学习对象的时候,我们有的时候需要一个与已存在对象一某一样的新对象 那在创建对象时,可否创建一个与已存在对象一某一样的新对…...
【前端高频面试题--Vuex下篇】
🚀 作者 :“码上有前” 🚀 文章简介 :前端高频面试题 🚀 欢迎小伙伴们 点赞👍、收藏⭐、留言💬前端高频面试题--Vuex篇 往期精彩内容Vuex 的原理Vuex中action和mutation的区别Vuex 和 localStor…...
MySQL性能调优篇(4)-查询语句的优化与重构
MySQL数据库查询语句的优化与重构 MySQL是一种常用的关系型数据库管理系统,广泛应用于Web开发中。在实际应用中,对数据库查询语句的优化和重构是提高应用性能和响应速度的重要手段。本文将介绍一些常见的优化技巧和重构方法,帮助开发者提高数…...
LInux、源码编译安装
步骤: 步骤1:安装开发工具gcc与make,释放源代码至指定目录 yum -y install gcc make 步骤2:tar解包,释放源代码至指定目录 tar -xf /root/tools.tar.gz -C /usr/local 步骤3:./configure 配置,…...
wordpress好的网站主题
有什么好的网站主题,都分享在这里了。 蓝色风格的wordpress模板,好的wordpress网站主题,需要既好看,又好用。 https://www.zhanyes.com/qiye/6305.html 血红色的好看的wordpress主题,布局经典,设计好的&am…...
【Java多线程】对进程与线程的理解
目录 1、进程/任务(Process/Task) 2、进程控制块抽象(PCB Process Control Block) 2.1、PCB重要属性 2.2、PCB中支持进程调度的一些属性 3、 内存分配 —— 内存管理(Memory Manage) 4、线程(Thread)…...
C# CAD交互界面-自定义面板集-查找定位(六)
运行环境 vs2022 c# cad2016 调试成功 一、代码说明 1. 类成员变量声明: List<ObjectId> objectIds new List<ObjectId>(); // 用于存储AutoCAD实体对象的ObjectId列表 private static Autodesk.AutoCAD.Windows.PaletteSet _ps2; // 自定义浮动面板…...
5.7 BCC工具之disksnoop.py解读
一,disksnoop.py简介 disksnoop工具用于追踪块设备的I/O操作的延迟,它会在每次I/O执行完成后打印一行摘要信息。我们根据这些摘要日志,来分析当前的I/O操作是否存在延迟,以判断I/O是否达到了瓶颈。 二,代码示例 #!/usr/bin/python # # disksnoop.py Trace block device…...
QT:实现图片选择器
一、效果图 二、用到的类 qApp:可以快速获取到项目目录位置。 QSettings :编写config文件,记录上次打开图片的位置,下次打开图片会从上次的位置查找图片。 QPixmap:用于图片的缩放,防止图片过小࿰…...
LLM大模型相关问题汇总---包括问题与答案
一、基础篇 1. 目前主流的开源模型体系有哪些? - Transformer体系:由Google提出的Transformer模型及其变体,如BERT、GPT等。 - PyTorch Lightning:一个基于PyTorch的轻量级深度学习框架,用于快速原型设计和实验…...
自动化测试定位不到元素怎么办?
1.动态id定位不到元素 分析原因:每次打开页面,ID都会变化。用ID去找元素,每次刷新页面ID都会发生变化。 解决方案:推荐使用xpath的相对路径方法或者cssSelector查找到该元素。 2.iframe原因定位不到元素 分析原因:…...
1 scala集合-数组
1 定长数组 定长数组,是指数组长度不可变。定义定长数组的方法有如下两种: 方法1: var/val variable_name new Array[元素类型](数组长度) // 通过制定长度定义例如,定义一个长度为20的Int 类型数组。 scala> val a new …...
双场板功率GaN HEMT电容模型以精确模拟开关行为
标题:Capacitance Modeling in Dual Field-Plate Power GaN HEMT for Accurate Switching Behavior(TED.16年) 摘要 本文提出了一种基于表面电位的紧凑模型,用于模拟具有栅极和源极场板(FP)结构的AlGaN/G…...
OpenCV Mat实例详解 四
OpenCV Mat实例详解三中详细介绍来了OpenCV Mat类的公有静态成员函数,下面介绍OpenCV Mat类的其他常用成员函数。 OpenCV Mat类常用成员函数 Mat & adjustROI (int dtop, int dbottom, int dleft, int dright); dtop ROI 上边界移动值,如…...
Fluke ADPT 连接器新增对福禄克万用 Fluke 106 的支持
所需设备: 1、Fluke ADPT连接器; 2、Fluke 106; Fluke 106 拆机图: 显示界面如下图: 并且可以将波形导出到EXCEL: 福禄克万用表需要自己动手改造!!!...
算法-3-基本的数据结构
单双链表 1.单链表双链表如何反转 import java.util.ArrayList; import java.util.List;public class Code01_ReverseList {public static class Node {public int value;public Node next;public Node(int data) {value data;}}public static class DoubleNode {public int…...