[ruby-list:49611] IO.popen の不審な挙動を再現するサンプルコード

From: 尾川敏也 <ogw@...>
Date: 2013-09-28 11:29:22 UTC
List: ruby-list #49611
尾川です。

IO.popen が不審な挙動を示す件、今更ですが、挙動を再現するサンプルコー
ドを作ることができました。

私の使い方が正しくないのが原因なら正しく使うにはどうすればよいのか、
あるいはひょっとして IO.popen に不具合が潜んでいるのか、が判ると嬉
しいのですが。

このメールの最後に、以下のサンプルファイル群を張り付けてあります。

なお、メールからファイルを切り分けたり、C のソースをコンパイルした
りするのはご面倒だと思いましたので、以下の URL に Windows 用のコン
パイル済みバイナリを含めて、一連のファイルを置いてあります。

    http://www6.shizuokanet.ne.jp/ogw/misc/ruby_pipe/index.html

test.rb
        テストスクリプト本体です。ruby test.rb で pipe を使ったメソ
        ッドチェーンを繰り返し実行してテストできます。

        pipe として使うライブラリ、外部コマンド、テストデータのサイ
        ズ等を、比較的簡単に変更できるようにしてあります。

use_iopopen.rb
use_open3.rb
        pipe を使ったメソッドチェーンのために test.rb が require し
        ている ライブラリの、IO.popen 版と Open3.pipeline_rw 版です。

external.c
        テスト用外部コマンドの C ソースです。

external.rb
        上記の C プログラムと同じ処理をする Ruby のスクリプトです。


このサンプルを使って、あらためていくつか試した結果を以下に記します。

なお、Ruby は Windows XP, Ubuntu 12.04 ともに ruby-2.0.0-p247.zip 
を付属のドキュメントに従って自分でビルドしたものです。

テストデータは test.rb 内で生成していて、タブ区切りで 1 行 30 フィー
ルド 3000 行のテキストデータです。

外部コマンドは標準入力から読んだ各行の先頭から 6 個のフィールドをタ
ブ区切りで書き出します。その際、標準入力から受け取ったフィールド数
が 6 個より少ないと stderr にメッセージを書き出します。

この外部コマンドをメソッドチェーンで2回連続して実行するのが 1 回分
のテストで、そのテストを 100 回繰り返しています。

ます、Windows での結果です。

C 版の外部コマンドで IO.popen を使った場合の結果の例です。

    ----------- 11 / 100 ---------
    ----------- 12 / 100 ---------
    external: too less field (3 for 6) at line 2784
    ----------- 13 / 100 ---------
    ----------- 14 / 100 ---------
    external: too less field (3 for 6) at line 2784
    ----------- 15 / 100 ---------
    external: too less field (3 for 6) at line 2784
    ----------- 16 / 100 ---------
    ----------- 17 / 100 ---------
    ----------- 18 / 100 ---------
    (この後 20 回程度、問題発生せず)

うまく動く回もあれば、本来のデータを受け取れていない回もあり、あま
り規則性がありません。

ちなみに、Ruby 版の外部コマンドを使った場合は滅多にこの現象は出ない
のですが、test.rb を繰り返すと、メソッドチェーンの実行の数百回に1
回くらいの割合で、同じような不具合が発生しました。

見ていると Ruby 版はかなり処理が遅いので、そのせいで内部のタイミン
グが変わっているのかもしれません。ともあれ、私の外部プログラムだけ
が原因ということは無さそうです。

以上は IO.popen を使った場合ですが、Open3. を使った場合には C プロ
グラムと Ruby プログラムともに、今回試した限りでは不具合は起きませ
んでした。

なお、IO.popen で C プログラムを使う状態で、試しに 入力データを 3000
行から 4000 行に増やしたら、これまで見かけなかった以下のようなエラー
メッセージが表示されました。

    ----------- 40 / 100 ---------
    ----------- 41 / 100 ---------
    ----------- 42 / 100 ---------
    ----------- 43 / 100 ---------
    ----------- 44 / 100 ---------
    C:/ogw/ruby/use_iopopen.rb:6:in `close_write': closed stream (IOError)
            from C:/ogw/ruby/use_iopopen.rb:6:in `block (2 levels) in |'
    [BUG] Segmentation fault
    ruby 2.0.0p247 (2013-06-27 revision 41674) [i386-mswin32_100]
    
    -- C level backtrace information -------------------------------------------
    (ここでフリーズして動かなくなりました。)

[BUG] とか C level backtrace information などというのが表示されてい
ますので、私の外部プログラムではなくて Ruby 側の問題のように見えま
す。(違っていたら、大変恥ずかしいですが。)

次に、Linux でもテストしました。外部プログラムは C 版で、入力は 3000
行です。

Open3. を使った場合は不具合は起きませんでしたが、IO.popen を使った
場合は以下のようになりました。

    ----------- 64 / 100 ---------
    ----------- 65 / 100 ---------
    ----------- 66 / 100 ---------
    ----------- 67 / 100 ---------
    ----------- 68 / 100 ---------
    /home/ogw/tmp/linux/use_iopopen.rb:6:in `close_write': closed stream (IOError)
        from /home/ogw/tmp/linux/use_iopopen.rb:6:in `block (2 levels) in |'

少し前のメールで、Linux は OK で Windows 固有の問題のようだと書きま
したが、必ずしもそういう訳ではなく、Linux でも不具合は起きるようで
す。

とりあえずテストした結果は以上です。

以下が一連のプログラムです。


#################################################################
#########################  test.rb ##############################
#################################################################
# どちらのパイプを使うか
require_relative        "use_iopopen"   # IO.popen
#require_relative       "use_open3"     # Open3.pipeline_rw

# 外部コマンド
cmd = "external 6"              # C 版 :   <= 各行の先頭の 6 フィールドを出力
#cmd = "ruby external.rb 6"     # Ruby 版: <= 各行の先頭の 6 フィールドを出力

# 繰り返し実行回数
cycle = 100


# テスト用データの生成
elem = "123456789ABC\t"                 # 1 要素
line = elem * 30                        # 1 行中の要素数
line.sub!(/\t$/, "\n")
data = line * 3000                      # 総行数

# pipe を使ったメソッドチェーンを cycle 回繰り返し実行
n = 0
cycle.times do
  n += 1
  puts "----------- #{n} / #{cycle} ---------"
  data.|(cmd).|(cmd)
end
#################################################################
#########################  use_iopopen.rb #######################
#################################################################
class String
  def |(cmdline)
    IO.popen(cmdline, "w+") do |pipe|
      t = Thread.fork {
        pipe.write self
        pipe.close_write
      }
      t.abort_on_exception = true
      pipe.read
    end
  end
end
#################################################################
#########################  use_open3.rb #########################
#################################################################
require "open3"
class String
  def |(cmdline)
    Open3.pipeline_rw(cmdline) do |stdin, stdout, wait_thrs|
      t = Thread.fork {
        stdin.write self
        stdin.close
      }
      t.abort_on_exception = true
      stdout.read 
    end
  end
end
#################################################################
#########################  external.c ###########################
#################################################################
/*----------------------------------------------------------------------*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define LINE_LEN        102400          /* 行読み込みバッファ長 */
#define FLD_NO            1024          /* 扱えるフィールドの最大個数 */
#define FLD_LEN            256          /* ひとつのフィールドの最大長 */

static char line[LINE_LEN];             /* 行読み込みバッファ */
static char fields[FLD_NO][FLD_LEN];    /* フィールドの配列 */

static int split(char *line, char fields[FLD_NO][FLD_LEN]);

/*----------------------------------------------------------------------
 * 1行ずつ読み込み、タブで区切られたフィールドに分割し、先頭の
 * フィールドから、コマンドラインで指定された個数のフィールドを
 * タブ区切りで出力する。
 *----------------------------------------------------------------------*/
int main(int argc, char *argv[])
{
    char *myname;               /* コマンド名 */
    int nout;                   /* 出力指定フィールド数 */
    int nline;                  /* 入力行カウンタ */
    int nfield;                 /* 行内フィールド数 */
    int n;                      /* 実際の出力フィールド数 */
    int i;

    /*
     * 使用方法 : external  出力するフィールド数
     *       例 : external  6
     */
    if (argc < 2) {
        fprintf(stderr, "Too less arguments\n");
        exit(EXIT_FAILURE);
    }
    myname = argv[0];
    nout = atoi(argv[1]);

    nline = 0;
    while (fgets(line, LINE_LEN, stdin) != NULL) {

        /* 行カウントしてフィールドに分割 */
        nline++;
        nfield = split(line, fields);

        /* フィールド数をチェック */
        n = nout;
        if (nfield < nout) {
            fprintf(stderr, "%s: too less field (%d for %d) at line %d\n",
                    myname, nfield, nout, nline);
            n = nfield;
        }

        /* 元と同じタブ1個の区切りで出力 */
        for (i = 0; i < n; i++) {
            fputs(fields[i], stdout);
            if (i == n -1) {
                fputs("\n", stdout);
            } else {
                fputs("\t", stdout);
            }
        }
    }

    return EXIT_SUCCESS;
}

/*----------------------------------------------------------------------
 * 行をタブ1個を区切りとしたフィールドに分けて配列に格納し、総数を返す
 *----------------------------------------------------------------------*/
static int split(char *line, char fields[FLD_NO][FLD_LEN])
{
    int n;
    char *p;

    if ((p = strrchr(line, '\n')) != NULL) { *p = '\0'; }
    n = 0;
    for (p = strtok(line, "\t"); p != NULL; p = strtok(NULL, "\t")) {
        strcpy(fields[n], p);
        n++;
    }

    return n;
}
/*----------------------------------------------------------------------*/
#################################################################
#########################  external.rb ##########################
#################################################################
# external.c と同じ処理
#
# $stdin から1行ずつ読み、タブ区切りのフィールドに分割し、
# 最初のフィールドからコマンドラインで指定された個数の
# フィールドをタブ区切りで出力する。

myname = "external.rb"
nout = ARGV[0].to_i

nline = 0
$stdin.each do |line|

  nline += 1
  fields = line.split(/[\t\n]+/)
  nfield = fields.size

  n = nout
  if (nfield < nout) then
      msg = "#{myname}: too less field (#{nfield} for #{nout}) at line #{nline}"
      $stderr.puts msg
      n = nfield
  end

  (0 .. (n-2)).each do |i|
    $stdout.print fields[i]   + "\t"
  end
  $stdout.print   fields[n-1] + "\n"

end
#################################################################

-- 
尾川敏也 ogw@shizuokanet.ne.jp
http://www6.shizuokanet.ne.jp/ogw/

In This Thread