「SYN+ACK が返ってこない」というのは、例えば以下のようなケースが考えられます。
FreeBSD・Linux・Solaris で SYN の再送間隔やタイムアウトまでの時間を調べた結果を下表に示します。 各 OS で再送間隔・再送回数・タイムアウトまでの時間がかなり異なることがわかるでしょう。 ただし、タイムアウト時間・再送間隔は OS の設定により変更可能と思われます。 この結果はあくまで一例と考えた方がよいでしょう。
OS | FreeBSD 5.2.1-RELEASE | Linux (カーネル 2.4.23) | Solaris8 |
---|---|---|---|
タイムアウトまでの時間 | 75秒 (1分15秒) | 189秒 (3分9秒) | 225秒 (3分45秒) |
SYN 送信回数 | 8回 | 6回 | 7回 |
再送間隔 |
0秒 SYN 送信 3秒(+3秒) SYN 再送 9秒(+6秒) SYN 再送 12秒(+5秒) SYN 再送 16秒(+4秒) SYN 再送 22秒(+6秒) SYN 再送 34秒(+12秒) SYN 再送 58秒(+24秒) SYN 再送 75秒(+17秒) タイムアウト |
0秒 SYN 送信 3秒(+3秒) SYN 再送 9秒(+6秒) SYN 再送 21秒(+12秒) SYN 再送 45秒(+24秒) SYN 再送 93秒(+48秒) SYN 再送 189秒(+96秒) タイムアウト |
0秒 SYN 送信 3秒(+3秒) SYN 再送 10秒(+7秒) SYN 再送 24秒(+14秒) SYN 再送 51秒(+27秒) SYN 再送 105秒(+54秒) SYN 再送 165秒(+60秒) SYN 再送 225秒(+60秒) タイムアウト |
●FreeBSD の場合 % telnet 192.168.x.x Trying 192.168.x.x... telnet: connect to address 192.168.x.x: Operation timed out telnet: Unable to connect to remote host ●Solaris8 の場合 % telnet 192.168.x.x Trying 192.168.x.x... telnet: Unable to connect to remote host: Connection timed out
実験するには、localhost で使用されていないポート宛に telnet してみるのが一番早いでしょう。 まず netstat で LISTEN しているポートを調べ、それ以外のポートに接続します。
% netstat -an | grep LISTEN tcp4 0 0 *.23 *.* LISTEN tcp4 0 0 *.21 *.* LISTEN tcp4 0 0 *.5680 *.* LISTEN tcp4 0 0 *.80 *.* LISTEN tcp4 0 0 *.587 *.* LISTEN tcp4 0 0 *.25 *.* LISTEN tcp4 0 0 *.22 *.* LISTEN ⇒ ポート 9999 は使われていないようだ。 % telnet localhost 9999 Trying 127.0.0.1... telnet: connect to address 127.0.0.1: Connection refused telnet: Unable to connect to remote host ⇒ ポート 9999 に接続すると、Connection refused に。タイムアウトとは異なり、RST の場合は即座にエラーが返ってきます。 tcpdump で確認してみると、SYN (S) を送信した後すぐに RST (R) を受信していることがわかります。
# tcpdump -i lo0 -n tcpdump: listening on lo0 04:20:39.716576 127.0.0.1.49209 > 127.0.0.1.9999: S 2843398866:2843398866(0) win 65535 (略) 04:20:39.716623 127.0.0.1.9999 > 127.0.0.1.49209: R 0:0(0) ack 2843398867 win 0 (DF)
この場合は、ルータから ICMP type3 (Destination Unreachable: 終点到達不能) が返されます。 ICMP type3 には、以下のようにコード別のさらに細かな分類があります。
Code | 内容 |
---|---|
0 | Net Unreachable |
1 | Host Unreachable |
2 | Protocol Unreachable |
3 | Port Unreachable |
4 | Fragmentation Needed and Don't Fragment was Set |
5 | Source Route Failed |
6 | Destination Network Unknown |
7 | Destination Host Unknown |
8 | Source Host Isolated |
略 |
例えば ICMP type3/Code0 は「Net Unreachable」、ICMP type3/Code1 は「Host Unreachable」です。
tcpdump で観察しつつ、Solaris8 の telnet で適当な IP アドレスに接続したところ、
# tcpdump icmp tcpdump: listening on hme0 01:32:40.944143 192.168.x.x > localhost: icmp: net 12.0.x.x unreachable (DF) [tos 0xc0] 01:32:43.938784 192.168.x.x > localhost: icmp: net 12.0.x.x unreachable (DF) [tos 0xc0] 01:32:44.306838 192.168.x.x > localhost: icmp: net 12.0.x.x unreachable (DF) [tos 0xc0] 01:32:47.307871 192.168.x.x > localhost: icmp: net 12.0.x.x unreachable (DF) [tos 0xc0] 01:32:51.057595 192.168.x.x > localhost: icmp: net 12.0.x.x unreachable (DF) [tos 0xc0] 01:32:54.058372 192.168.x.x > localhost: icmp: net 12.0.x.x unreachable (DF) [tos 0xc0] 01:33:04.557744 192.168.x.x > localhost: icmp: net 12.0.x.x unreachable (DF) [tos 0xc0] 01:33:07.558123 192.168.x.x > localhost: icmp: net 12.0.x.x unreachable (DF) [tos 0xc0] 01:33:31.558338 192.168.x.x > localhost: icmp: net 12.0.x.x unreachable (DF) [tos 0xc0] 01:33:34.559462 192.168.x.x > localhost: icmp: net 12.0.x.x unreachable (DF) [tos 0xc0] 01:34:25.562518 192.168.x.x > localhost: icmp: net 12.0.x.x unreachable (DF) [tos 0xc0] 01:34:28.559367 192.168.x.x > localhost: icmp: net 12.0.x.x unreachable (DF) [tos 0xc0] 01:35:25.564932 192.168.x.x > localhost: icmp: net 12.0.x.x unreachable (DF) [tos 0xc0] 01:35:28.561028 192.168.x.x > localhost: icmp: net 12.0.x.x unreachable (DF) [tos 0xc0]とルータから ICMP type3 がバシバシ返ってきていましたが、 結局 225秒待たされて ENETUNREACH (Network is unreachable) で終了しました。
しかし、Linux (カーネル 2.4.23) の telnet で同じ IP アドレスに接続すると、 即座に ENETUNREACH となってしまいました。
UNIX ネットワークプログラミング 第2版 Vol.1 には、
FreeBSD の環境では、ICMP type3 を発生させることはできませんでした (上位ルータでブロックされている?)。 デフォルトルートを削除して EHOSTUNREACH を発生させることはできました。 しかしこれはタイムアウトまで待つことなく即座にエラーとなりましたし、 どこかから ICMP type3 を受け取ったわけではないので (そもそもパケットが飛んでないので返事が返ってくるはずがない)、 似て非なる状態と思われます。
# netstat -rn Routing tables Internet: Destination Gateway Flags Refs Use Netif Expire default 192.168.1.1 UGS 0 11 fxp0 127.0.0.1 127.0.0.1 UH 0 98 lo0 192.168.1 link#1 UC 0 0 fxp0 192.168.1.1 00:09:41:25:c2:fe UHLW 1 0 fxp0 604 192.168.1.11 00:80:45:46:9f:15 UHLW 0 16829 fxp0 1131 ⇒ ルーティングテーブル確認 # route delete default ⇒ デフォルトルートを削除 # telnet 192.168.10.x Trying 192.168.10.x... telnet: connect to address 192.168.10.x: No route to host telnet: Unable to connect to remote host ⇒ EHOSTUNREACH 発生 # route add default 192.168.1.1 ⇒ デフォルトルートを元に戻すのを忘れずに
% kill -シグナル プロセスIDとしてもよいのですが、面倒なので実験用プログラムを書きました。コンパイル方法は以下の通りです。
% cc -o tcp-connect-signal tcp-connect-signal.c % cc -o tcp-connect-signal tcp-connect-signal.c -lresolv -lsocket -lnsl ⇒ Solaris ではこちらを。このプログラムは、サーバに connect(2) します。というより、connect(2) しかしません。
connect(2) の実行中に自分自身にシグナルが届くようにして、その挙動を観察しよう、というプログラムです。 connect(2) が一瞬で終わってしまうとシグナルを送るタイミングが難しくなるので、 タイムアウトが発生するような IP アドレス (telnet してもいつまでたっても返事が返ってこないような IP アドレス) に書き換えてください (接続先サーバはソース中に直接書いてあります)。
引数なしで実行すると、使用方法を表示して終了します。
% ./tcp-connect-signal tcp-connect-signal [--retry --use-handler] (SIGALRM | SIGWINCH)引数には「SIGALRM」か「SIGWINCH」のどちらかを必ず指定します。 また、それとは別に--retry と --use-handler のオプションを指定することができます。
% ./tcp-connect-signal SIGALRM alarm を実行しました。3秒後に SIGALRM が飛んでくるはずです。 192.168.33.44:80 に connect します。 Alarm clock (すぐに終了)これは以下の疑似コードのような状況です。
alarm(3); if ( connect(略) == -1 ){ printf("connect に失敗しました [%s]\n", strerror(errno)); }connect(2) 後、約 3秒経過すると SIGALRM が飛んできます。 SIGALRM が飛んでくるとデフォルトのシグナルハンドラが実行され、 「Alarm clock」と表示し、プロセスは終了します。 上記コードでは connect(2) が -1 を返すと「エラー発生」 という文字列を出力するようにしていますが、ここに処理が到達することはありません。
なぜプロセスが終了してしまうのか。 SIGALRM 受信時のデフォルトの動作が「プロセス終了 (terminate process)」だからです。
signal(2) より抜粋 Name Default Action Description ------------- ----------------- ----------------------- SIGALRM terminate process real-time timer expired
% ./tcp-connect-signal SIGWINCH [(sleep 3; kill -WINCH 7063 ) &] を実行しました。3秒後に SIGWINCH が飛んでくるはずです。 192.168.33.44:80 に connect します。 (この後、数分待ち続ける) connect に失敗しました [Operation timed out]SIGWINCH の場合は以下の疑似コードのような処理になります。 3秒後に SIGWINCH を受けるため、system(3) で手抜き処理をしています。
system("(sleep 3; kill -WINCH 自分のプロセス番号) &"); if ( connect(略) == -1 ){ printf("connect に失敗しました [%s]\n", strerror(errno)); }connect(2) 後、約 3秒経過すると SIGWINCH が飛んできます。 SIGWINCH が飛んできても何も起こりません。タイムアウトまで数分待ち続け、 その後に connect(2) が -1 を返し、errno には ETIMEDOUT がセットされます。
あたかも SIGWINCH なぞ飛んできていないかのような挙動になります。
これはなぜか。 SIGWINCH のデフォルト動作が「シグナル破棄 (discard signal)」であるからです。
signal(2) より抜粋 Name Default Action Description ------------- ----------------- ----------------------------- SIGWINCH discard signal Window size change
シグナル受信時にシグナルハンドラをセットした場合はどうなるでしょうか。 --use-handler オプションを指定すると、シグナルハンドラをセットします。 そうすると、シグナル受信時に関数が実行されて、メッセージが表示されます。
% ./tcp-connect-signal --use-handler SIGALRM SIGALRM のシグナルハンドラをセットしました alarm を実行しました。3秒後に SIGALRM が飛んでくるはずです。 192.168.33.44:80 に connect します。 シグナル 14 (Alarm clock) 受信 connect に失敗しました [Interrupted system call] (ここで終了)これは以下の疑似コードのような状況です。
void sig_handler(int sig){ printf("シグナル %d (%s)受信\n", sig, strsignal(sig)); } main(){ ... signal(SIGALRM, sig_handler); alarm(3); if ( connect(略) == -1 ){ printf("connect に失敗しました [%s]\n", strerror(errno)); } ... }SIGALRM 受信時に関数 sig_handler を実行するよう、シグナルハンドラを設定しています。
シグナル 14 (Alarm clock) 受信という表示が出ていますので、確かに sig_handler に処理が渡っていることがわかります。
シグナル番号からシグナルの解説文 ("Alarm clock" など) を得るには strsignal(3) を使います。 ただしこの関数は *BSD・Linux・Solaris にはありますが、HP-UX にはありません (strsignal(3) は規格化されていない)。 FreeBSD には配列 sys_siglist[]・sys_signame[] もありますが、 これも使えない OS が多いです。 最終手段は
char *my_siglist[300]; /* 300 は適当 */ my_siglist[SIGHUP] = "Hangup"; my_siglist[SIGINT] = "Interrrupt"; my_siglist[SIGQUIT] = "Quit"; (略)と自前で定義しておき、my_siglist[シグナル番号] とするしかないでしょう。
connect に失敗しました [Interrupted system call]です。シグナルハンドラ未設定のときは connect(2) から処理が戻ってきませんでしたが、 シグナルハンドラを設定すると connect(2) が -1 を返してくるようになりました。
errno は EINTR (Interrupted system call) となっています。 文字通り、「システムコールが割り込まれた」ということです。
SIGWINCH に --use-handler オプションを付けた場合も、同様の動作をします。
% ./tcp-connect-signal --use-handler SIGWINCH [(sleep 3; kill -WINCH 7072 ) &] を実行しました。3秒後に SIGWINCH が飛んでくるはずです。 SIGWINCH のシグナルハンドラをセットしました 192.168.33.44:80 に connect します。 シグナル 28 (Window size changes) 受信 connect に失敗しました [Interrupted system call] (ここで終了)疑似コードで表すと、以下のような動作になります。
void sig_handler(int sig){ printf("シグナル %d (%s)受信\n", sig, strsignal(sig)); } main(){ ... system("(sleep 3; kill -WINCH 自分のプロセス番号) &"); if ( connect(略) == -1 ){ printf("connect に失敗しました [%s]\n", strerror(errno)); } ... }--use-handler オプションなしで実行したとき、
% ./tcp-connect-signal SIGALRMと
% ./tcp-connect-signal SIGWINCHの挙動が異なるのは、SIGALRM と SIGWINCH でシグナル受信時のデフォルト動作が違うためです (SIGALRM はプロセス終了、SIGWINCH はシグナル無視)。
--use-handler オプションを付けると、シグナル受信時のデフォルト動作を上書きし、 sig_handler 関数が呼ばれるようになりますので、SIGALRM と SIGWINCH の違いはなくなります。
--retry オプションを付けると、connect(2) が割り込まれて EINTR となった場合は もう一度 connect(2) を再実行するようになります。
SIGALRM の実行結果は以下の通りです (SIGWINCH も同じ動作をするので省略します)。
% ./tcp-connect-signal --use-handler --retry SIGALRM SIGALRM のシグナルハンドラをセットしました alarm を実行しました。3秒後に SIGALRM が飛んでくるはずです。 192.168.33.44:80 に connect します。 シグナル 14 (Alarm clock) 受信 connect に失敗しました [Interrupted system call] システムコールが割り込まれたので再実行します 192.168.33.44:80 に connect します。 (この後、数分待ち続ける) connect に失敗しました [Operation timed out]疑似コードで書くと以下のようになります。
void sig_handler(int sig){ printf("シグナル %d (%s)受信\n", sig, strsignal(sig)); } main(){ ... signal(SIGALRM, sig_handler); alarm(3); RETRY: int s = socket(AF_INET, SOCK_STREAM, 0); if ( connect(略) == -1 ){ int err = errno; printf("connect に失敗しました [%s]\n", strerror(errno)); if ( errno == EINTR ){ printf("システムコールが割り込まれたので再実行します\n"); goto RETRY; } } ... }connect(2) が -1 を返し、なおかつ errno が EINTR だった場合、 connect(2) を再実行しています。
ただし connect(2) だけを再実行するのではなく、ソケット生成からやり直しています。 なぜなら、connect(2) だけを再実行しようとすると、 EALREADY (Operation already in progress) になるからです。
一般に、ストリームソケットが正常に connect() できるのは 1 回だけです。とあります。少なくとも *BSD では 4.4BSD Lite の時代からこういう挙動をします。 FreeBSD 5.2.1-RELEASE では kern/uipc_syscalls.c の kern_connect() の先頭でチェックしています。
本当は、close(s) してからソケット生成・connect(2)、とした方がよいかもしれない。
--use-handler を付けず、--retry オプションのみを指定した場合はどうなるでしょうか。
% ./tcp-connect-signal --retry SIGALRM alarm を実行しました。3秒後に SIGALRM が飛んでくるはずです。 192.168.33.44:80 に connect します。 Alarm clock (すぐに終了)--retry オプションを付けない場合と同じ挙動になります。 疑似コードで書くと以下のようになりますが、 SIGALRM が飛んでくるとデフォルトのシグナルハンドラに処理が渡り、 そのままプロセスは終了してしまいます。 いくらリトライの準備をしていても、connect(2) から返ってこないので意味がありません。
alarm(3); RETRY: int s = socket(AF_INET, SOCK_STREAM, 0); if ( connect(略) == -1 ){ int err = errno; printf("connect に失敗しました [%s]\n", strerror(errno)); if ( errno == EINTR ){ printf("システムコールが割り込まれたので再実行します\n"); goto RETRY; } }●SIGWINCH でシグナルハンドラを設定せず、リトライの場合
同様に、SIGWINCH の場合も --retry なしの場合と同じ挙動です。 シグナルは無視されるので、そもそも EINTR が返ってくることがありません。
% ./tcp-connect-signal --retry SIGWINCH [(sleep 3; kill -WINCH 7084 ) &] を実行しました。3秒後に SIGWINCH が飛んでくるはずです。 192.168.33.44:80 に connect します。 (この後、数分待ち続ける)
1: /* 2: * $Id: tcp-connect-signal.c,v 1.1 2005/06/11 18:12:50 68user Exp $ 3: * 4: * connect(2) とシグナルの実験 5: * 6: * written by 68user http://X68000.q-e-d.net/~68user/ 7: */ 8: 9: #include <stdio.h> 10: #include <string.h> 11: #include <strings.h> 12: #include <stdlib.h> 13: #include <sys/types.h> 14: #include <sys/socket.h> 15: #include <unistd.h> 16: #include <netinet/in.h> 17: #include <signal.h> 18: #include <netdb.h> 19: #include <errno.h> 20: 21: void sig_handler(int sig){ 22: printf("シグナル %d (%s) 受信\n", sig, strsignal(sig)); 23: } 24: 25: void usage(){ 26: fprintf(stderr, "tcp-connect-signal [--retry --use-handler] (SIGALRM | SIGWINCH)\n"); 27: exit(1); 28: } 29: 30: int main(int argc, char *argv[]){ 31: /* 接続先。タイムアウトになるような IP アドレスを指定すること */ 32: char host[] = "192.168.33.44"; 33: unsigned short port = 80; 34: 35: struct hostent *servhost; 36: struct sockaddr_in server; 37: int s; 38: int ret; 39: int will_retry = 0; 40: int use_handler = 0; 41: 42: while ( argc > 2 ){ 43: if ( strcmp(argv[1], "--retry") == 0 ){ 44: will_retry = 1; 45: } else if ( strcmp(argv[1], "--use-handler") == 0 ){ 46: use_handler = 1; 47: } else { 48: usage(); 49: } 50: argc--; 51: argv++; 52: } 53: 54: if ( argc != 2 || 55: ( strcmp(argv[1], "SIGALRM") != 0 && strcmp(argv[1], "SIGWINCH") != 0 ) ){ 56: usage(); 57: return 1; 58: } 59: 60: if ( strcmp(argv[1], "SIGALRM") == 0 ){ 61: if ( use_handler ){ 62: printf("SIGALRM のシグナルハンドラをセットしました\n"); 63: signal(SIGALRM, sig_handler); 64: } 65: alarm(3); 66: printf("alarm を実行しました。3秒後に SIGALRM が飛んでくるはずです。\n"); 67: 68: } else { 69: char buf[256]; 70: int ret; 71: sprintf(buf, "(sleep 3; kill -WINCH %d ) &", getpid()); 72: ret = system(buf); 73: if ( ret != 0 ){ 74: fprintf(stderr, "system(3) でエラー発生。SIGWINCH を送信できません [%s]\n", 75: strerror(errno)); 76: } 77: printf("[%s] を実行しました。3秒後に SIGWINCH が飛んでくるはずです。\n", buf); 78: 79: if ( use_handler ){ 80: printf("SIGWINCH のシグナルハンドラをセットしました\n"); 81: signal(SIGWINCH, sig_handler); 82: } 83: } 84: 85: servhost = gethostbyname(host); 86: if ( servhost == NULL ){ 87: fprintf(stderr, "[%s] から IP アドレスへの変換に失敗しました。\n", host); 88: return 1; 89: } 90: 91: bzero(&server, sizeof(server)); 92: server.sin_family = AF_INET; 93: bcopy(servhost->h_addr, &server.sin_addr, servhost->h_length); 94: server.sin_port = htons(port); 95: 96: RETRY: 97: if ( ( s = socket(AF_INET, SOCK_STREAM, 0) ) < 0 ){ 98: fprintf(stderr, "ソケットの生成に失敗しました [%s]\n", strerror(errno)); 99: return 1; 100: } 101: 102: printf("%s:%d に connect します。\n", host, port); 103: ret = connect(s, (struct sockaddr *)&server, sizeof(server)); 104: if ( ret == -1 ){ 105: int err = errno; 106: fprintf(stderr, "connect に失敗しました [%s]\n", strerror(errno)); 107: 108: if ( err == EINTR && will_retry ){ 109: fprintf(stderr, "システムコールが割り込まれたので再実行します\n"); 110: goto RETRY; 111: } 112: return 1; 113: } 114: 115: /* 後始末省略 */ 116: return 0; 117: }
しかし、
errno | 解説 | 発生させるには |
---|---|---|
EBADF | s 引数が有効な記述子でありません。 | オープンしていないディスクリプタ (例えば 100) を s に指定。 |
ENOTSOCK | s 引数がソケットではなくファイルの記述子です。 | s に 0 (標準入力) を指定。 |
EAFNOSUPPORT | 指定のアドレスファミリ内のアドレスがこのソケットでは使用できません。 | sockaddr_in.sin_family に異常な値 (100 など) を指定。 |
EISCONN | ソケットは既に接続されています。 | connect(2) 済のソケットに対し、再度 connect(2) しようとした。 |
EFAULT | name 引数はプロセスアドレス空間の外側の領域を指定しています。 | name にアドレス 0 を指定。 |
EINPROGRESS | 非ブロッキングのソケットで、接続がすぐには確立できませんでした。 ソケットへの書込みを select(2) で待つことによって、接続完了を待つことができます。 | 解説通り。 |
EALREADY | 前の接続の試みが未だ完了していません。 | ブロッキングソケットがシグナルで割り込まれた後、再度 connect(2) しようとした。 あるいはノンブロッキングソケットが EINPROGRESS になった後、再度 connect(2) しようとした。 |
int connect(int s, const struct sockaddr *name, socklen_t namelen);です。
ご意見・ご指摘は Twitter: @68user までお願いします。