TCP/IP エラー処理 connect 編

前へ << *BSD で kqueue・kevent を使ってみよう Java で HTTP クライアントを作ってみよう (1) >> 次へ

connect(2) のエラー

TCP において connect(2) 呼出し時に発生する可能性のあるエラーは以下の通りです。
  • タイムアウト
  • RST 受信
  • EHOSTUNREACH また ENETUNREACH
  • シグナル受信
  • その他
まず、connect(2) 時の正常な流れをしっかり覚えておいてください。
  1. (connect(2) を呼んで) SYN を送る
  2. SYN+ACK が返ってくる (ここで connect(2) から戻る)
  3. ACK を送る

タイムアウト

もし仮に、SYN を送ったものの、相手側から SYN+ACK が返ってこない場合は、 (ローカルの TCP スタックが) しつこく SYN を再送します。何度 SYN を送っても SYN+ACK が返ってこない場合はあきらめてタイムアウトします。

「SYN+ACK が返ってこない」というのは、例えば以下のようなケースが考えられます。

  • ローカルまたはピアのネットワークケーブル断線
  • 途中経路の HUB 故障・ルータ故障
  • ピアのマシンが故障や停電でダウン
  • 指定した IP アドレスに、そもそもマシンが存在しない

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秒) タイムアウト

表内の「x秒(+y秒)」とは、connect 開始時点から x 秒が経過し、前回の SYN 送信から y 秒が経過したということ。
タイムアウトが発生した場合は connect(2) が -1 を返し、 errno には ETIMEDOUT が設定されます。 ETIMEDOUT に対応するメッセージ (strerror(3) の返す文字列) は、以下のように OS によって異なります。
●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

RST 受信

送信した SYN に対して、RST (リセット) が返ってくるケースもあります。例えば、
相手側のマシンは生きていてネットワーク自体も繋がっているが、 指定のポートを見張っているプロセスがいない (listen(2) していない)
というケースがこれに該当します。

実験するには、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)
localhost へのパケットをダンプする場合は、 -i オプションでループバックインタフェースを指定することを忘れずに。 ループバックインタフェースの名前は ifconfig -a で確認できます。
RST が返ってきたということは、以下のことが言えます。
  • 相手側の OS は生きている
  • ネットワークは繋がっている
  • 該当のポートを監視 (listen) しているはずのプロセスが落ちているか、そもそも誤ったポート番号を指定している

EHOSTUNREACH または ENETUNREACH

途中経路のルータで「ルーティング先がない」と判断された場合は、どうなるのでしょうか。

この場合は、ルータから 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」です。

どういうときにどの Code を返すべきか、ゲートウェイが返すのはどの Code でホストが返すのはどの Code か、 などの情報が RFC 792RFC 1122 に書いてあるよう気がします。 ICMP の type・code 一覧は ICMP TYPE NUMBERS をどうぞ。
このようなエラーが返ってきた場合、connect(2) は即座にエラーとはせず、タイムアウト時間に達するまで待ち続けます。 なぜなら、しばらく待っていればその間に正しいルーティング情報に戻り、 再送した SYN が相手側に届くかもしれないからです。 タイムアウト時間に達してしまうと connect(2) は -1 を返し、 errnoENETUNREACHEHOSTUNREACH がセットされます。

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 には、

4.2BSD のような初期のシステムでは、ICMP 終点到達不可メッセージを受信した際に、 ただちにコネクション確立を中止していた。 この ICMP エラーは過渡的な状態を示すものであるため、 この処理は明らかに間違いである。
とありますので、この文章が正しいとするなら Linux の挙動は誤りです。 ただ、ポリシーの違いや、最近はすぐあきらめるのが流行、という可能性もあります。 どうなんでしょうね。

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
  ⇒ デフォルトルートを元に戻すのを忘れずに

シグナル受信

シグナルを受信した場合も、connect(2) はエラーを返します。 実際に試す場合は、connect(2) しているときに別端末から
% 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 のオプションを指定することができます。
●SIGALRM の場合

% ./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

●SIGWINCH の場合
% ./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

●SIGALRM かつシグナルハンドラ使用の場合

シグナル受信時にシグナルハンドラをセットした場合はどうなるでしょうか。 --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 に処理が渡っていることがわかります。
シグナルハンドラの引数には、受信したシグナル番号が渡されます。 FreeBSD 5.2.1-RELEASE においてはシグナル 14 は SIGALRM ですが、 他の OS では異なる可能性があります。

シグナル番号からシグナルの解説文 ("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[シグナル番号] とするしかないでしょう。
本当はシグナルハンドラ内で printf(3) を使ってはいけません。そのうち詳しく書きます。
注目してほしいのは、その後の
connect に失敗しました [Interrupted system call]
です。シグナルハンドラ未設定のときは connect(2) から処理が戻ってきませんでしたが、 シグナルハンドラを設定すると connect(2) が -1 を返してくるようになりました。

errnoEINTR (Interrupted system call) となっています。 文字通り、「システムコールが割り込まれた」ということです。


●SIGWINCH かつシグナルハンドラ使用の場合

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 を返し、なおかつ errnoEINTR だった場合、 connect(2) を再実行しています。

ただし connect(2) だけを再実行するのではなく、ソケット生成からやり直しています。 なぜなら、connect(2) だけを再実行しようとすると、 EALREADY (Operation already in progress) になるからです。

FreeBSD 5.2.1-RELEASE の connect(2) には
一般に、ストリームソケットが正常に connect() できるのは 1 回だけです。
とあります。少なくとも *BSD では 4.4BSD Lite の時代からこういう挙動をします。 FreeBSD 5.2.1-RELEASE では kern/uipc_syscalls.ckern_connect() の先頭でチェックしています。

本当は、close(s) してからソケット生成・connect(2)、とした方がよいかもしれない。


●SIGALRM でシグナルハンドラを設定せず、リトライの場合

--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 します。
(この後、数分待ち続ける)

tcp-connect-signal のソースは以下の通りです。

tcp-connect-signal.c

    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: }

シグナル受信のまとめ

まとめます。
  • 無視されるシグナル (SIGWINCH・SIGCHLD・SIGINFO など) は、システムコールの処理に影響ありません。
  • プロセスが終了してしまうシグナル (SIGALRM・SIGTERM・SIGHUP・SIGPIPE など) は、プロセスが終了してしまいます。
  • 無視されるシグナルでも、プロセスが終了してしまうシグナルでも、シグナルハンドラを明示的に指定すれば、 connect(2)EINTR となります。その場合、必要であればソケット生成・connect(2) をやり直します。
そもそも、あなたが作ろうとしているプログラムにはシグナルが飛んでくるかどうか考えてください。 利用者が kill したのならば、無理にシグナルハンドラをセットしたりせず、 デフォルト動作のままにしておくのがよいでしょう。 その場合は EINTR 対策は不要です (無視されるか、プロセスが終了するかのどちらかだから)。

しかし、

  • シグナル受信で終了する際、一時ファイルなどの後始末をしたい
  • SIGALRM でタイムアウト処理を行いたい
  • SIGCHLD で子プロセスの状態を知りたい
という場合は、シグナルハンドラを使わざるをえません。 この場合は connect(2)EINTR を返す場合に備えましょう。
以下、言い訳。signal(2) よるシグナル処理は OS によって動作が大きく異なります。 例えば、一部システムでは EINTR が発生した場合、自動的にシステムコールが再開されます。 sigaction や SA_RESTART についてはそのうち書きます。

その他

この他に connect(2) にエラーを起こさせる方法はたくさんあります。 FreeBSD 5.2.1-RELEASE の connect(2) が設定する可能性のある errno から、 適当にピックアップして、どうやったらそのエラーが起こるかを書いてみました。

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);
です。
前へ << *BSD で kqueue・kevent を使ってみよう Java で HTTP クライアントを作ってみよう (1) >> 次へ

ご意見・ご指摘は Twitter: @68user までお願いします。