天天看點

從linux源碼看socket(tcp)的timeout從linux源碼看socket(tcp)的timeout總結

從linux源碼看socket(tcp)的timeout

前言

網絡程式設計中逾時時間是一個重要但又容易被忽略的問題,對其的設定需要仔細斟酌。在經曆了數次實體機當機之後,筆者詳細的考察了在網絡程式設計(tcp)中的各種逾時設定,于是就有了本篇博文。本文大部分讨論的是socket設定為block的情況,即setNonblock(false),僅在最後提及了nonblock socket(本文基于linux 2.6.32-431核心)。

connectTimeout

在讨論connectTimeout之前,讓我們先看下java和C語言對于socket connect調用的函數簽名:

java:
 // 函數調用中攜帶有逾時時間
 public void connect(SocketAddress endpoint, int timeout) ;
C語言:
 // 函數調用中并不攜帶逾時時間
 int connect(int sockfd, const struct sockaddr * sockaddr, socklen_t socklent)                 

作業系統提供的connect系統調用并沒有提供timeout的參數設定而java卻有,我們先考察一下原生系統調用的逾時政策。

connect系統調用

我們觀察一下此系統調用的kernel源碼,調用棧如下所示:

connect[使用者态]
    |->SYSCALL_DEFINE3(connect)[核心态]
            |->sock->ops->connect           

由于我們考察的是tcp的connect,其socket的内部結構如下圖所示:

從linux源碼看socket(tcp)的timeout從linux源碼看socket(tcp)的timeout總結

最終調用的是tcp_connect,代碼如下所示:

int tcp_connect(struct sock *sk) {
    ......
    // 發送SYN
    err = tcp_transmit_skb(sk, buff, 1, sk->sk_allocation);
    ...
    /* Timer for repeating the SYN until an answer. */
    // 由于是剛建立連接配接,是以其rto是TCP_TIMEOUT_INIT
    inet_csk_reset_xmit_timer(sk, ICSK_TIME_RETRANS,
                inet_csk(sk)->icsk_rto, TCP_RTO_MAX);
    return 0;    
}           

又上面代碼可知,在tcp_connect設定了重傳定時器之後return回了tcp_v4_connect再return到inet_stream_connect。我們繼續考察:

int inet_stream_connect(struct socket *sock, struct sockaddr *uaddr,
            int addr_len, int flags)
{
    ......
    // tcp_v4_connect=>tcp_connect
    err = sk->sk_prot->connect(sk, uaddr, addr_len);
    // 這邊用的是sk->sk_sndtimeo
    timeo = sock_sndtimeo(sk, flags & O_NONBLOCK);
    ......
    inet_wait_for_connect(sk, timeo));
    ......
out:
    release_sock(sk);
    return err;

sock_error:
    err = sock_error(sk) ? : -ECONNABORTED;
    sock->state = SS_UNCONNECTED;
    if (sk->sk_prot->disconnect(sk, flags))
        sock->state = SS_DISCONNECTING;
    goto out
}           

由上面代碼可見,可以采用設定SO_SNDTIMEO來控制connect系統調用的逾時,如下所示:

setsockopt(sockfd, SOL_SOCKET, SO_SNDTIMEO, &timeout, len);           

不設定SO_SNDTIMEO

如果不設定SO_SNDTIMEO,那麼會由tcp重傳定時器在重傳超過設定的時候後逾時,如下圖所示:

從linux源碼看socket(tcp)的timeout從linux源碼看socket(tcp)的timeout總結

這個syn重傳的次數由:

cat /proc/sys/net/ipv4/tcp_syn_retries 筆者機器上是5            

來決定。那麼我們就來看一下這個重傳到底是多長時間:

tcp_connect中:
        // 設定的初始逾時時間為icsk_rto=TCP_TIMEOUT_INIT為1s
        inet_csk_reset_xmit_timer(sk, ICSK_TIME_RETRANS,
                inet_csk(sk)->icsk_rto, TCP_RTO_MAX);           

其重傳定時器的回掉函數為tcp_retransmit_timer:

void tcp_retransmit_timer(struct sock *sk)
{
    ......
    // 檢測是否逾時
    if (tcp_write_timeout(sk))
        goto out;
    ......
    // icsk_rto = icsk_rto * 2,由于syn階段,是以isck_rto不會由于網絡傳輸而改變
    // 重傳的時候會以1,2,4,8指數遞增
    icsk->icsk_rto = min(icsk->icsk_rto << 1, TCP_RTO_MAX);
    // 重設timer
    inet_csk_reset_xmit_timer(sk, ICSK_TIME_RETRANS, icsk->icsk_rto, TCP_RTO_MAX);
out:;        
}           

而計算tcp_write_timeout的邏輯則是在這篇blog中已經較長的描述過,

https://my.oschina.net/alchemystar/blog/1936433           

隻不過在connect時刻,重傳的計算以TCP_TIMEOUT_INIT為機關進行計算。而ESTABLISHED(read/write)時刻,重傳以TCP_RTO_MIN進行計算。那麼根據這段重傳邏輯,我們就可以計算出不同tcp_syn_retries最終表現的逾時時間。如下圖所示:

從linux源碼看socket(tcp)的timeout從linux源碼看socket(tcp)的timeout總結

那麼整理下表格,對于系統調用,connect的逾時時間為:

tcp_syn_retries timeout
1 min(so_sndtimeo,3s)
2 min(so_sndtimeo,7s)
3 min(so_sndtimeo,15s)
4 min(so_sndtimeo,31s)
5 min(so_sndtimeo,63s)

上述逾時時間和筆者的實測一緻。

kernel代碼版本細微變化

值得注意的是,linux本身官方釋出的2.6.32源碼對于tcp_syn_retries2的解釋和RFC并不一緻(至少筆者閱讀的代碼如此,這個細微的變化困擾了筆者好久,筆者下載下傳了和機器對應的核心版本後才發現代碼改了)。而redhat釋出的2.6.32-431已經修複了這個問題(不清楚具體哪個小版本修改的),并将初始RTO設定為1s(官方2.6.32為3s)。這也是,不同核心小版本上的實驗會有不同的connect timeout表現的原因(有的抓包到的重傳SYN時間間隔為3,6,12......)。以下為代碼對比:

========================>linux 核心版本2.6.32-431<========================
#define TCP_TIMEOUT_INIT ((unsigned)(1*HZ))    /* RFC2988bis initial RTO value    */

static inline bool retransmits_timed_out(struct sock *sk,
                     unsigned int boundary,
                     unsigned int timeout,
                     bool syn_set)
{
    ......
    unsigned int rto_base = syn_set ? TCP_TIMEOUT_INIT : TCP_RTO_MIN;
    ......
    timeout = ((2 << boundary) - 1) * rto_base;
    ......

}
========================>linux 核心版本2.6.32.63<========================
#define TCP_TIMEOUT_INIT ((unsigned)(3*HZ))    /* RFC 1122 initial RTO value    */

static inline bool retransmits_timed_out(struct sock *sk,
                     unsigned int boundary
{
    ......
    timeout = ((2 << boundary) - 1) * TCP_RTO_MIN;
    ......
}           

另外,tcp_syn_retries重傳次數可以在單個socket中通過setsockopt設定。

JAVA connect API

現在我們考察下java的connect api,其connect最終調用下面的代碼:

Java_java_net_PlainSocketImpl_socketConnect(...){

    if (timeout <= 0) {
         ......
        connect_rv = NET_Connect(fd, (struct sockaddr *)&him, len);
         .....
    }else{
         // 如果timeout > 0 ,則設定為nonblock模式
        SET_NONBLOCKING(fd);
        /* no need to use NET_Connect as non-blocking */
        connect_rv = connect(fd, (struct sockaddr *)&him, len);
        /*
         * 這邊用系統調用select來模拟阻塞調用逾時
         */
        while (1) {
            ......
            struct timeval t;
            t.tv_sec = timeout / 1000;
            t.tv_usec = (timeout % 1000) * 1000;
            connect_rv = NET_Select(fd+1, 0, &wr, &ex, &t);
            ......
        }
        ......
        // 重新設定為阻塞模式
        SET_BLOCKING(fd);
        ......
    }
}           

其和connect系統調用的不同點是,在timeout為0的時候,走預設的系統調用不設定逾時時間的邏輯。在timeout>0時,将socket設定為非阻塞,然後用select系統調用去模拟逾時,而沒有走linux本身的逾時邏輯,如下圖所示:

從linux源碼看socket(tcp)的timeout從linux源碼看socket(tcp)的timeout總結

由于沒有java并沒有設定so_sndtimeo的選項,是以在timeout為0的時候,直接就通過重傳次數來控制逾時時間。而在調用connect時設定了timeout(不為0)的時候,逾時時間如下表格所示:

min(timeout,3s)
min(timeout,7s)
min(timeout,15s)
min(timeout,31s)
min(timeout,63s)

socketTimeout

write系統調用的逾時時間

socket的write系統調用最後調用的是tcp_sendmsg,源碼如下所示:

int tcp_sendmsg(struct kiocb *iocb, struct socket *sock, struct msghdr *msg,
        size_t size){
    ......
    timeo = sock_sndtimeo(sk, flags & MSG_DONTWAIT);
    ......
    while (--iovlen >= 0) {
        ......
        // 此種情況是buffer不夠了
        if (copy <= 0) {
    new_segment:
          ......
          if (!sk_stream_memory_free(sk))
              goto wait_for_sndbuf;

          skb = sk_stream_alloc_skb(sk, select_size(sk),sk->sk_allocation);
          if (!skb)
              goto wait_for_memory;
        }
        ......
    }
    ......
    // 這邊等待write buffer有空間
wait_for_sndbuf:
        set_bit(SOCK_NOSPACE, &sk->sk_socket->flags);
wait_for_memory:
        if (copied)
            tcp_push(sk, flags & ~MSG_MORE, mss_now, TCP_NAGLE_PUSH);
            // 這邊等待timeo長的時間
        if ((err = sk_stream_wait_memory(sk, &timeo)) != 0)
            goto do_error;
        ......
out:
    // 如果拷貝了資料,則傳回
    if (copied)
        tcp_push(sk, flags, mss_now, tp->nonagle);
    TCP_CHECK_TIMER(sk);
    release_sock(sk);
    return copied;        
out_err:
    // error的處理
    err = sk_stream_error(sk, flags, err);
    TCP_CHECK_TIMER(sk);
    release_sock(sk);
    return err;        
}
           

從上面的核心代碼看出,如果socket的write buffer依舊有空間的時候,會立馬傳回,并不會有timeout。但是write buffer不夠的時候,會等待SO_SNDTIMEO的時間(nonblock時候為0)。但是如果SO_SNDTIMEO沒有設定的時候,預設初始化為MAX_SCHEDULE_TIMEOUT,可以認為其逾時時間為無限。那麼其逾時時間會有另一個條件來決定,我們看下sk_stream_wait_memory的源碼:

int sk_stream_wait_memory(struct sock *sk, long *timeo_p){
        // 等待socket shutdown或者socket出現err
        sk_wait_event(sk, &current_timeo, sk->sk_err ||
                          (sk->sk_shutdown & SEND_SHUTDOWN) ||
                          (sk_stream_memory_free(sk) &&
                          !vm_wait));
}                                    

在write等待的時候,如果出現socket被shutdown或者socket出現錯誤的時候,則會跳出wait進而傳回錯誤。在不考慮對端shutdown的情況下,出現sk_err的時間其實就是其write的timeout時間,那麼我們看下什麼時候出現sk->sk_err。

SO_SNDTIMEO不設定,write buffer滿之後ack一直不傳回的情況(例如,實體機當機)

實體機當機後,tcp發送msg的時候,ack不會傳回,則會在重傳定時器tcp_retransmit_timer到期後timeout,其重傳到期時間通過tcp_retries2以及TCP_RTO_MIN計算出來。其源碼可見筆者的blog:

https://my.oschina.net/alchemystar/blog/1936433           

tcp_retries2的設定位置為:

cat /proc/sys/net/ipv4/tcp_retries2 筆者機器上是5,預設是15           

SO_SNDTIMEO不設定,write buffer滿之後對端不消費,導緻buffer一直滿的情況

和上面ack逾時有些許不一樣的是,一個邏輯是用TCP_RTO_MIN通過tcp_retries2計算出來的時間。另一個是真的通過重傳超過tcp_retries2次數來time_out,兩者的差別和rto的動态計算有關。但是可以大緻認為是一緻的。

上述邏輯如下圖所示:

從linux源碼看socket(tcp)的timeout從linux源碼看socket(tcp)的timeout總結

write_timeout表格

tcp_retries2 buffer未滿 buffer滿
立即傳回 min(SO_SNDTIMEO,(25.6s-51.2s)根據動态rto定
15 min(SO_SNDTIMEO,(924.6s-1044.6s)根據動态rto定

java的SocketOutputStream的sockWrite0逾時時間

java的sockWrite0沒有設定逾時時間的地方,同時也沒有設定過SO_SNDTIMEOUT,其直接調用了系統調用,是以其逾時時間和write系統調用保持一緻。

readTimeout

ReadTimeout可能是最容易導緻問題的地方。我們先看下系統調用的源碼:

read系統調用

socket的read系統調用最終調用的是tcp_recvmsg, 其源碼如下:

int tcp_recvmsg(struct kiocb *iocb, struct sock *sk, struct msghdr *msg,
        size_t len, int nonblock, int flags, int *addr_len)
{
    ......
    // 這邊timeo=SO_RCVTIMEO
    timeo = sock_rcvtimeo(sk, nonblock);
    ......
    do{
        ......
        // 下面這一堆判斷表明,如果出現錯誤,或者已經被CLOSE/SHUTDOWN則跳出循環
        if(copied) {
            if (sk->sk_err ||
                sk->sk_state == TCP_CLOSE ||
                (sk->sk_shutdown & RCV_SHUTDOWN) ||
                !timeo ||
                signal_pending(current))
                break;
        } else {
            if (sock_flag(sk, SOCK_DONE))
                break;

            if (sk->sk_err) {
                copied = sock_error(sk);
                break;
            }
            // 如果socket shudown跳出
            if (sk->sk_shutdown & RCV_SHUTDOWN)
                break;
            // 如果socket close跳出
            if (sk->sk_state == TCP_CLOSE) {
                if (!sock_flag(sk, SOCK_DONE)) {
                    /* This occurs when user tries to read
                     * from never connected socket.
                     */
                    copied = -ENOTCONN;
                    break;
                }
                break;
            }
            .......
        }
        .......

        if (copied >= target) {
            /* Do not sleep, just process backlog. */
            release_sock(sk);
            lock_sock(sk);
        } else /* 如果沒有讀到target自己數(和水位有關,可以暫認為是1),則等待SO_RCVTIMEO的時間 */
            sk_wait_data(sk, &timeo);    
    } while (len > 0);
    ......
}           

上面的邏輯如下圖所示:

從linux源碼看socket(tcp)的timeout從linux源碼看socket(tcp)的timeout總結

重傳以及探測定時器timeout事件的觸發時機如下圖所示:

從linux源碼看socket(tcp)的timeout從linux源碼看socket(tcp)的timeout總結

如果核心層面ack正常傳回而且對端視窗不為0,僅僅應用層不傳回任何資料,那麼就會無限等待,直到對端有資料或者socket close/shutdown為止,如下圖所示:

從linux源碼看socket(tcp)的timeout從linux源碼看socket(tcp)的timeout總結

很多應用就是基于這個無限逾時來設計的,例如activemq的消費者邏輯。

java的SocketInputStream的sockRead0逾時時間

java的逾時時間由SO_TIMOUT決定,而linux的socket并沒有這個選項。其sockRead0和上面的java connect一樣,在SO_TIMEOUT>0的時候依舊是由nonblock socket模拟,在此就不再贅述了。

ReadTimeout逾時表格

C系統調用:

對端無響應 對端核心響應正常
min(SO_RCVTIMEO,(25.6s-51.2s)根據動态rto定 SO_RCVTIMEO==0?無限,SO_RCVTIMEO)
min(SO_RCVTIMEO,(924.6s-1044.6s)根據動态rto定

Java系統調用

min(SO_TIMEOUT,(25.6s-51.2s)根據動态rto定 SO_TIMEOUT==0?無限,SO_RCVTIMEO
min(SO_TIMEOUT,(924.6s-1044.6s)根據動态rto定

對端實體機當機之後的timeout

對端實體機當機後還依舊有資料發送

對端實體機當機時對端核心也gg了(不會發出任何包通知當機),那麼本端發送任何資料給對端都不會有響應。其逾時時間就由上面讨論的

min(設定的socket逾時[例如SO_TIMEOUT],核心内部的定時器逾時來決定)。

對端實體機當機後沒有資料發送,但在read等待

這時候如果設定了逾時時間timeout,則在timeout後傳回。但是,如果僅僅是在read等待,由于底層沒有資料互動,那麼其無法知道對端是否當機,是以會一直等待。但是,核心會在一個socket兩個小時都沒有資料互動情況下(可設定)啟動keepalive定時器來探測對端的socket。如下圖所示:

從linux源碼看socket(tcp)的timeout從linux源碼看socket(tcp)的timeout總結

大概是2小時11分鐘之後會逾時傳回。keepalive的設定由核心參數指定:

cat /proc/sys/net/ipv4/tcp_keepalive_time 7200 即兩個小時後開始探測
cat /proc/sys/net/ipv4/tcp_keepalive_intvl 75 即每次探測間隔為75s
cat /proc/sys/net/ipv4/tcp_keepalve_probes 9 即一共探測9次           

可以在setsockops中對單獨的socket指定是否啟用keepalive定時器(java也可以)。

對端實體機當機後沒有資料發送,也沒有read等待

和上面同理,也是在keepalive定時器逾時之後,将連接配接close。是以我們可以看到一個不活躍的socket在對端實體機突然當機之後,依舊是ESTABLISHED狀态,過很長一段時間之後才會關閉。

程序宕後的逾時

如果僅僅是對端程序當機的話(程序所在核心會close其所擁有的所有socket),由于fin包的發送,本端核心可以立刻知道目前socket的狀态。如果socket是阻塞的,那麼将會在目前或者下一次write/read系統調用的時候傳回給應用層相應的錯誤。如果是nonblock,那麼會在select/epoll中觸發出對應的事件通知應用層去處理。

如果fin包沒發送到對端,那麼在下一次write/read的時候核心會發送reset包作為回應。

nonblock

設定為nonblock=true後,由于read/write都是立刻傳回,且通過select/epoll等處理重傳逾時/probe逾時/keep alive逾時/socket close等事件,是以根據應用層代碼決定其逾時特性。定時器逾時事件發生的時間如上面幾小節所述,和是否nonblock無關。nonblock的程式設計模式可以讓應用層對這些事件做出響應。

總結

網絡程式設計中逾時時間是個重要但又容易被忽略的問題,這個問題隻有在遇到實體機當機等平時遇不到的現象時候才會凸顯。筆者在經曆數次實體機當機之後才好好的研究了一番,希望本篇文章可以對讀者在以後遇到類似逾時問題時有所幫助。