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