TCP的定时器系列 — SYNACK定时器

转载

主要内容:SYNACK定时器的实现,TCP_DEFER_ACCPET选项的实现。

内核版本:3.15.2

我的博客:http://blog.csdn.net/zhangskd

 

在上一篇博客中,已经连带介绍了SYNACK定时器的创建和删除,所以本文直接从它的激活和超时处理函数写起。

 

激活

 

在三次握手期间,服务器端收到SYN包后,会分配一个连接请求块,并初始化这个连接请求块。

然后构造和发送SYNACK包,把这个连接请求块链入半连接队列中,并启动SYNACK超时定时器。

之后如果再收到ACK,就能完成三次握手了。

 

具体路径为:

tcp_v4_do_rcv

    |--> tcp_rcv_state_process

               |--> tcp_v4_conn_request

                          |--> tcp_v4_send_synack

                          |--> inet_csk_reqsk_hash_add

                                     |--> inet_csk_reqsk_queue_added

 

inet_csk_reqsk_queue_hash_add()做的事情是:把连接请求块链入半连接队列中,设置超时时间,

启动SYNACK定时器。这便是SYNACK定时器的激活时机,三次握手的详情可见之前的blog。

static inline void inet_csk_reqsk_queue_added(struct sock *sk, const unsigned long timeout)
{/* 更新半连接队列长度,如果之前的长度为0 */if (reqsk_queue_added(&inet_csk(sk)->icsk_accept_queue) == 0)inet_csk_reset_keepalive_timer(sk, timeout); /*启动定时器 */
}static inline int reqsk_queue_added(struct request_sock_queue *queue)
{struct listen_opt *lopt = queue->listen_opt; /* 半连接队列 */const int prev_qlen = lopt->qlen; /* 之前的半连接队列长度 */lopt->qlen_young++;  /* 更新未重传过的请求块数 */lopt->qlen++; /* 更新半连接队列长度 */return prev_qlen;
}void inet_csk_reset_keepalive_timer(struct sock *sk, unsigned long len)
{sk_reset_timer(sk, &sk->sk_timer, jiffies + len);
}

 

超时处理函数

 

sk->sk_timer可以同时扮演几个角色:保活定时器,SYNACK定时器,FIN_WAIT2定时器。

通过判断此时连接的状态是LISTEN、ESTABLISHED,还是FIN_WAIT2,就可以直接区分它们了。

static void tcp_keepalive_timer(unsigned long data)
{struct sock *sk = (struct sock *) data;struct inet_connection_sock *icsk = inet_csk(sk);struct tcp_sock *tp = tcp_sk(sk);u32 elapsed;/* Only process if socket is not in use. */bh_lock_sock(sk);/* 如果用户进程正在使用此sock,那么一般过50ms后再来看看。* 可见SYNACK定时器不一定能够准时呢,而实际上它本身的误差就有200ms,详细可见下文。*/if (sock_owned_by_user(sk)) {/* Try again later. */inet_csk_reset_keepalive_timer(sk, HZ/20);goto out;}/* 连接处于LISTEN状态,那么肯定是SYNACK定时器了 */if (sk->sk_state == TCP_LISTEN) {tcp_synack_timer(sk);goto out;}...
out:bh_unlock_sock(sk);sock_put(sk);
}

 

对于SYNACK定时器,真正的处理函数是inet_csk_reqsk_queue_prune()。

#define TCP_SYNQ_INTERVAL (HZ/5) /* Period of SYNACK timer */
#define TCP_TIMEOUT_INIT ((unsigned) (1*HZ)) /* RFC6298 2.1 initial RTO value */
#define TCP_RTO_MAX ((unsigned) (120*HZ))/* Timer for listening sockets. */
static void tcp_synack_timer(struct sock *sk)
{inet_csk_reqsk_queue_prune(sk, TCP_SYNQ_INTERVAL, TCP_TIMEOUT_INIT, TCP_RTO_MAX);
}
void inet_csk_reqsk_queue_prune(struct sock *parent, const unsigned long interval,const unsigned long timeout, const unsigned long max_rto)
{struct inet_connection_sock *icsk = inet_csk(parent);struct request_sock_queue *queue = &icsk->icsk_accept_queue;struct listen_sock *lopt = queue->listen_opt; /* 半连接队列 *//* 如果没有设置TCP_SYNCNT选项,默认最多允许重传5次SYNACK */int max_retries = icsk->icsk_syn_retries ? : sysctl_tcp_synack_retries; int thresh = max_retries;unsigned long now = jiffies;struct request_sock **reqp, *req;int i, budget;/* 半连接队列要存在且至少有个连接请求块 */if (lopt == NULL || lopt->qlen == 0)return;/* 如果半连接队列的长度超过了最大值的一半,需要降低SYNACK的最大重传次数,详细见下文(1) */if (lopt->qlen >> (lopt->max_qlen_log - 1)) {int young = (lopt->qlen_young << 1);/* 半连接队列中,未重传过的连接请求块的比重越低,则允许的最大重传次数就越少。* 不这样做的话,老的连接请求块会存活很长时间,导致没有足够的空间接纳新的连接请求块。* 具体来说,默认的thresh为5,当未重传过的连接请求块的占比:* < 1/2,thresh = 4* < 1/4,thresh = 3* < 1/8,thresh = 2*/while (thresh > 2) {if (lopt->qlen < young)break;thresh--;young <<= 1;}}/* 如果设置了TCP_DEFER_ACCEPT选项,则更新SYNACK的最大重传次数,详细见下文(2) */if (queue->rskq_defer_accept)max_retries = queue->rskq_defer_accept; /* 连接的初始超时时间是1s,SYNACK定时器在首次触发之后,接下来每200ms就触发一次。* Q:连接请求块的超时时间依然是1s,那么SYNACK定时器为什么要更加频繁的触发呢?* A:增加了定时器的精确度,误差从1s降到200ms,也能更加及时的剔除太老的连接请求块。* 默认1s内遍历2次的半连接表。*/budget = 2 * (lopt->nr_table_entries / (timeout / interval));i = lopt->clock_hand; /* 半连接表中的第i个连接请求块队列,是上次遍历到的+1,这样不用每次都从头开始 */do {reqp = &lopt->syn_table[i]; /* 半连接表中的第i个连接请求块队列 */while ((req = *reqp) != NULL) { /* 遍历队列 */if (time_after_eq(now, req->expires)) { /* 如果SYNACK超时了 */int expire = 0, resend = 0; /* expire表示是否要丢弃本连接请求块,resend表示是否要重传SYNACK *//* 判断expire和resend的值,详细见下文(3) */syn_ack_recalc(req, thresh, max_retries, queue->rskq_defer_accept, &expire, &resend);req->rsk_ops->syn_ack_timeout(parent, req); /* 增加timeout统计计数 *//* 有意思的条件判断,先考虑expire,再考虑resend。* 条件为真时,表示此连接请求块还不是太老,不用删除。*/if (! expire && (! resend || ! inet_rtx_syn_ack(parent, req) || inet_rsk(req)->acked)) {unsigned long timeo;if (req->num_timeout++ == 0) /* 如果是尚未重传过的 */lopt->qlen_young--;timeo = min(timeout << req->num_timeout, max_rto); /* 超时时间指数增大 */req->expires = now + timeo;reqp = &req->dl_next;continue;}/* Drop this request */inet_csk_reqsk_queue_unlink(parent, req, reqp); /* 把连接请求块从半连接队列中删除 */reqsk_queue_removed(queue, req); /* 更新半连接队列长度相关变量 */reqsk_free(req); /* 释放连接请求块 */continue;}reqp = &req->dl_next;}i = (i + 1) & (lopt->nr_table_entries - 1);} while (--budget > 0);lopt->clock_hand = i; /* 本地变量到第(i - 1)个连接请求块队列,下次从第i个开始 */if (lopt->qlen)inet_csk_reset_keepalive_timer(parent, interval); /* 重置SYNACK定时器,超时时间为200ms */
}

 

最大重传次数的动态调整

 

如果半连接队列的长度超过了最大值的一半,就需要降低SYNACK所允许的最大重传次数。

影响因素是半连接队列中未重传过的连接请求块所占的比重。

在半连接队列中,未重传过的连接请求块的比重越低,则允许重传的次数就越少。

因为不这样做的话,老的连接请求块会存活很长时间,导致没有足够的空间接纳新的连接请求块,

而显然新的连接请求块(即未重传过的连接请求块)更加可信和有价值。

 

具体来说,默认的thresh为5,当未重传过的连接请求块的占比:

PCT < 1/2,thresh = 4

PCT < 1/4,thresh = 3

PCT < 1/8,thresh = 2

 

代码中的注释很详细,赞一个:

Normally all the openreqs are young and become mature (i.e. converted to established socket) for

first timeout. If synack was not acknowledged for 1 second, it means one of the following things:

synack was lost, ack was lost, rtt is high or nobody planned to ack (i.e. synflood). When server is a

bit loaded, queue is populated with old open requests, reducing effective size of queue. When server

is well loaded, queue size reduces to zero after several minutes of work. It is not synflood, it is normal

operation. The solution is pruning too old entries overriding normal timeout, when situation becomes

dangerous.

Essentially, we reserve half of room for young embrions; and abort old ones without pity, if old ones

are about to clog our table.

 

TCP_DEFER_ACCEPT选项

 

用于三次握手阶段,使用这个选项时,收到客户端发送的纯ACK后,会直接把纯ACK丢弃。

所以就不会马上创建和初始化一个新的sock,不会把此连接请求块从半连接队列移动到全连接队列,

更不会唤醒监听进程来accept。TCP_DEFER_ACCEPT,顾名思义,就是延迟连接的accept。

 

Q:那么使用这个选项有什么好处呢?

A:想象一个场景,三次握手建立连接后,客户端过了很长时间、或者根本没有发送请求,那么服务端

岂不是瞎忙活了。这时如果设置了这个选项,只有当客户端发送带有负荷的ACK过来时,才会真正的建立

连接、分配资源、唤醒监听进程。有人觉得这个选项可以节约连接建立时间,这是没有依据的,节省服务器

端的资源倒是真的。

 

选项的设置:

#define TCP_DEFER_ACCEPT 9 /* Wake up listener only when data arrive */static int do_tcp_setsockopt(struct sock *sk, int level, int optname, char __user *optval,unsigned int optlen)
{...case TCP_DEFER_ACCEPT:/* Translate value in seconds to number of retransmits */icsk->icsk_accept_queue.rskq_defer_accept = secs_to_retrans(val, TCP_TIMEOUT_INIT / HZ,TCP_RTO_MAX / HZ);...
}
/* Convert seconds to retransmits based on initial and max timeout */
static u8 secs_to_retrans(int seconds, int timeout, int rto_max)
{u8 res = 0;if (seconds > 0) {int period = timeout;res = 1;while(seconds > period && res < 255) {res++;timeout <<= 1;if (timeout > rto_max)timeout = rto_max;period += timeout;}}return res;
}

secs_to_retrans()用于把需要延迟的时间,转换为SYNACK的最大重传次数。

我们知道客户端的纯ACK会被无情的丢弃,所以之后客户端如果没有及时发送带有负荷的ACK(就是请求),

会导致服务端SYNACK发生超时,超时后就要重传此SYNACK。

 

Q:假如TCP_DEFER_ACCEPT告诉服务器,要延迟的时间为10s,那么SYNACK的最大重传次数要设成多少呢?

A:用超时时间的指数退避来计算。当SYNACK的最大重传次数为4时,才能等够10s的延迟时间。

第一次重传,等待时间+1s

第二次重传,等待时间+2s

第三次重传,等待时间+4s

第四次重传,等待时间+8s

第四次重传过后,总共的等待时间为15s,已经等够了10s的延迟时间了。

如果客户端的请求还不过来,就放弃这个连接请求块了。

 

放弃连接和重传SYNACK的判断

 

判断是否要丢弃老连接请求块、是否要重传SYNACK包,要分两种情况考虑:

 

(1) 没有使用TCP_DEFER_ACCEPT选项

丢弃条件:超过了动态调整后的最大重传次数。

重传条件:不丢弃时就重传SYNACK。

 

(2) 使用了TCP_DEFER_ACCPET选项

丢弃条件(同时满足):

1. 超过了动态调整后的最大重传次数。

2. 没有收到过纯ACK,或者已经超过了设置的最大延迟时间。

 

重传条件(满足一条即可):

1. 没有收到过纯ACK。

2. 已收到纯ACK,本次是最后一次重传机会了。

/* Decide when to expire the request and when to resend SYN-ACK */static inline void syn_ack_recalc (struct request_sock *req, const int thresh, const int max_retries,const u8 rskq_defer_accept, int *expire, int *resend)
{/* 如果没有使用TCP_DEFER_ACCEPT选项 */if (!rskq_defer_accept) {*expire = req->num_timeout >= thresh; /* 超过了动态调整后的最大重传次数则放弃本连接请求块 */*resend = 1; /* 始终为1,但其实是只有不放弃时(expire为0)才会真的重传 */return;}/* 启用TCP_DEFER_ACCEPT时,放弃的条件更加严格,还需要满足以下两个条件之一:* 1. 没有收到过纯ACK。* 2. 超过了设置的最大延迟时间。* 满足了以上两个条件之一,就不值得再抢救了,已弃疗。*/*expire = req->num_timeout >= thresh && (!inet_rsk(req)->acked || req->num_timeout >= max_entries);/* 要重传SYNACK的情况有两种:* 1. 没有收到过纯ACK时。* 2. 已收到纯ACK,本次是最后一次重传机会了。*//* Do not resend while waiting for data after ACK, start to resend on end of defering period to give* last chance for data or ACK to create established socket.*/*resend = ! inet_rsk(req)->acked || req->num_timeout >= rskq_defer_accept - 1;
}

SYNACK的重传函数。

int inet_rtx_syn_ack(struct sock *parent, struct request_sock *req)
{int err = req->rsk_ops->rtx_syn_ack(parent, req); /* 调用tcp_v4_rtx_synack()来重传SYNACK */if (! err)req->num_retrans++; /* 增加SYNACK的重传次数 */return err;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/459378.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

android -- 蓝牙 bluetooth (二) 打开蓝牙

4.2的蓝牙打开流程这一部分还是有些变化的&#xff0c;从界面上看蓝牙开关就是设置settings里那个switch开关&#xff0c;widget开关当然也可以&#xff0c;起点不同而已&#xff0c;后续的流程是一样的。先来看systemServer.java的代码&#xff0c;蓝牙服务开启的地方&#xf…

开始nodejs+express的学习+实践(8)

为什么80%的码农都做不了架构师&#xff1f;>>> 1.session使用 介绍的非常详细&#xff1a; http://www.cnblogs.com/chenchenluo/p/4197181.html 对比我们的app.js需要引入express-session模块和使用这个模块&#xff0c;在package依赖&#xff0c;并加载。 我们修…

TCP的定时器系列 — 保活定时器(有图有代码有真相!!!)

转载 主要内容&#xff1a;保活定时器的实现&#xff0c;TCP_USER_TIMEOUT选项的实现。 内核版本&#xff1a;3.15.2 我的博客&#xff1a;http://blog.csdn.net/zhangskd 原理 HTTP有Keepalive功能&#xff0c;TCP也有Keepalive功能&#xff0c;虽然都叫Keepalive&#xff0c…

浅谈 Scala 中下划线的用途

Scala 作为一门函数式编程语言&#xff0c;对习惯了指令式编程语言的同学来说&#xff0c;会不大习惯&#xff0c;这里除了思维方式之外&#xff0c;还有语法层面的&#xff0c;比如 underscore&#xff08;下划线&#xff09;就会出现在多种场合&#xff0c;令初学者相当疑惑&…

maven项目部署到linux上的奇葩问题

2019独角兽企业重金招聘Python工程师标准>>> 经常会遇到这样子的问题&#xff0c;maven项目在本地的eclipse配置的好好的&#xff0c;结果一到服务器就运行不起来。 当然遇到这种情况&#xff0c;我们首先会想到环境变量和相关的路径问题&#xff0c;但是当这两个条…

TCP的定时器系列 — 零窗口探测定时器(有图有代码有真相!!!)

转载 主要内容&#xff1a;零窗口探测定时器的实现。 内核版本&#xff1a;3.15.2 我的博客&#xff1a;http://blog.csdn.net/zhangskd 出现以下情况时&#xff0c;TCP接收方的接收缓冲区将被塞满数据&#xff1a; 发送方的发送速度大于接收方的接收速度。 接收方的应用程序未…

java中XPATH操作xml,非常便捷

<?xml version"1.0" encoding"UTF-8"?> <MessageList><item type"1"><template_id value"p2ItJPj0taTTP4QRXP-z51nYuD3aDNhgvLOusWGY4p0"/><topcolor value"#173177"/><first value&quo…

【python】r+,w+ 全局变量

来源&#xff1a;http://www.educity.cn/wenda/352188.html r&#xff1a;可读可写&#xff0c;若文件不存在&#xff0c;报错w: 可读可写&#xff0c;若文件不存在&#xff0c;创建文本模式&#xff1a;遇换行符时根据操作系统不同自动转换换行符&#xff0c;比如读文件时遇\n…

网络:TCP通讯之 time_wait 状态

基于TCP协议的通讯流程1、TCP建立连接2、TCP断开连接3、TCP状态转换TCP状态解释&#xff1a; SYN-RECVD&#xff1a;再收到和发送一个连接请求后等待对方对连接请求的确认 ESTABLISHED&#xff1a;代表一个打开的连接 FIN-WAIT-1&#xff1a;等待远程TCP连接中断请求&#xff0…

linux下echo与time服务的程序实现

一、针对ECHO服务的TCP客户软件的实现 1.网络拓扑结构&#xff1a; 2.源码&#xff1a; 1 #include <stdio.h>2 #include <stdlib.h>3 #include <unistd.h>4 #include <string.h>5 #include <stdarg.h>6 #include <sys/types.h>7 #include…

iOS UI-AlertView(警示框)和ActionSheet(选择框、操作表单)

1 #import "ViewController.h"2 3 interface ViewController ()<UIAlertViewDelegate,UIActionSheetDelegate>4 5 end6 7 implementation ViewController8 9 #pragma mark - 生命周期10 - (void)viewDidLoad {11 [super viewDidLoad];12 // 创建展示Al…

Linux高性能服务器编程:进程池和线程池原理及应用(有图有代码有真相!!!)

一、问题引入 在前面编写多进程、多线程服务器时通过动态创建子进程和子线程来实现并发服务器&#xff0c;这样做有以下缺点&#xff1a; 1&#xff09;动态创建进程、线程将会比较耗费时间&#xff0c;将导致较慢的客户响应。 2&#xff09;动态创建的子进程只为一个客户服…

Linux:多进程、多线程服务器的实现解析(有图有代码有真相!!!)

一、问题引入 阻塞型的网络编程接口 几乎所有的程序员第一次接触到的网络编程都是从 listen()、send()、recv()等接口开始的。使用这些接口可以很方便的构建服务器 /客户机的模型。 我们假设希望建立一个简单的服务器程序&#xff0c;实现向单个客户机提供类似于“一问一答”的…

数据结构:神奇的B树实现解析(有图有代码有真相!!!)

一、B树引入 二叉搜索树、平衡二叉树、红黑树都是动态查找树&#xff0c;典型的二叉搜索树结构&#xff0c;查找的时间复杂度和树的高度相关O(log2N)。 1&#xff09;数据杂乱无章-------线性查找--O&#xff08;n&#xff09; 2&#xff09;数据有序-------二分查找 ---O(lo…

Linux:dup/dup2 文件描述符重定向函数(有图有代码有真相!!!)

一、dup/dup2 有时我们希望把标准输入重定向到一个文件&#xff0c;或者把标准输出重定向到一个网络连接。系统调用dup和dup2能够复制文件描述符。dup返回新的文件文件描述符&#xff08;没有用的文件描述符最小的编号&#xff09;。 dup2可以让用户指定返回的文件描述符的值…

Linux:I/O多路转接之select(有图有代码有真相!!!)

一、select引入 一次 I/O 分为两个部分&#xff1a;1&#xff09;等待数据就绪 2&#xff09;进行数据转移 1、select 原理&#xff1a; select的原理就是减少等待数据就绪的比重&#xff0c;巧妙的利用等待队列机制让用户进程适当在没有资源可读/写时睡眠&#xff0c;有资…

Linux: I/O多路转接之poll(有图有代码有真相!!!)

一、poll()函数解析 不同与select使⽤用三个位图来表⽰示三个fdset的⽅方式&#xff0c;poll使⽤用⼀一个 pollfd的指针实现。pollfd结构包含了要监视的event和发⽣生的event&#xff0c; 不再使⽤用select“参数-值”传递的⽅方式。同时&#xff0c;pollfd并没有最⼤大数量限…

kalilinux装到u盘上的弊端_付费下载的歌曲,竟然无法在汽车上播放!原因在这里...

『使用某音乐播放器下载了周杰伦的110首歌曲&#xff0c;其中106首是kgm格式&#xff0c;4首mp3格式&#xff0c;装到U盘后&#xff0c;在其它设备播放只有4首mp3格式的可以播放&#xff0c;其它的均无法播放&#xff0c;请问该如何处理&#xff1f;』网友留言截图这是一位网友…

iconsvg image怎么变为path_昆凌是怎么收服天王周杰伦的?这几招太高明了

周杰伦和昆凌又出来撒狗粮了&#xff01;就在前两天(6月2日)&#xff0c;在参加郎朗的婚礼时&#xff0c;#周杰伦搂昆凌看烟花#的消息悄悄上了热搜。视频中&#xff0c;两人并肩站立&#xff0c;一起欣赏着窗外的美景。周杰伦时不时在昆凌的耳边私语几句&#xff0c;看起来很是…

Linux: I/O多路转接之epoll(有图有代码有真相!!!)

一、基本概念 epoll是Linux内核为处理大批量文件描述符而作了改进的poll&#xff0c;是Linux下多路复用IO接口select/poll的增强版本&#xff0c;它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。 另一点原因就是获取事件的时候&#xff0c;它无须遍历整…