原文連接:http://www.pagefault.info/?p=346服務器
TCP_DEFER_ACCEPT這個選項可能你們都知道,不過我這裏會從源碼和數據包來詳細的分析這個選項。要注意,這裏我所使用的內核版本是3.0.socket
首先看man手冊中的介紹(man 7 tcp):tcp
TCP_DEFER_ACCEPT (since Linux 2.4)
Allow a listener to be awakened only when data arrives on the socket. Takes an integer value (seconds), this can bound the maximum number of attempts TCP will make to complete the connection. This option should not be used in code intended to be portable.函數
我先來簡單介紹下,這個選項主要是針對server端的服務器,通常來講咱們三次握手,當客戶端發送syn,而後server端接收到,而後發送 syn + ack,而後client接收到syn+ack以後,再次發送ack(client進入establish狀態),最終server端收到最後一個 ack,進入establish狀態。this
而當正確的設置了TCP_DEFER_ACCEPT選項以後,server端會在接收到最後一個ack以後,並不進入establish狀態,而只 是將這個socket標記爲acked,而後丟掉這個ack。此時server端這個socket仍是處於syn_recved,而後接下來就是等待 client發送數據, 而因爲這個socket仍是處於syn_recved,所以此時就會被syn_ack定時器所控制,對syn ack進行重傳,而重傳次數是由咱們設置TCP_DEFER_ACCEPT傳進去的值以及TCP_SYNCNT選項,proc文件系統的 tcp_synack_retries一塊兒來決定的(後面分析源碼會看到如何來計算這個值).而咱們知道咱們傳遞給TCP_DEFER_ACCEPT的是 秒,而在內核裏面會將這個東西轉換爲重傳次數.
咱們來看抓包,這裏server端設置deffer accept,而後客戶端connect並不發送數據,咱們來看會發生什麼:spa
//客戶端發送syn 19:38:20.631611 IP T-diaoliang.60277 > T-diaoliang.sunproxyadmin: Flags [S], seq 2500439144, win 32792, options [mss 16396,sackOK,TS val 9008384 ecr 0,nop,wscale 4], length 0 //server回了syn+ack 19:38:20.631622 IP T-diaoliang.sunproxyadmin > T-diaoliang.60277: Flags [S.], seq 1342179593, ack 2500439145, win 32768, options [mss 16396,sackOK,TS val 9008384 ecr 9008384,nop,wscale 4], length 0 //client發送最後一個ack 19:38:20.631629 IP T-diaoliang.60277 > T-diaoliang.sunproxyadmin: Flags [.], ack 1, win 2050, options [nop,nop,TS val 9008384 ecr 9008384], length 0 //這裏注意時間,能夠看到過了大概1分半以後,server從新發送了syn+ack 19:39:55.035893 IP T-diaoliang.sunproxyadmin > T-diaoliang.60277: Flags [S.], seq 1342179593, ack 2500439145, win 32768, options [mss 16396,sackOK,TS val 9036706 ecr 9008384,nop,wscale 4], length 0 19:39:55.035899 IP T-diaoliang.60277 > T-diaoliang.sunproxyadmin: Flags [.], ack 1, win 2050, options [nop,nop,TS val 9036706 ecr 9036706,nop,nop,sack 1 {0:1}], length 0 //再過了1分鐘,server close掉這條鏈接。 19:40:55.063435 IP T-diaoliang.sunproxyadmin > T-diaoliang.60277: Flags [F.], seq 1, ack 1, win 2048, options [nop,nop,TS val 9054714 ecr 9036706], length 0 19:40:55.063692 IP T-diaoliang.60277 > T-diaoliang.sunproxyadmin: Flags [F.], seq 1, ack 2, win 2050, options [nop,nop,TS val 9054714 ecr 9054714], length 0 19:40:55.063701 IP T-diaoliang.sunproxyadmin > T-diaoliang.60277: Flags [.], ack 2, win 2048, options [nop,nop,TS val 9054714 ecr 9054714], length 0
接下來就來看內核的代碼。code
先從設置TCP_DEFER_ACCEPT開始,設置TCP_DEFER_ACCEPT是經過setsockopt來作的,而傳遞給內核的值是秒, 下面就是內核中對應的do_tcp_setsockopt函數,它用來設置tcp相關的option,下面咱們能看到主要就是將傳遞進去的val轉換爲將 要重傳的次數。server
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); break;
這裏能夠看到經過調用secs_to_retrans來將秒轉換爲重傳次數。接下來就來看這個函數,它有三個參數,第一個是將要轉換的秒,第二個是 RTO的初始值,第三個是RTO的最大值。 能夠看到這裏都是依據RTO來計算的,這是由於這個重傳次數是syn_ack的重傳次數。blog
這個函數實現很簡單,就是一個定時器退避的計算過程(定時器退避能夠看我前面的blog的介紹),每次乘2,而後來計算重傳次數。隊列
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; }
而後來看當server端接收到最後一個ack的處理,這裏只關注defer_accept的部分,這個函數是tcp_check_req,它主要用來檢測SYN_RECV狀態接收到包的校驗。
req->retrans表示已經重傳的次數。
acked標記主要是爲了syn_ack定時器來使用的。
//兩個條件,一個是重傳次數小於defer_accept,一個是序列號,這兩個都必須知足。 if (req->retrans < inet_csk(sk)->icsk_accept_queue.rskq_defer_accept && TCP_SKB_CB(skb)->end_seq == tcp_rsk(req)->rcv_isn + 1) { //此時設置acked。 inet_rsk(req)->acked = 1; NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_TCPDEFERACCEPTDROP); return NULL; }
而當tcp_check_req返回以後,在tcp_v4_do_rcv中會丟掉這個包,讓socket繼續保存在半鏈接隊列中。
而後來看syn ack定時器,這個定時器我之前有分析過(http://simohayha.iteye.com/admin/blogs/481989)
,所以我這裏只是簡要的再次分析下。若是須要更詳細的分析,能夠看我上面的連接,這個定時器會調用inet_csk_reqsk_queue_prune函數,在這個函數中作相關的處理。
這裏咱們就主要關注重試次數。其中icsk_syn_retries是TCP_SYNCNT這個option設置的。這個值會比 sysctl_tcp_synack_retries優先.而後是rskq_defer_accept,它又比icsk_syn_retries優先.
void inet_csk_reqsk_queue_prune(struct sock *parent, const unsigned long interval, const unsigned long timeout, const unsigned long max_rto) { ........................ //最大的重試次數 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 (queue->rskq_defer_accept) max_retries = queue->rskq_defer_accept; budget = 2 * (lopt->nr_table_entries / (timeout / interval)); i = lopt->clock_hand; do { reqp=&lopt->syn_table[i]; while ((req = *reqp) != NULL) { if (time_after_eq(now, req->expires)) { int expire = 0, resend = 0; //這個函數主要是判斷超時和是否從新發送syn ack,而後保存在expire和resend這個變量中。 syn_ack_recalc(req, thresh, max_retries, queue->rskq_defer_accept, &expire, &resend); .................................................... if (!expire && (!resend || !req->rsk_ops->rtx_syn_ack(parent, req, NULL) || inet_rsk(req)->acked)) { unsigned long timeo; //更新重傳次數. if (req->retrans++ == 0) lopt->qlen_young--; timeo = min((timeout << req->retrans), 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); ............................................... }