TCP的定时器系列 — 保活定时器(有图有代码有真相!!!)
轉載
主要內容:保活定時器的實現,TCP_USER_TIMEOUT選項的實現。
內核版本:3.15.2
我的博客:http://blog.csdn.net/zhangskd
?
原理
?
HTTP有Keepalive功能,TCP也有Keepalive功能,雖然都叫Keepalive,但是它們的目的卻是不一樣的。
為了說明這一點,先來看下長連接和短連接的定義。
?
連接的“長短”是什么?
短連接:建立一條連接,傳輸一個請求,馬上關閉連接。
長連接:建立一條連接,傳輸一個請求,過會兒,又傳輸若干個請求,最后再關閉連接。
長連接的好處是顯而易見的,多個請求可以復用一條連接,省去連接建立和釋放的時間開銷和系統調用,
但也意味著服務器的一部分資源會被長時間占用著。
?
HTTP的Keepalive,顧名思義,目的在于延長連接的時間,以便在同一條連接中傳輸多個HTTP請求。
HTTP服務器一般會提供Keepalive Timeout參數,用來決定連接保持多久,什么時候關閉連接。
當連接使用了Keepalive功能時,對于客戶端發送過來的一個請求,服務器端會發送一個響應,然后開始計時,
如果經過Timeout時間后,客戶端沒有再發送請求過來,服務器端就把連接關了,不再保持連接了。
?
TCP的Keepalive,是掛羊頭賣狗肉的,目的在于看看對方有沒有發生異常,如果有異常就及時關閉連接。
當傳輸雙方不主動關閉連接時,就算雙方沒有交換任何數據,連接也是一直有效的。
如果這個時候對端、中間網絡出現異常而導致連接不可用,本端如何得知這一信息呢?
答案就是保活定時器。它每隔一段時間會超時,超時后會檢查連接是否空閑太久了,如果空閑的時間超過
了設置時間,就會發送探測報文。然后通過對端是否響應、響應是否符合預期,來判斷對端是否正常,
如果不正常,就主動關閉連接,而不用等待HTTP層的關閉了。
?
當服務器發送探測報文時,客戶端可能處于4種不同的情況:仍然正常運行、已經崩潰、已經崩潰并重啟了、
由于中間鏈路問題不可達。在不同的情況下,服務器會得到不一樣的反饋。
?
(1) 客戶主機依然正常運行,并且從服務器端可達
客戶端的TCP響應正常,從而服務器端知道對方是正常的。保活定時器會在兩小時以后繼續觸發。
?
(2) 客戶主機已經崩潰,并且關閉或者正在重新啟動
客戶端的TCP沒有響應,服務器沒有收到對探測包的響應,此后每隔75s發送探測報文,一共發送9次。
socket函數會返回-1,errno設置為ETIMEDOUT,表示連接超時。
?
(3) 客戶主機已經崩潰,并且重新啟動了
客戶端的TCP發送RST,服務器端收到后關閉此連接。
socket函數會返回-1,errno設置為ECONNRESET,表示連接被對端復位了。
?
(4) 客戶主機依然正常運行,但是從服務器不可達
雙方的反應和第二種是一樣的,因為服務器不能區分對端異常與中間鏈路異常。
socket函數會返回-1,errno設置為EHOSTUNREACH,表示對端不可達。
?
選項
?
內核默認并不使用TCP Keepalive功能,除非用戶設置了SO_KEEPALIVE選項。
有兩種方式可以自行調整保活定時器的參數:一種是修改TCP參數,一種是使用TCP層選項。
?
(1) TCP參數
tcp_keepalive_time
最后一次數據交換到TCP發送第一個保活探測報文的時間,即允許連接空閑的時間,默認為7200s。
tcp_keepalive_intvl
保活探測報文的重傳時間,默認為75s。
tcp_keepalive_probes
保活探測報文的發送次數,默認為9次。
?
Q:一次完整的保活探測需要花費多長時間?
A:tcp_keepalive_time + tcp_keepalive_intvl * tcp_keepalive_probes,默認值為7875s。
如果覺得兩個多小時太長了,可以自行調整上述參數。
?
(2) TCP層選項
TCP_KEEPIDLE:含義同tcp_keepalive_time。
TCP_KEEPINTVL:含義同tcp_keepalive_intvl。
TCP_KEEPCNT:含義同tcp_keepalive_probes。
?
Q:既然有了TCP參數可供調整,為什么還增加了上述的TCP層選項?
A:TCP參數是面向本機的所有TCP連接,一旦調整了,對所有的連接都有效。
而TCP層選項是面向一條連接的,一旦調整了,只對本條連接有效。
?
激活
?
在連接建立后,可以通過設置SO_KEEPALIVE選項,來激活保活定時器。
int keepalive = 1;
setsockopt(fd, SOL_SOCKET, SO_KEEPALIVE, &keepalive, sizeof(keepalive));
int sock_setsockopt(struct socket *sock, int level, int optname, char __user *optval, unsigned int optlen) {...case SO_KEEPALIVE: #ifdef CONFIG_INETif (sk->sk_protocol == IPPROTO_TCP && sk->sk_type == SOCK_STREAM)tcp_set_keepalive(sk, valbool); /* 激活或刪除保活定時器 */ #endifsock_valbool_flag(sk, SOCK_KEEPOPEN, valbool); /* 設置或取消SOCK_KEEPOPEN標志位 */break;... }static inline void sock_valbool_flag (struct sock *sk, int bit, int valbool) {if (valbool)sock_set_flag(sk, bit);elsesock_reset_flag(sk, bit); } void tcp_set_keepalive(struct sock *sk, int val) {/* 不在以下兩個狀態設置保活定時器:* TCP_CLOSE:sk_timer用作FIN_WAIT2定時器* TCP_LISTEN:sk_timer用作SYNACK重傳定時器*/if ((1 << sk->sk_state) & (TCPF_CLOSE | TCPF_LISTEN))return;/* 如果SO_KEEPALIVE選項值為1,且此前沒有設置SOCK_KEEPOPEN標志,* 則激活sk_timer,用作保活定時器。*/if (val && !sock_flag(sk, SOCK_KEEPOPEN))inet_csk_reset_keepalive_timer(sk, keepalive_time_when(tcp_sk(sk)));else if (!val)/* 如果SO_KEEPALIVE選項值為0,則刪除保活定時器 */inet_csk_delete_keepalive_timer(sk); }/* 保活定時器的超時時間 */ static inline int keepalive_time_when(const struct tcp_sock *tp) {return tp->keepalive_time ? : sysctl_tcp_keepalive_time; }void inet_csk_reset_keepalive_timer (struc sock *sk, unsigned long len) {sk_reset_timer(sk, &sk->sk_timer, jiffies + len); }可以使用TCP層選項來動態調整保活定時器的參數。
int keepidle = 600;
int keepintvl = 10;
int keepcnt = 6;
setsockopt(fd, SOL_TCP, TCP_KEEPIDLE, &keepidle, sizeof(keepidle));
setsockopt(fd, SOL_TCP, TCP_KEEPINTVL, &keepintvl, sizeof(keepintvl));
setsockopt(fd, SOL_TCP, TCP_KEEPCNT, &keepcnt, sizeof(keepcnt));
struct tcp_sock {.../* 最后一次接收到ACK的時間 */u32 rcv_tstamp; /* timestamp of last received ACK (for keepalives) */.../* time before keep alive takes place, 空閑多久后才發送探測報文 */unsigned int keepalive_time;/* time iterval between keep alive probes */unsigned int keepalive_intvl; /* 探測報文之間的時間間隔 *//* num of allowed keep alive probes */u8 keepalive_probes; /* 探測報文的發送次數 */...struct {.../* 最后一次接收到帶負荷的報文的時間 */__u32 lrcvtime; /* timestamp of last received data packet */...} icsk_ack;... };#define TCP_KEEPIDLE 4 /* Start Keepalives after this period */ #define TCP_KEEPINTVL 5 /* Interval between keepalives */ #define TCP_KEEPCNT 6 /* Number of keepalives before death */#define MAX_TCP_KEEPIDLE 32767 #define MAX_TCP_KEEPINTVL 32767 #define MAX_TCP_KEEPCNT 127 static int do_tcp_setsockopt(struct sock *sk, int level, int optname, char __user *optval,unsigned int optlen) {...case TCP_KEEPIDLE:if (val < 1 || val > MAX_TCP_KEEPIDLE)err = -EINVAL;else {tp->keepalive_time = val * HZ; /* 設置新的空閑時間 *//* 如果有使用SO_KEEPALIVE選項,連接處于非監聽非結束的狀態。* 這個時候保活定時器已經在計時了,這里設置新的超時時間。*/if (sock_flag(sk, SOCK_KEEPOPEN) && !((1 << sk->sk_state) & (TCPF_CLOSE | TCPF_LISTEN))) {u32 elapsed = keepalive_time_elapsed(tp); /* 連接已經經歷的空閑時間 */if (tp->keepalive_time > elapsed)elapsed = tp->keepalive_time - elapsed; /* 接著等待的時間,然后超時 */elseelapsed = 0; /* 會導致馬上超時 */inet_csk_reset_keepalive_timer(sk, elapsed);}}break;case TCP_KEEPINTVL:if (val < 1 || val > MAX_TCP_KEEPINTVL)err = -EINVAL;elsetp->keepalive_intvl = val * HZ; /* 設置新的探測報文間隔 */break;case TCP_KEEPCNT:if (val < 1 || val > MAX_TCP_KEEPCNT)err = -EINVAL;elsetp->keepalive_probes = val; /* 設置新的探測次數 */break;... }到目前為止,連接已經經歷的空閑時間,即最后一次接收到報文至今的時間。
static inline u32 keepalive_time_elapsed (const struct tcp_sock *tp) {const struct inet_connection_sock *icsk = &tp->inet_conn;/* lrcvtime是最后一次接收到數據報的時間* rcv_tstamp是最后一次接收到ACK的時間* 返回值就是最后一次接收到報文,到現在的時間,即經歷的空閑時間。*/return min_t(u32, tcp_time_stamp - icsk->icsk_ack.lrcvtime,tcp_time_stamp - tp->rcv_tstamp); }?
超時處理函數
?
我們知道保活定時器、SYNACK重傳定時器、FIN_WAIT2定時器是共用一個定時器實例sk->sk_timer,
所以它們的超時處理函數也是一樣的,都為tcp_keepalive_timer()。
而在函數內部,可以根據此時連接所處的狀態,來判斷是哪個定時器觸發了超時。
?
Q:什么時候判斷對端為異常并關閉連接?
A:分兩種情況。
1. 用戶使用了TCP_USER_TIMEOUT選項。當連接的空閑時間超過了用戶設置的時間,且有發送過探測報文。
2. 用戶沒有使用TCP_USER_TIMEOUT選項。當發送保活探測包的次數達到了保活探測的最大次數時。
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再來看看。*/if (sock_owned_by_user(sk)) {/* Try again later. */inet_csk_reset_keepalive_timer(sk, HZ/20);goto out;}/* 三次握手期間,用作SYNACK定時器 */if (sk->sk_state == TCP_LISTEN) {tcp_synack_timer(sk);goto out;} /* 連接釋放期間,用作FIN_WAIT2定時器 */if (sk->sk_state == TCP_FIN_WAIT2 && sock_flag(sk, SOCK_DEAD)) {...}/* 接下來就是用作保活定時器了 */if (!sock_flag(sk, SOCK_KEEPOPEN) || sk->sk_state == TCP_CLOSE)goto out;elapsed = keepalive_time_when(tp); /* 連接的空閑時間超過此值,就發送保活探測報文 *//* It is alive without keepalive.* 如果網絡中有發送且未確認的數據包,或者發送隊列不為空,說明連接不是idle的?* 既然連接不是idle的,就沒有必要探測對端是否正常。* 保活定時器重新開始計時即可。* * 而實際上當網絡中有發送且未確認的數據包時,對端也可能會發生異常而沒有響應。* 這個時候會導致數據包的不斷重傳,只能依靠重傳超過了允許的最大時間,來判斷連接超時。* 為了解決這一問題,引入了TCP_USER_TIMEOUT,允許用戶指定超時時間,可見下文:)*/if (tp->packets_out || tcp_send_head(sk))goto resched; /* 保活定時器重新開始計時 *//* 連接經歷的空閑時間,即上次收到報文至今的時間 */elapsed = keepalive_time_elapsed(tp);/* 如果連接空閑的時間超過了設置的時間值 */if (elapsed >= keepalive_time_when(tp)) {/* 什么時候關閉連接?* 1. 使用了TCP_USER_TIMEOUT選項。當連接空閑時間超過了用戶設置的時間,且有發送過探測報文。* 2. 用戶沒有使用選項。當發送的保活探測包達到了保活探測的最大次數。*/if (icsk->icsk_user_timeout != 0 && elapsed >= 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); /* 構造一個RST包并發送 */tcp_write_err(sk); /* 報告錯誤,關閉連接 */goto out;}/* 如果還不到關閉連接的時候,就繼續發送保活探測包 */if (tcp_write_wakeup(sk) <= 0) {icsk->icsk_probes_out++; /* 已發送的保活探測包個數 */elapsed = keepalive_intvl_when(tp); /* 下次超時的時間,默認為75s */} else {/* If keepalive was lost due to local congestion, try harder. */elapsd = TCP_RESOURCE_PROBE_INTERVAL; /* 默認為500ms,會使超時更加頻繁 */}} else {/* 如果連接的空閑時間,還沒有超過設定值,則接著等待 */elapsed = keepalive_time_when(tp) - elapsed;} sk_mem_reclaim(sk);resched: /* 重設保活定時器 */inet_csk_reset_keepalive_timer(sk, elapsed);goto out; out:bh_unlock_sock(sk);sock_put(sk); }
Q:TCP是如何發送Keepalive探測報文的?
A:分兩種情況。
1. 有新的數據段可供發送,且對端接收窗口還沒被塞滿。發送新的數據段,來作為探測包。
2. 沒有新的數據段可供發送,或者對端的接收窗口滿了。發送序號為snd_una - 1、長度為0的ACK包作為探測包。
/* Initiate keepalive or window probe from timer. */int tcp_write_wakeup (struct sock *sk) {struct tcp_sock *tp = tcp_sk(sk);struct sk_buff *skb;if (sk->sk_state == TCP_CLOSE)return -1;/* 如果還有未發送過的數據包,并且對端的接收窗口還沒有滿 */if ((skb = tcp_send_head(sk)) != NULL && before(TCP_SKB_CB(skb)->seq, tcp_wnd_end(tp))) {int err;unsigned int mss = tcp_current_mss(sk); /* 當前的MSS *//* 對端接收窗口所允許的最大報文長度 */unsigned int seg_size = tcp_wnd_end(tp) - TCP_SKB_CB(skb)->seq;/* pushed_seq記錄發送出去的最后一個字節的序號 */if (before(tp->pushed_seq, TCP_SKB_CB(skb)->end_seq))tp->pushed_seq = TCP_SKB_CB(skb)->end_seq;/* 如果對端接收窗口小于此數據段的長度,或者此數據段的長度超過了MSS,那么就要進行分段 */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; /* 設置PSH標志,讓對端馬上把數據提交給程序 */if (tcp_fragment(sk, skb, seg_size, mss)) /* 進行分段 */return -1;} else if (! tcp_skb_pcount(skb)) /* 進行TSO分片 */tcp_set_skb_tso_segs(sk, skb, mss); /* 初始化分片相關變量 */TCP_SKB_CB(skb)->tcp_flags |= TCPHDR_PSH;TCP_SKB_CB(skb)->when = tcp_time_stamp;err = tcp_transmit_skb(sk, skb, 1, GFP_ATOMIC); /* 發送此數據段 */if (!err)tcp_event_new_data_sent(sk, skb); /* 發送了新的數據,更新相關參數 */} else { /* 如果沒有新的數據段可用作探測報文發送,或者對端的接收窗口為0 *//* 處于緊急模式時,額外發送一個序號為snd_una的ACK包,告訴對端緊急指針 */if (between(tp->snd_up, tp->snd_una + 1, tp->snd_una + 0xFFFF))tcp_xmit_probe_skb(sk, 1);/* 發送一個序號為snd_una -1的ACK包,長度為0,這是一個序號過時的報文。* snd_una: first byte we want an ack for,所以snd_una - 1序號的字節已經被確認過了。* 對端會響應一個ACK。*/return tcp_xmit_probe_skb(sk, 0);} }?
Q:當沒有新的數據可以用作探測包、或者對端的接收窗口為0時,怎么辦呢?
A:發送一個序號為snd_una - 1、長度為0的ACK包,對端收到此包后會發送一個ACK響應。
如此一來本端就能夠知道對端是否還活著、接收窗口是否打開了。
/* This routine sends a packet with an out of date sequence number.* It assumes the other end will try to ack it.* * Question: what should we make while urgent mode?* 4.4BSD forces sending single byte of data. We cannot send out of window* data, because we have SND.NXT == SND.MAX...* * Current solution: to send TWO zero-length segments in urgent mode:* one is with SEG.SEG=SND.UNA to deliver urgent pointer, another is out-of-date with* SND.UNA - 1 to probe window.*/static int tcp_xmit_probe_skb (struct sock *sk, int urgent) {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_atomic(sk, GFP_ATOMIC));if (skb == NULL)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.*//* 如果沒有設置緊急指針,那么發送的序號為snd_una - 1,否則發送的序號為snd_una */tcp_init_nondata_skb(skb, tp->snd_una - !urgent, TCPHDR_ACK);TCP_SKB_CB(skb)->when = tcp_time_stamp;return tcp_transmit_skb(sk, skb, 0, GFP_ATOMIC); /* 發送探測包 */ }發送RST包。
/* We get here when a process closes a file descriptor (either due to an explicit close()* or as a byproduct of exit()'ing) and there was unread data in the receive queue.* This behavior is recommended by RFC 2525, section 2.17. -DaveM*/void tcp_send_active_reset (struct sock *sk, gfp_t priority) {struct sk_buff *skb;/* NOTE: No TCP options attached and we never retransmit this. */skb = alloc_skb(MAX_TCP_HEADER, priority);if (!skb) {NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPABORTFAILED);return;}/* Reserve space for headers and prepare control bits. */skb_reserve(skb, MAX_TCP_HEADER); /* 為報文頭部預留空間 *//* 初始化不攜帶數據的skb的一些控制字段 */tcp_init_nondata_skb(skb, tcp_acceptable_seq(sk), TCPHDR_ACK | TCPHDR_RST);/* Send if off,發送此RST包*/TCP_SKB_CB(skb)->when = tcp_time_stamp;if (tcp_transmit_skb(sk, skb, 0, priority))NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPABORTFAILED);TCP_INC_STATS(sock_net(sk), TCP_MIB_OUTRSTS); }static inline __u32 tcp_acceptable_seq (const struct sock *sk) {const struct tcp_sock *tp = tcp_sk(sk);/* 如果snd_nxt在對端接收窗口范圍內 */if (! before(tcp_wnd_end(tp), tp->snd_nxt))return tp->snd_nxt;elsereturn tcp_wnd_end(tp); }?
TCP_USER_TIMEOUT選項
?
從上文可知同時符合以下條件時,保活定時器才會發送探測報文:
1. 網絡中沒有發送且未確認的數據包。
2. 發送隊列為空。
3. 連接的空閑時間超過了設定的時間。
Q:如果網絡中有發送且未確認的數據包、或者發送隊列不為空時,保活定時器不起作用了,
豈不是不能夠檢測到對端的異常了?
A:可以使用TCP_USER_TIMEOUT,顯式的指定當發送數據多久后還沒有得到響應,就判定連接超時,
從而主動關閉連接。
?
TCP_USER_TIMEOUT選項會影響到超時重傳定時器和保活定時器。
?
(1) 超時重傳定時器
判斷連接是否超時,分3種情況:
1. SYN包:當SYN包的重傳次數達到上限時,判定連接超時。(默認允許重傳5次,初始超時時間為1s,總共歷時31s)
2. 非SYN包,用戶使用TCP_USER_TIMEOUT:當數據包發出去后的等待時間超過用戶設置的時間時,判定連接超時。
3. 非SYN包,用戶沒有使用TCP_USER_TIMEOUT:當數據包發出去后的等待時間超過以TCP_RTO_MIN為初始超時
時間,重傳boundary次所花費的時間后,判定連接超時。(boundary的最大值為tcp_retries2,默認值為15)
?
(2) 保活定時器
判斷連接是否異常,分2種情況:
1. 用戶使用了TCP_USER_TIMEOUT選項。當連接的空閑時間超過了用戶設置的時間,且有發送過探測報文。
2. 用戶沒有使用TCP_USER_TIMEOUT選項。當發送保活探測包的次數達到了保活探測的最大次數時。
總結
以上是生活随笔為你收集整理的TCP的定时器系列 — 保活定时器(有图有代码有真相!!!)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Linux elf文件分析
- 下一篇: Java 数据库image型输出图片