仕事で若い人から相談を受けるときがたまにあり、今回はその時のお話です.
TCPのサーバープログラムでポートをListenしているプロセスが入れ替わる、という怪現象を解析してみました.
Table of Contents
サーバープログラムを再起動できない問題
Linux PCで動作するTCPで接続するサーバープログラムがPC起動直後は正常に起動するんだけど、それを一度終了してサーバーを再起動するとうまく動作しない、という相談でした.
サーバープログラムの accept() でエラーになるんですけど…
accept() の前の listen() や bind() のエラーチェック
ちゃんとやってるか?
あ、やってませんでした. エラーチェックしてみます.
bind() がエラーになってます
ということで、listen() と bind() のエラーチェックを追加してみたところ, bind()がエラーになっていたそうです.
bind() が EADDRINUSE (Address already in use) で失敗してました!
あー、それな.
TIME_WAIT の間にサーバー再起動したからやろ.
setsocopt(SO_REUSEADDR) したらどうや?
いや、じつは
setsocopt(SO_REUSEADDR) してるんですけど…
そうかぁ
ちょっと見てみよか
…ということで若手といっしょにみてみました.
Listen している Port の状態を確認してみました.
まずはサーバープログラムで Listen している TCP の Port の状態を netstat で確認してみました.
2回目のサーバー起動の bind() で失敗する状態で netstat でポートの状態を見ると、なんと該当ポート TCP:12345 を Listen している人がいます…
kawauso@linux$ netstat -apnt
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 0.0.0.0:12345 0.0.0.0:* LISTEN –
最初に起動したサーバープログラムは終了してるはずなのに…
プログラム終了するときにsocketはちゃんとcloseしてるか?
はい、終了処理はバッチリです
ポートをListenしてるのは誰だ?
ポートTCP:12345ポートをListenしているプロセスを調べてみました.
まず一度目にサーバーが起動中のポートの状態は?
kawauso@linux$ netstat -apnt
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 0.0.0.0:12345 0.0.0.0:* LISTEN 458/hogehoge
確かにサーバープログラム hogehoge がListenしてます.
この状態でサーバープログラムを終了してnetstatでポートの状態を調べてみると…
kawauso@linux$ netstat -apnt
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 0.0.0.0:12345 0.0.0.0:* LISTEN 459/dhclient
あれっ、Listenしているプロセスが入れ替わってるぞ!?
ちょっとコードを見たら、怪現象の原因はここかぁ!
ちょっとコードを見せてもらいました.
製品のコードを出すわけにはいかないので、原因がわかるように抜粋したコードを書いたものがこれです.
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main ()
{
int sock0;
struct sockaddr_in addr;
struct sockaddr_in client;
int len;
int sock;
int yes = 1;
sock0 = socket (AF_INET, SOCK_STREAM, 0);
if (sock0 < 0)
{
perror ("socket");
return -1;
}
addr.sin_family = AF_INET;
addr.sin_port = htons (12345);
addr.sin_addr.s_addr = INADDR_ANY;
setsockopt (sock0, SOL_SOCKET, SO_REUSEADDR, (const char *) &yes, sizeof (yes));
if (bind (sock0, (struct sockaddr *) &addr, sizeof (addr)))
{
perror ("bind");
return -1;
}
if (listen (sock0, 5))
{
perror ("listen");
return -1;
}
system ("dhclient eth1");
while (1)
{
len = sizeof (client);
sock = accept (sock0, (struct sockaddr *) &client, &len);
write (sock, "HELLO", 5);
close (sock);
}
close (sock0);
return 0;
}
サーバープログラムはこちらを参照させていただきました.
コードを見てわかりました、怪現象の原因.
socket() でsocketを作ったあとに、system() で DHCP client daemon を起動しているます. (コードの42行目). dhclient は daemon modeで起動しているので起動した親プロセスhogehoge が終了しても終了せずに稼働しつづけます.
親プロセスの hogehoge が終了したあと, ポート TCP:12345 を子プロセスである dhclient が引き継ぐ、ということです.
Linux の man fork には子プロセスには親のファイル・ディスクリプタを引き継ぐとあります.
親プロセスで生成したソケットも子プロセスに引き継がれます.
そのため、親がListenしたポートを子プロセスが引き継ぐということなんですね.
頭では分かってましたが、目の当たりにしないと分からないモノなんですね…
対応策として
サーバープログラムを終了するときに、起動した dhclient をちゃんと終了させると TCP:12345 ポートをListenしているプロセスもいなくなり、2回目以降も正常に起動できるようになりました.
最近この手の本って少ないんですよね. Cで書いたプログラムってなくならないんだけどな…
私は未だにこれを参考にしてます.