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; }總結
以上是生活随笔為你收集整理的TCP的定时器系列 — SYNACK定时器的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 计算机一级题库百度云0,全国计算机等级考
- 下一篇: Matrix Calculus Refe