ポートをListenするプロセスが切り替わる怪現象の巻

仕事で若い人から相談を受けるときがたまにあり、今回はその時のお話です.

TCPのサーバープログラムでポートをListenしているプロセスが入れ替わる、という怪現象を解析してみました.

サーバープログラムを再起動できない問題

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で書いたプログラムってなくならないんだけどな…
私は未だにこれを参考にしてます.