Tcl – パイプラインの活用 -プロセス間通信-

外部プログラムとのデータの受け渡し

外部プログラムの起動とデータの受け渡しはexecコマンドで説明しましたが、他の方法としてopenコマンドを使う方法があります。

execコマンドは、入出力のデータ量が少なくプログラムの実行時間が短い場合はいいですが、実行に時間がかかるプログラムでは以下の問題があります。

  • execコマンドで起動したプログラムが終了するまで、処理を再開できない。
  • execコマンドで起動したプログラムが終了した時に、プログラムの出力が一度に返って来る。

この問題を解決するにはopenコマンドでパイプを開いて送受信するようにします。

openコマンドはファイルだけでなくパイプを開くことができます。openコマンドを使用したデータの受け渡しは、開いたパイプを通して外部プロクラムとデータの受け渡しを行います。

この機能を使うとファイルの入出力と同じようにパイプをクローズするまで、起動したプログラムとデータの受け渡しができるようになります。

OSは起動したプログラムをプロセスという単位で管理しています。起動したプロセスからさらにプロセスを起動すると、起動元を親プロセス、起動したプロセスを子プロセスといいます。プロセス同士でデータの受け渡しをすることをプロセス間通信といいます。

Tclではopenコマンドを使ってプロセス間通信をする事ができます。

パイプの機能

パイプは、標準出力を次のコマンドの標準入力へ繋げる機能があります。

Unixでは、以下の例のようにコマンドの出力結果を次のコマンドの標準入力へ繋げて使うことがよくあります。Windowsでもコマンドプロンプト画面で使えますが、基本的にGUI操作がメインなので使ったことがない方もいるかも知れません。

[使用例]

# 各ファイルのディスク使用量を確認する

$ du -ah tcl-tk | more
4.0K	tcl-tk/hello.tcl
4.0K	tcl-tk/hello-w.tcl
4.0K	tcl-tk/input.tcl
     ・
     ・
4.0K	tcl-tk/swap.tcl
4.0K	tcl-tk/sum.tcl
--続ける--

# 行番号付で表示する。

$ cat hello-w.tcl | nl
     1	#!/bin/sh
     2	# the next line restarts using tclsh \
     3	exec wish "$0" "$@"
       
     4	# hello-w.tcl
       
     5	message .msg -width 200 -text {hello, world!}
     6	button .btn -text 終了 -command exit
     7	pack .msg .btn

# (3)小文字を大文字に変える。

$ echo "hello" | tr a-z A-Z
HELLO

Tclでパイプを開く

ファイルの入出力の記事でopenコマンドを紹介しましたが、このコマンドは、ファイル名の代わりに「|(pipe)」を指定することも出来ます。

パイプを使うと外部のプログラムとデータの受け渡しをすることができます。

openコマンドによるパイプの使い方をいくつか紹介します。

外部プログラムの標準出力からデータ受け取る

[書式]

set varName [open "| program" "r"]

アクセスモードに”r”を指定してパイプを開くとprogramの標準出力チャネルから出力されたデータをgetsコマンドまたはreadコマンドで受け取ることができます。

execコマンドと違い、closeコマンドでパイプを閉じる必要があります。varNameにはファイル識別子がセットされます。

[使用例]

set fid [open "| df -h" "r"]
set str [read -nonewline $fid]
puts $str

[出力]

ファイルシス        サイズ  使用  残り 使用% マウント位置
/dev/mapper/cl-root    17G  5.7G   12G   34% /
devtmpfs              903M     0  903M    0% /dev
tmpfs                 920M     0  920M    0% /dev/shm
tmpfs                 920M  9.5M  910M    2% /run
tmpfs                 920M     0  920M    0% /sys/fs/cgroup
/dev/sda1            1014M  386M  629M   38% /boot
vbox共有              356G  176G  180G   50% /media/sf_vbox共有
tmpfs                 184M   24K  184M    1% /run/user/1000
/dev/sr0               56M   56M     0  100% /run/media/xxxx/VBox_GAs_5.2.26
tmpfs                 184M  4.0K  184M    1% /run/user/42

外部プログラムの標準入力へデータを渡す

[書式]

set varName [open "| program" "w"]

アクセスモードに”w”を指定して開くと、putsコマンドでprogramの標準入力へデータを渡すことができます。

execコマンドと違い、closeコマンドでパイプを閉じる必要があります。varNameにはファイル識別子がセットされます。

[使用例]

set fid [open "| cat" "w"]
puts $fid "Hello! Hello!\nGood morning!"
flush $fid

[出力]

Hello! Hello!
Good morning!

上の例のように、putsコマンドで外部プログラムにデータ送る場合、バッファリングの問題があるのでflushコマンドで吐き出す必要があります。

※バッファリングについては「Tcl – 文字列の入力と出力について -標準入出力-」で紹介しています。

もう1つの方法としてfconfigureコマンドまたはchanコマンドで、バッファリングモードを変更しておくこともできます。

[書式]

fconfigure channelId -buffering newValue 
chan configure channelId -buffering newValue

newValueは次の値を指定できます。

newValue 意味
full バッファが一杯になると書き出す。(デフォルト)
line改行文字ごとに書き出す。
none出力操作の実行直後に自動的に書き出す。

[使用例:chanコマンドの例]

set fid [open "| cat" "w"]
chan configure $fid -buffering line
puts $fid "Hello! Hello!\nGood morning!"
close $fid

双方向にパイプを開く

[書式]

set varName [open "| program" "r+"]

アクセスモードに”r+”を指定して開くと、データの受け取り、受け渡しの双方向のパイプを開くことができます。

この方法は、起動したプログラム(子プロセス)側も出力をバッファリングしないようにしなければ上手く通信が出来ません。

送受信の都度、子プロセスを起動する場合

[使用例]

子プロセス側:strlen-s.tcl

#!/bin/sh
# the next line restarts using tclsh \
exec tclsh "$0" "$@"

# n 文字数
# s 文字列

set n [gets stdin s]
puts $n

親プロセス側:strlen-m.tcl

#!/bin/sh
# the next line restarts using tclsh \
exec tclsh "$0" "$@"

puts "入力した文字列の文字数を表示します。"

while {1} {
    set fid [open "| tclsh strlen-s.tcl" "r+"]
    chan configure $fid -buffering line

    puts $fid [gets stdin]
    puts [gets $fid]
    close $fid

    puts -nonewline "終了?(y/n):"
    flush stdout

    if {[gets stdin] == y} {
        break
    }
}

[実行例]

$ ./strlen-m.tcl
入力した文字列の文字数を表示します。
hello!
6
終了?(y/n):n
Good morning!
13
終了?(y/n):y

putsコマンドのデフォルトでは、標準出力へ出力する際に改行がくると自動でフラッシュします。よって上の例では子プロセス側はflushコマンドやchanコマンドは不要です。

上の例では、子プロセス側は文字数を返すとすぐに終了します。親プロセス側のループ内で子プロセスの起動をその都度行っています。

次の例は、文字数の出力後も子プロセスを継続して起動させた状態にした場合の例です。

送受信後も子プロセスを継続して稼働させる場合

[使用例]

子プロセス側:strlen-s2.tcl

#!/bin/sh
# the next line restarts using tclsh \
exec tclsh "$0" "$@"

# n 文字数
# s 文字列

while {1} {
    set n [gets stdin s]
    puts $n
}

親プロセス側:strlen-m2.tcl

#!/bin/sh
# the next line restarts using tclsh \
exec tclsh "$0" "$@"

puts "入力した文字列の文字数を表示します。"

set fid [open "| tclsh strlen-s2.tcl" "r+"]
chan configure $fid -buffering line

while {1} {
    puts $fid [gets stdin]
    puts [gets $fid]

    puts -nonewline "終了?(y/n):"
    flush stdout

    if {[gets stdin] == y} {
        break
    }
}
catch {close $fid}

この場合、親プロセス側の最後に記述しているcloseコマンドをcatchコマンドで捕捉するようにしないと、以下のエラーメッセージを表示して異常終了します。

error writing "stdout": broken pipe

closeコマンドのマニュアルに以下の説明が記載されています。

If channelId is a blocking channel for a command pipeline then close waits for the child processes to complete.

channelIdがコマンドパイプラインのブロッキングチャネルである場合、クローズは子プロセスが終了するのを待ちます。

http://www.tcl.tk/man/tcl8.6/TclCmd/close.htm

どうやらcloseコマンドでパイプラインを閉じる時は、子プロセスを終了させないとダメなようです。

継続稼働中の子プロセスを終了させるには

子プロセスを終了させる方法を2つ紹介します。

[使用例1]

子プロセス側:strlen-s3.tcl

#!/bin/sh
# the next line restarts using tclsh \
exec tclsh "$0" "$@"

# n 文字数
# s 文字列

while {[set n [gets stdin s]] > 0} {
    puts $n
}

親プロセス側:strlen-m3.tcl

#!/bin/sh
# the next line restarts using tclsh \
exec tclsh "$0" "$@"

puts "入力した文字列の文字数を表示します。"

set fid [open "| tclsh strlen-s3.tcl" "r+"]
chan configure $fid -buffering line

while {1} {
    puts $fid [gets stdin]
    puts [gets $fid]

    puts -nonewline "終了?(y/n):"
    flush stdout

    if {[gets stdin] == y} {
        puts $fid ""    ;# 空行を送って子プロセスを終了させる。
        break
    }
}
close $fid

使用例1では、空の文字列(文字数0)を送ることで、子プロセスを終了するようにしてます。

空の文字列でなくても”quit”が来たら終了させるなど、子プロセスへ送る文字列のルールを決めることで終了させることができそうですよね。

子プロセスを終了させる方法をもう1つ紹介します。

[使用例2]

子プロセス側はstrlen-s2.tclを使います。

親プロセス側:strlen-m4.tcl

#!/bin/sh
# the next line restarts using tclsh \
exec tclsh "$0" "$@"

puts "入力した文字列の文字数を表示します。"

set fid [open "| tclsh strlen-s2.tcl" "r+"]
chan configure $fid -buffering line

while {1} {
    puts $fid [gets stdin]
    puts [gets $fid]

    puts -nonewline "終了?(y/n):"
    flush stdout

    if {[gets stdin] == y} {
        set pid [pid $fid] 
        exec kill $pid
        break
    }
}
catch {close $fid}

pidコマンドはopenコマンドで起動したプロセスのプロセスIDを返します。

例2では、OS標準のkillコマンドを使って子プロセスを終了させています。この場合もcloseコマンドをcatchコマンドで捕捉するようにしないと、以下のエラーメッセージを表示します。

child killed: software termination signal
    while executing
"close $fid"
    (file "./strlen-m4.tcl" line 23)

ネットで色々調べていると、クローズする時に、パイプラインプロセスの異常終了などにより、closeコマンドがエラーになることがあるので、catchコマンドを使うようにしたほうがいいようです。

参考:killing process in tcl

Windowsには、killコマンドがないので、Tclの標準機能だけで、この方法を使うことは出来ませんが、TclXという拡張ライブラリを使うと、拡張ライブラリのkillコマンドが使えます。

ActiveTclにインストールされていたので使用してみました。

[使用例2-2]

#!/bin/sh
# the next line restarts using tclsh \
exec tclsh "$0" "$@"

package require Tclx    ;# Tclxを追加

puts "入力した文字列の文字数を表示します。"

set fid [open "| tclsh strlen-s2.tcl" "r+"]
chan configure $fid -buffering line

while {1} {
    puts $fid [gets stdin]
    puts [gets $fid]

    puts -nonewline "終了?(y/n):"
    flush stdout

    if {[gets stdin] == y} {
        set pid [pid $fid] 
        kill $pid    ;# Tclxのkillコマンド
        break
    }
}
catch {close $fid}

package require xxxx で拡張ライブラリが使えます。
packageコマンドについては別途紹介しようと思います。

参考:
https://wiki.tcl-lang.org/page/kill
https://wiki.tcl-lang.org/page/chan+pipe

コメント

タイトルとURLをコピーしました