java linux 调用32位so_从linux源码看socket(tcp)的timeout
從linux源碼看socket(tcp)的timeout
前言
網(wǎng)絡(luò)編程中超時(shí)時(shí)間是一個(gè)重要但又容易被忽略的問題,對(duì)其的設(shè)置需要仔細(xì)斟酌。在經(jīng)歷了數(shù)次物理機(jī)宕機(jī)之后,筆者詳細(xì)的考察了在網(wǎng)絡(luò)編程(tcp)中的各種超時(shí)設(shè)置,于是就有了本篇博文。本文大部分討論的是socket設(shè)置為block的情況,即setNonblock(false),僅在最后提及了nonblock socket(本文基于linux 2.6.32-431內(nèi)核)。
connectTimeout
在討論connectTimeout之前,讓我們先看下java和C語言對(duì)于socket connect調(diào)用的函數(shù)簽名:
java:// 函數(shù)調(diào)用中攜帶有超時(shí)時(shí)間
public void connect(SocketAddress endpoint, int timeout) ;
C語言:
// 函數(shù)調(diào)用中并不攜帶超時(shí)時(shí)間
int connect(int sockfd, const struct sockaddr * sockaddr, socklen_t socklent)
操作系統(tǒng)提供的connect系統(tǒng)調(diào)用并沒有提供timeout的參數(shù)設(shè)置而java卻有,我們先考察一下原生系統(tǒng)調(diào)用的超時(shí)策略。
connect系統(tǒng)調(diào)用
我們觀察一下此系統(tǒng)調(diào)用的kernel源碼,調(diào)用棧如下所示:
connect[用戶態(tài)]|->SYSCALL_DEFINE3(connect)[內(nèi)核態(tài)]
|->sock->ops->connect
由于我們考察的是tcp的connect,其socket的內(nèi)部結(jié)構(gòu)如下圖所示:
最終調(diào)用的是tcp_connect,代碼如下所示:
......
// 發(fā)送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設(shè)置了重傳定時(shí)器之后return回了tcp_v4_connect再return到inet_stream_connect。我們繼續(xù)考察:
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
}
由上面代碼可見,可以采用設(shè)置SO_SNDTIMEO來控制connect系統(tǒng)調(diào)用的超時(shí),如下所示:
setsockopt(sockfd, SOL_SOCKET, SO_SNDTIMEO, &timeout, len);不設(shè)置SO_SNDTIMEO
如果不設(shè)置SO_SNDTIMEO,那么會(huì)由tcp重傳定時(shí)器在重傳超過設(shè)置的時(shí)候后超時(shí),如下圖所示:
這個(gè)syn重傳的次數(shù)由:
來決定。那么我們就來看一下這個(gè)重傳到底是多長時(shí)間:
tcp_connect中:// 設(shè)置的初始超時(shí)時(shí)間為icsk_rto=TCP_TIMEOUT_INIT為1s
inet_csk_reset_xmit_timer(sk, ICSK_TIME_RETRANS,
inet_csk(sk)->icsk_rto, TCP_RTO_MAX);
其重傳定時(shí)器的回掉函數(shù)為tcp_retransmit_timer:
void tcp_retransmit_timer(struct sock *sk){
......
// 檢測(cè)是否超時(shí)
if (tcp_write_timeout(sk))
goto out;
......
// icsk_rto = icsk_rto * 2,由于syn階段,所以isck_rto不會(huì)由于網(wǎng)絡(luò)傳輸而改變
// 重傳的時(shí)候會(huì)以1,2,4,8指數(shù)遞增
icsk->icsk_rto = min(icsk->icsk_rto << 1, TCP_RTO_MAX);
// 重設(shè)timer
inet_csk_reset_xmit_timer(sk, ICSK_TIME_RETRANS, icsk->icsk_rto, TCP_RTO_MAX);
out:;
}
而計(jì)算tcp_write_timeout的邏輯則是在這篇blog中已經(jīng)詳細(xì)描述過,
https://my.oschina.net/alchemystar/blog/1936433只不過在connect時(shí)刻,重傳的計(jì)算以TCP_TIMEOUT_INIT為單位進(jìn)行計(jì)算。而ESTABLISHED(read/write)時(shí)刻,重傳以TCP_RTO_MIN進(jìn)行計(jì)算。那么根據(jù)這段重傳邏輯,我們就可以計(jì)算出不同tcp_syn_retries最終表現(xiàn)的超時(shí)時(shí)間。如下圖所示:
那么整理下表格,對(duì)于系統(tǒng)調(diào)用,connect的超時(shí)時(shí)間為:
| 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) |
上述超時(shí)時(shí)間和筆者的實(shí)測(cè)一致。
推薦一本書
這本書教你怎么用wireshark來分析網(wǎng)絡(luò)問題,非常有意思,而且很薄~
kernel代碼版本細(xì)微變化
值得注意的是,linux本身官方發(fā)布的2.6.32源碼對(duì)于tcp_syn_retries2的解釋和RFC并不一致(至少筆者閱讀的代碼如此,這個(gè)細(xì)微的變化困擾了筆者好久,筆者下載了和機(jī)器對(duì)應(yīng)的內(nèi)核版本后才發(fā)現(xiàn)代碼改了)。而redhat發(fā)布的2.6.32-431已經(jīng)修復(fù)了這個(gè)問題(不清楚具體哪個(gè)小版本修改的),并將初始RTO設(shè)置為1s(官方2.6.32為3s)。這也是,不同內(nèi)核小版本上的實(shí)驗(yàn)會(huì)有不同的connect timeout表現(xiàn)的原因(有的抓包到的重傳SYN時(shí)間間隔為3,6,12……)。以下為代碼對(duì)比:
========================>linux 內(nèi)核版本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 內(nèi)核版本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重傳次數(shù)可以在單個(gè)socket中通過setsockopt設(shè)置。
JAVA connect API
現(xiàn)在我們考察下java的connect api,其connect最終調(diào)用下面的代碼:
Java_java_net_PlainSocketImpl_socketConnect(...){if (timeout <= 0) {
......
connect_rv = NET_Connect(fd, (struct sockaddr *)&him, len);
.....
}else{
// 如果timeout > 0 ,則設(shè)置為nonblock模式
SET_NONBLOCKING(fd);
/* no need to use NET_Connect as non-blocking */
connect_rv = connect(fd, (struct sockaddr *)&him, len);
/*
* 這邊用系統(tǒng)調(diào)用select來模擬阻塞調(diào)用超時(shí)
*/
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);
......
}
......
// 重新設(shè)置為阻塞模式
SET_BLOCKING(fd);
......
}
}
其和connect系統(tǒng)調(diào)用的不同點(diǎn)是,在timeout為0的時(shí)候,走默認(rèn)的系統(tǒng)調(diào)用不設(shè)置超時(shí)時(shí)間的邏輯。在timeout>0時(shí),將socket設(shè)置為非阻塞,然后用select系統(tǒng)調(diào)用去模擬超時(shí),而沒有走linux本身的超時(shí)邏輯,如下圖所示:
由于沒有java并沒有設(shè)置so_sndtimeo的選項(xiàng),所以在timeout為0的時(shí)候,直接就通過重傳次數(shù)來控制超時(shí)時(shí)間。而在調(diào)用connect時(shí)設(shè)置了timeout(不為0)的時(shí)候,超時(shí)時(shí)間如下表格所示:
| 1 | min(timeout,3s) |
| 2 | min(timeout,7s) |
| 3 | min(timeout,15s) |
| 4 | min(timeout,31s) |
| 5 | min(timeout,63s) |
socketTimeout
write系統(tǒng)調(diào)用的超時(shí)時(shí)間
socket的write系統(tǒng)調(diào)用最后調(diào)用的是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長的時(shí)間
if ((err = sk_stream_wait_memory(sk, &timeo)) != 0)
goto do_error;
......
out:
// 如果拷貝了數(shù)據(jù),則返回
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;
}
從上面的內(nèi)核代碼看出,如果socket的write buffer依舊有空間的時(shí)候,會(huì)立馬返回,并不會(huì)有timeout。但是write buffer不夠的時(shí)候,會(huì)等待SO_SNDTIMEO的時(shí)間(nonblock時(shí)候?yàn)?)。但是如果SO_SNDTIMEO沒有設(shè)置的時(shí)候,默認(rèn)初始化為MAX_SCHEDULE_TIMEOUT,可以認(rèn)為其超時(shí)時(shí)間為無限。那么其超時(shí)時(shí)間會(huì)有另一個(gè)條件來決定,我們看下sk_stream_wait_memory的源碼:
int sk_stream_wait_memory(struct sock *sk, long *timeo_p){// 等待socket shutdown或者socket出現(xiàn)err
sk_wait_event(sk, ¤t_timeo, sk->sk_err ||
(sk->sk_shutdown & SEND_SHUTDOWN) ||
(sk_stream_memory_free(sk) &&
!vm_wait));
}
在write等待的時(shí)候,如果出現(xiàn)socket被shutdown或者socket出現(xiàn)錯(cuò)誤的時(shí)候,則會(huì)跳出wait進(jìn)而返回錯(cuò)誤。在不考慮對(duì)端shutdown的情況下,出現(xiàn)sk_err的時(shí)間其實(shí)就是其write的timeout時(shí)間,那么我們看下什么時(shí)候出現(xiàn)sk->sk_err。
SO_SNDTIMEO不設(shè)置,write buffer滿之后ack一直不返回的情況(例如,物理機(jī)宕機(jī))
物理機(jī)宕機(jī)后,tcp發(fā)送msg的時(shí)候,ack不會(huì)返回,則會(huì)在重傳定時(shí)器tcp_retransmit_timer到期后timeout,其重傳到期時(shí)間通過tcp_retries2以及TCP_RTO_MIN計(jì)算出來。其源碼可見筆者的blog:
https://my.oschina.net/alchemystar/blog/1936433tcp_retries2的設(shè)置位置為:
cat /proc/sys/net/ipv4/tcp_retries2 筆者機(jī)器上是5,默認(rèn)是15SO_SNDTIMEO不設(shè)置,write buffer滿之后對(duì)端不消費(fèi),導(dǎo)致buffer一直滿的情況
和上面ack超時(shí)有些許不一樣的是,一個(gè)邏輯是用TCP_RTO_MIN通過tcp_retries2計(jì)算出來的時(shí)間。另一個(gè)是真的通過重傳超過tcp_retries2次數(shù)來time_out,兩者的區(qū)別和rto的動(dòng)態(tài)計(jì)算有關(guān)。但是可以大致認(rèn)為是一致的。
上述邏輯如下圖所示:
write_timeout表格
| 5 | 立即返回 | min(SO_SNDTIMEO,(25.6s-51.2s)根據(jù)動(dòng)態(tài)rto定 |
| 15 | 立即返回 | min(SO_SNDTIMEO,(924.6s-1044.6s)根據(jù)動(dòng)態(tài)rto定 |
java的SocketOutputStream的sockWrite0超時(shí)時(shí)間
java的sockWrite0沒有設(shè)置超時(shí)時(shí)間的地方,同時(shí)也沒有設(shè)置過SO_SNDTIMEOUT,其直接調(diào)用了系統(tǒng)調(diào)用,所以其超時(shí)時(shí)間和write系統(tǒng)調(diào)用保持一致。
readTimeout
ReadTimeout可能是最容易導(dǎo)致問題的地方。我們先看下系統(tǒng)調(diào)用的源碼:
read系統(tǒng)調(diào)用
socket的read系統(tǒng)調(diào)用最終調(diào)用的是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{
......
// 下面這一堆判斷表明,如果出現(xiàn)錯(cuò)誤,或者已經(jīng)被CLOSE/SHUTDOWN則跳出循環(huán)
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自己數(shù)(和水位有關(guān),可以暫認(rèn)為是1),則等待SO_RCVTIMEO的時(shí)間 */
sk_wait_data(sk, &timeo);
} while (len > 0);
......
}
上面的邏輯如下圖所示:
重傳以及探測(cè)定時(shí)器timeout事件的觸發(fā)時(shí)機(jī)如下圖所示:
如果內(nèi)核層面ack正常返回而且對(duì)端窗口不為0,僅僅應(yīng)用層不返回任何數(shù)據(jù),那么就會(huì)無限等待,直到對(duì)端有數(shù)據(jù)或者socket close/shutdown為止,如下圖所示:
很多應(yīng)用就是基于這個(gè)無限超時(shí)來設(shè)計(jì)的,例如activemq的消費(fèi)者邏輯。
java的SocketInputStream的sockRead0超時(shí)時(shí)間
java的超時(shí)時(shí)間由SO_TIMOUT決定,而linux的socket并沒有這個(gè)選項(xiàng)。其sockRead0和上面的java connect一樣,在SO_TIMEOUT>0的時(shí)候依舊是由nonblock socket模擬,在此就不再贅述了。
ReadTimeout超時(shí)表格
C系統(tǒng)調(diào)用:
| 5 | min(SO_RCVTIMEO,(25.6s-51.2s)根據(jù)動(dòng)態(tài)rto定 | SO_RCVTIMEO==0?無限,SO_RCVTIMEO) |
| 15 | min(SO_RCVTIMEO,(924.6s-1044.6s)根據(jù)動(dòng)態(tài)rto定 | SO_RCVTIMEO==0?無限,SO_RCVTIMEO) |
Java系統(tǒng)調(diào)用
| 5 | min(SO_TIMEOUT,(25.6s-51.2s)根據(jù)動(dòng)態(tài)rto定 | SO_TIMEOUT==0?無限,SO_RCVTIMEO |
| 15 | min(SO_TIMEOUT,(924.6s-1044.6s)根據(jù)動(dòng)態(tài)rto定 | SO_TIMEOUT==0?無限,SO_RCVTIMEO |
對(duì)端物理機(jī)宕機(jī)之后的timeout
對(duì)端物理機(jī)宕機(jī)后還依舊有數(shù)據(jù)發(fā)送
對(duì)端物理機(jī)宕機(jī)時(shí)對(duì)端內(nèi)核也gg了(不會(huì)發(fā)出任何包通知宕機(jī)),那么本端發(fā)送任何數(shù)據(jù)給對(duì)端都不會(huì)有響應(yīng)。其超時(shí)時(shí)間就由上面討論的
min(設(shè)置的socket超時(shí)[例如SO_TIMEOUT],內(nèi)核內(nèi)部的定時(shí)器超時(shí)來決定)。
對(duì)端物理機(jī)宕機(jī)后沒有數(shù)據(jù)發(fā)送,但在read等待
這時(shí)候如果設(shè)置了超時(shí)時(shí)間timeout,則在timeout后返回。但是,如果僅僅是在read等待,由于底層沒有數(shù)據(jù)交互,那么其無法知道對(duì)端是否宕機(jī),所以會(huì)一直等待。但是,內(nèi)核會(huì)在一個(gè)socket兩個(gè)小時(shí)都沒有數(shù)據(jù)交互情況下(可設(shè)置)啟動(dòng)keepalive定時(shí)器來探測(cè)對(duì)端的socket。如下圖所示:
大概是2小時(shí)11分鐘之后會(huì)超時(shí)返回。keepalive的設(shè)置由內(nèi)核參數(shù)指定:
cat /proc/sys/net/ipv4/tcp_keepalive_intvl 75 即每次探測(cè)間隔為75s
cat /proc/sys/net/ipv4/tcp_keepalve_probes 9 即一共探測(cè)9次
可以在setsockops中對(duì)單獨(dú)的socket指定是否啟用keepalive定時(shí)器(java也可以)。
對(duì)端物理機(jī)宕機(jī)后沒有數(shù)據(jù)發(fā)送,也沒有read等待
和上面同理,也是在keepalive定時(shí)器超時(shí)之后,將連接close。所以我們可以看到一個(gè)不活躍的socket在對(duì)端物理機(jī)突然宕機(jī)之后,依舊是ESTABLISHED狀態(tài),過很長一段時(shí)間之后才會(huì)關(guān)閉。
進(jìn)程宕后的超時(shí)
如果僅僅是對(duì)端進(jìn)程宕機(jī)的話(進(jìn)程所在內(nèi)核會(huì)close其所擁有的所有socket),由于fin包的發(fā)送,本端內(nèi)核可以立刻知道當(dāng)前socket的狀態(tài)。如果socket是阻塞的,那么將會(huì)在當(dāng)前或者下一次write/read系統(tǒng)調(diào)用的時(shí)候返回給應(yīng)用層相應(yīng)的錯(cuò)誤。如果是nonblock,那么會(huì)在select/epoll中觸發(fā)出對(duì)應(yīng)的事件通知應(yīng)用層去處理。
如果fin包沒發(fā)送到對(duì)端,那么在下一次write/read的時(shí)候內(nèi)核會(huì)發(fā)送reset包作為回應(yīng)。
nonblock
設(shè)置為nonblock=true后,由于read/write都是立刻返回,且通過select/epoll等處理重傳超時(shí)/probe超時(shí)/keep alive超時(shí)/socket close等事件,所以根據(jù)應(yīng)用層代碼決定其超時(shí)特性。定時(shí)器超時(shí)事件發(fā)生的時(shí)間如上面幾小節(jié)所述,和是否nonblock無關(guān)。nonblock的編程模式可以讓應(yīng)用層對(duì)這些事件做出響應(yīng)。
總結(jié)
網(wǎng)絡(luò)編程中超時(shí)時(shí)間是個(gè)重要但又容易被忽略的問題,這個(gè)問題只有在遇到物理機(jī)宕機(jī)等平時(shí)遇不到的現(xiàn)象時(shí)候才會(huì)凸顯。筆者在經(jīng)歷數(shù)次物理機(jī)宕機(jī)之后才好好的研究了一番,希望本篇文章可以對(duì)讀者在以后遇到類似超時(shí)問題時(shí)有所幫助。
推薦一本書,這本書教你怎么用wireshark來分析網(wǎng)絡(luò)問題,非常有意思,而且很薄~
總結(jié)
以上是生活随笔為你收集整理的java linux 调用32位so_从linux源码看socket(tcp)的timeout的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: html输入支付密码样式,基于JS实现类
- 下一篇: eve可以在linux运行吗,ubunt