Perlでサブプロセスを使用する
2023/12/31
プロセスの実行状況を監視して動作するツールを作りたい
  • Perlスクリプトから外部ツールを起動して、その出力や実行状態を取り込みつつ何かをしたいことがあります。例えば外部装置を制御するツールを起動して、そのステータスや測定値を取得するなどです。

  • 以前Perlの[Tips]として、外部コマンド実行の紹介をしていますが、その場合では下記の何れかになります。
    • system関数: コマンドの終了をwaitする(その間スクリプトは停止)
    • start/バックグラウンド実行: 並列実行になるが実行状況をつかみにくい

  • 一般的にこういったケースではプロセスのフォークによりサブプロセスを作成します。Perlでは文字通り組み込み関数fork()によってサブプロセスを作成することができます。

サブプロセス利用例
  • 実際に例を示したいと思います。下図がメインプロセス/サブプロセスに分割後、サブプロセスを監視しつつ動作する今回のサンプルです。

  • Figure 1: サブプロセス利用例

  • 流れとしては下記になります。
    • スクリプト実行開始後fork()でサブプロセスを作成
    • サブプロセスはファイル作成後1秒毎にメッセージ出力し、10秒で終了
    • 一方メインプロセスはサブプロセスのファイル出力確認後、内容を追従表示(*1)
    • サブプロセス終了を検知するまで追従表示を続ける

  • それではコードを見てみましょう。少し長くなりましたが、注目箇所はそれほどありませんのでご勘弁ください。
  • #!perl -w
    use POSIX ':sys_wait_h';
    use Time::HiRes;
    use IO::Handle;
    use strict;
    
    {
        my $fname = 'log.txt'; # サブプロセス出力ファイル名
        my $cnum = 10;         # サブプロセスカウント秒数
        
        unlink($fname) if (-f $fname); # 出力ファイル残っていたら一度消す
        
        my $spid = fork(); # サブプロセス生成
        die "ERR: sub process is not defined.\n" unless (defined($spid));
        
        if ($spid == 0) { # サブプロセス
            func_sub($fname, $cnum);
        }
        else { # メインプロセス
            func_main($fname, $spid);
        }
    }
    
    # メインプロセスはサブプロセスファイル出力を追従表示
    sub func_main {
        my ($fname, $spid) = @_;
        
        print "sub process id is [$spid]\n"; # 監視対象サブプロセスID表示
        
        while(1) { # サブプロセスファイル出力開始待ち
            last if (-f $fname);           # 出力ファイル見つけたらループ抜ける
            Time::HiRes::usleep(100*1000); # 100ms待ち
        }
        
        my $fh;    # ファイルハンドル
        my $fpos;  # ファイルポジション
        
        open($fh, "< $fname");
            while(1) {
                my $epid = waitpid($spid, WNOHANG); # サブプロセス状態取得
                
                my @data = <$fh>;   # ファイルデータRead(Read後EOF)
                print @data;        # Readデータ表示
                $fpos = tell($fh);  # Read後ファイルポジション取得
                
                last if ($epid == $spid);  # サブプロセス終了したらループ抜ける
                
                Time::HiRes::usleep(100*1000); # 100ms待ち
                seek($fh, $fpos, 0);           # seekダミー実行(EOF解除)
            }
        close($fh);
    }
    
    # サブプロセスはファイルに指定秒間メッセージ出力
    sub func_sub {
        my ($fname, $cnum) = @_;
        
        my $fh;
        open($fh, "> $fname") or die;
            $fh->IO::Handle::autoflush;  # バッファリング無効
            for (my $i=0; $i<$cnum; $i++) {
                print $fh "$i sec...\n"; # ファイルへメッセージ出力
                sleep(1);                # 1秒待ち
            }
        close($fh);
    }
    
    >test0.pl
    sub process id is [-5048]
    0 sec...
    1 sec...
    2 sec...
    3 sec...
    4 sec...
    5 sec...
    6 sec...
    7 sec...
    8 sec...
    9 sec...
    

プロセス分岐部
  • コードの冒頭部を切り出してみました。
  • {
        my $fname = 'log.txt'; # サブプロセス出力ファイル名
        my $cnum = 10;         # サブプロセスカウント秒数
        
        unlink($fname) if (-f $fname); # 出力ファイル残っていたら一度消す
        
        my $spid = fork(); # サブプロセス生成
        die "ERR: sub process is not defined.\n" unless (defined($spid));
        
        if ($spid == 0) { # サブプロセス
            func_sub($fname, $cnum);
        }
        else { # メインプロセス
            func_main($fname, $spid);
        }
    }
    
  • my $spid = fork(); でサププロセスを作成しています。forkとはプロセスのコピーなので、fork()関数実行段階では全く同じプロセスが並列に存在している状態です。

  • die "ERR: sub process is not defined.\n" unless (defined($spid));fork()関数のサブプロセス作成状態チェックです。成功時はメインプロセスに対しサブプロセスのプロセス番号が戻ります。失敗時はundefが戻ります。

  • するとif ($spid == 0) { # サブプロセス はかなり謎の記述に見えますが、サブプロセスから見てfork()関数で取得したプロセスID0 になります。

  • メインプロセスでは取得プロセス番号(≠0)が見えるので、これを利用してメインプロセスとサブプロセスの処理分岐を記述します。

サブプロセス終了監視部
  • 次はプロセスの終了監視部を切り出しました。
  • #!perl -w
    use POSIX ':sys_wait_h';
    ...中略...
    # メインプロセスはサブプロセスファイル出力を追従表示
    sub func_main {
        my ($fname, $spid) = @_;
        
        ...中略...
        
        open($fh, "< $fname");
            while(1) {
                my $epid = waitpid($spid, WNOHANG); # サブプロセス状態取得
                
                my @data = <$fh>;   # ファイルデータRead(Read後EOF)
                print @data;        # Readデータ表示
                $fpos = tell($fh);  # Read後ファイルポジション取得
                
                last if ($epid == $spid);  # サブプロセス終了したらループ抜ける
                
                Time::HiRes::usleep(100*1000); # 100ms待ち
                seek($fh, $fpos, 0);           # seekダミー実行(EOF解除)
            }
        close($fh);
    }
    
  • サブプロセスの監視にはwaitpid()関数を使用します。コード冒頭の use POSIX ':sys_wait_h'; も忘れないようにして下さい。

  • waitpid関数は第1引数で挙動が変わります。第2引数は決め打ち(WNOHANG)(*2)です。
    • サブプロセスID(指定プロセスの監視): 実行時=0 / 終了時=サブプロセスID / 未存在時=-1
    • 0(同一グループIDの監視): 実行時=0 / 終了時=最初の終了ID / 未存在時=-1
    • -1(全プロセスIDの監視): 実行時=0 / 終了時=最初の終了ID / 未存在時=-1

  • 子プロセス終了前に親プロセスが終了すると「ゾンビプロセス」になってしまうので、事実上ID指定によるwaitpid検出を使用するのがほとんどだと思います。
Notes
  • ファイルの追従表示については[こちら]を参照ください。
  • ノンブロッキングwaitとするフラグ。
Copyright(C) 2023 Altmo
本HPについて