Linux內核提供了可設置的TCP用戶超時時長(TCP User Timeout),其控制發送的未確認數據可保持多長時間,之後強制關閉連接。但是,內核不支持RFC5482定義的TCP UTO選項(User Timeout Option),不會將此設置通告給對端,其爲本地超時時長。
UTO用戶接口
應用層可通過setsockopt選項TCP_USER_TIMEOUT設置超時時長,內核將其保存在icsk_user_timeout變量中,單位爲毫秒。
static int do_tcp_setsockopt(struct sock *sk, int level,
int optname, char __user *optval, unsigned int optlen)
{
switch (optname) {
case TCP_USER_TIMEOUT:
/* Cap the max time in ms TCP will retry or probe the window
* before giving up and aborting (ETIMEDOUT) a connection.
*/
if (val < 0)
err = -EINVAL;
else
icsk->icsk_user_timeout = val;
break;
UTO超時
如下函數tcp_clamp_rto_to_user_timeout,其確保RTO值不超出UTO定義的時長。如果用戶未設置UTO,或者當前連接也未進行過報文重傳,使用變量icsk_rto中的RTO值,來設置TCP的重傳超時定時器。否則,計算重傳報文開始到當前經過的時長,之後計算到UTO時長剩餘的時長,如果剩餘時長小於等於零,取值1,否者,取RTO值和剩餘時長兩者之間的較小值,作爲重傳超時定時器的時長。
static u32 tcp_clamp_rto_to_user_timeout(const struct sock *sk)
{
struct inet_connection_sock *icsk = inet_csk(sk);
u32 elapsed, start_ts;
s32 remaining;
start_ts = tcp_retransmit_stamp(sk);
if (!icsk->icsk_user_timeout || !start_ts)
return icsk->icsk_rto;
elapsed = tcp_time_stamp(tcp_sk(sk)) - start_ts;
remaining = icsk->icsk_user_timeout - elapsed;
if (remaining <= 0)
return 1; /* user timeout has passed; fire ASAP */
return min_t(u32, icsk->icsk_rto, msecs_to_jiffies(remaining));
}
函數tcp_retransmit_timer中的調用inet_csk_reset_xmit_timer,使用以上的函數計算要設置的ICSK_TIME_RETRANS定時器的時長。注意最後一個參數爲TCP_RTO_MAX(120秒),其定義了超時的最大時間值,UTO的設置值如果過大,將不會生效。
void tcp_retransmit_timer(struct sock *sk)
{
...
inet_csk_reset_xmit_timer(sk, ICSK_TIME_RETRANS,
tcp_clamp_rto_to_user_timeout(sk), TCP_RTO_MAX);
if (retransmits_timed_out(sk, net->ipv4.sysctl_tcp_retries1 + 1, 0))
__sk_dst_reset(sk);
UTO超時判斷
在報文超時之後,函數tcp_write_timeout負責超時相關處理,判斷此連接是否已不可用。
static int tcp_write_timeout(struct sock *sk)
{
...
if ((1 << sk->sk_state) & (TCPF_SYN_SENT | TCPF_SYN_RECV)) {
i...
} else {
...
expired = retransmits_timed_out(sk, retry_until, icsk->icsk_user_timeout);
在函數retransmits_timed_out中判斷此鏈接是否已經超時,如果第三個參數UTO不爲零,檢測在報文重傳之後,是否已經經過了UTO的時長,爲真的話,返回1,表示此連接已經超時。否則,在UTO爲零時,通過重傳次數和退避算法計算超時時長。
static bool retransmits_timed_out(struct sock *sk,
unsigned int boundary, unsigned int timeout)
{
const unsigned int rto_base = TCP_RTO_MIN;
unsigned int linear_backoff_thresh, start_ts;
if (!inet_csk(sk)->icsk_retransmits)
return false;
start_ts = tcp_retransmit_stamp(sk);
if (!start_ts) return false;
if (likely(timeout == 0)) {
linear_backoff_thresh = ilog2(TCP_RTO_MAX/rto_base);
if (boundary <= linear_backoff_thresh)
timeout = ((2 << boundary) - 1) * rto_base;
else
timeout = ((2 << linear_backoff_thresh) - 1) * rto_base +
(boundary - linear_backoff_thresh) * TCP_RTO_MAX;
timeout = jiffies_to_msecs(timeout);
}
return (s32)(tcp_time_stamp(tcp_sk(sk)) - start_ts - timeout) >= 0;
UTO與窗口探測定時器
如下零窗口探測定時器函數tcp_probe_timer,如果用戶設置了UTO(變量icsk_user_timeout的值),並且發送隊列中還有待發送報文,此報文等待的時長不能超過UTO,否則,認爲此連接已經出錯。
static void tcp_probe_timer(struct sock *sk)
{
struct inet_connection_sock *icsk = inet_csk(sk);
struct sk_buff *skb = tcp_send_head(sk);
...
start_ts = tcp_skb_timestamp(skb);
if (!start_ts)
skb->skb_mstamp_ns = tp->tcp_clock_cache;
else if (icsk->icsk_user_timeout &&
(s32)(tcp_time_stamp(tp) - start_ts) > icsk->icsk_user_timeout)
goto abort;
...
if (icsk->icsk_probes_out >= max_probes) {
abort: tcp_write_err(sk);
UTO與Keepalive定時器
函數tcp_keepalive_timer,如果此連接自從最後一次接收到對端ACK或者數據報文,到當前時刻還沒有接收到其它報文,如果時長超出UTO時長,並且本地已經發送過探測報文,還是沒有收到相應,則判定此連接已經出錯。
在未設置UTO的情況下,此處以發送探測報文的次數爲判定連接出錯的依據。
static void tcp_keepalive_timer (struct timer_list *t)
{
struct sock *sk = from_timer(sk, t, sk_timer);
...
elapsed = keepalive_time_elapsed(tp);
if (elapsed >= keepalive_time_when(tp)) {
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;
}
內核版本 5.0