1. 程式人生 > >從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的內部結構如下圖所示: ![](https://oscimg.oschina.net/oscnet/up-59900e9bb1796ee054b0a28e52e6684e9fe.png) 最終呼叫的是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重傳定時器在重傳超過設定的時候後超時,如下圖所示: ![](https://oscimg.oschina.net/oscnet/up-76e0d5c60919bde35ee06e38f8adb45e7f9.png) 這個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最終表現的超時時間。如下圖所示: ![](https://oscimg.oschina.net/oscnet/up-22bc1461d14944bb9cc5fe5bef605fedac7.png) 那麼整理下表格,對於系統呼叫,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本身的超時邏輯,如下圖所示: ![](https://oscimg.oschina.net/oscnet/up-3cc271ca5a7e95b18e48b6b60e79a911ea1.png) 由於沒有java並沒有設定so\_sndtimeo的選項,所以在timeout為0的時候,直接就通過重傳次數來控制超時時間。而在呼叫connect時設定了timeout(不為0)的時候,超時時間如下表格所示: |tcp\_syn\_retries|timeout| | --- | :--- | |1|min(timeout,3s)| |2|min(timeout,7s)| |3|min(timeout,15s)| |4|min(timeout,31s)| |5|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, ¤t_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的動態計算有關。但是可以大致認為是一致的。 ## 上述邏輯如下圖所示: ![](https://oscimg.oschina.net/oscnet/up-f02f4126c3b549ff4075392bee425e72d23.png) ### write_timeout表格 |tcp\_retries2|buffer未滿|buffer滿| | --- | :--- |:---| |5|立即返回|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); ...... } ``` 上面的邏輯如下圖所示: ![](https://oscimg.oschina.net/oscnet/up-b431d593ea5bb7b7f473a84ed775a2e9e85.png) 重傳以及探測定時器timeout事件的觸發時機如下圖所示: ![](https://oscimg.oschina.net/oscnet/up-c6832599cec6d81b23385925379b64500cf.png) 如果核心層面ack正常返回而且對端視窗不為0,僅僅應用層不返回任何資料,那麼就會無限等待,直到對端有資料或者socket close/shutdown為止,如下圖所示: ![](https://oscimg.oschina.net/oscnet/up-3fde656a742968de3f2e228cbac6eeb90bc.png) 很多應用就是基於這個無限超時來設計的,例如activemq的消費者邏輯。 ### java的SocketInputStream的sockRead0超時時間 java的超時時間由SO_TIMOUT決定,而linux的socket並沒有這個選項。其sockRead0和上面的java connect一樣,在SO\_TIMEOUT>0的時候依舊是由nonblock socket模擬,在此就不再贅述了。 ### ReadTimeout超時表格 C系統呼叫: |tcp\_retries2|對端無響應|對端核心響應正常| | --- | :--- |:---| |5|min(SO\_RCVTIMEO,(25.6s-51.2s)根據動態rto定|SO\_RCVTIMEO==0?無限,SO\_RCVTIMEO)| |15|min(SO\_RCVTIMEO,(924.6s-1044.6s)根據動態rto定|SO\_RCVTIMEO==0?無限,SO\_RCVTIMEO)| Java系統呼叫 |tcp\_retries2|對端無響應|對端核心響應正常| | --- | :--- |:---| |5|min(SO\_TIMEOUT,(25.6s-51.2s)根據動態rto定|SO\_TIMEOUT==0?無限,SO\_RCVTIMEO| |15|min(SO\_TIMEOUT,(924.6s-1044.6s)根據動態rto定|SO\_TIMEOUT==0?無限,SO\_RCVTIMEO|| ## 對端物理機宕機之後的timeout ### 對端物理機宕機後還依舊有資料傳送 對端物理機宕機時對端核心也gg了(不會發出任何包通知宕機),那麼本端傳送任何資料給對端都不會有響應。其超時時間就由上面討論的 min(設定的socket超時[例如SO_TIMEOUT],核心內部的定時器超時來決定)。 ### 對端物理機宕機後沒有資料傳送,但在read等待 這時候如果設定了超時時間timeout,則在timeout後返回。但是,如果僅僅是在read等待,由於底層沒有資料互動,那麼其無法知道對端是否宕機,所以會一直等待。但是,核心會在一個socket兩個小時都沒有資料互動情況下(可設定)啟動keepalive定時器來探測對端的socket。如下圖所示: ![](https://oscimg.oschina.net/oscnet/up-442257698dc7b0243f1a356a55ea96e6aa2.JPEG) 大概是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的程式設計模式可以讓應用層對這些事件做出響應。 # 總結 網路程式設計中超時時間是個重要但又容易被忽略的問題,這個問題只有在遇到物理機宕機等平時遇不到的現象時候才會凸顯。筆者在經歷數次物理機宕機之後才好好的研究了一番,希望本篇文章可以對讀者在以後遇到類似超時問題時有所幫助。 ## 公眾號 關注筆者公眾號,獲取更多幹貨文章: ![](https://oscimg.oschina.net/oscnet/up-03e8bdd592b3eb9dec0a50fa5ff56192df0.JPEG)