TCP用戶超時(UTO)

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章