大きなファイルの途中まで読む

大きなファイルの途中まで読む

1 TB のファイルがあります。12345678901 バイト目から 19876543212 バイト目までを読み取り、100 MB の RAM を搭載したマシンの標準出力に表示したいと思います。

これを実行する Perl スクリプトを簡単に作成できます。sysread は 700 MB/秒 (これは問題ありません) を実現しますが、syswrite は 30 MB/秒しか実現できません。もっと効率的なもの、できればすべての Unix システムにインストールされ、1 GB/秒程度の速度を実現できるものが欲しいです。

私の最初のアイデアは次のとおりです。

dd if=1tb skip=12345678901 bs=1 count=$((19876543212-12345678901))

しかし、それは効率的ではありません。

編集:

syswrite の測定を間違えた理由はわかりません。これは 3.5 GB/秒です:

perl -e 'sysseek(STDIN,shift,0) || die; $left = shift; \
         while($read = sysread(STDIN,$buf, ($left > 32768 ? 32768 : $left))){ \
            $left -= $read; syswrite(STDOUT,$buf);
         }' 12345678901 $((19876543212-12345678901)) < bigfile

そしてyes | dd bs=1024k count=10 | wc悪夢を回避します。

答え1

これはブロックサイズが小さいため遅いです。最近のGNU ddコアユーティリティv8.16以降)、最も簡単な方法は、skip_bytesおよびcount_bytesオプションを使用することです。

in_file=1tb

start=12345678901
end=19876543212
block_size=4096

copy_size=$(( $end - $start ))

dd if="$in_file" iflag=skip_bytes,count_bytes,fullblock bs="$block_size" \
  skip="$start" count="$copy_size"

アップデート

fullblockオプションは上記に従って追加されました@Gilles の回答最初は によって暗示されるかもしれないと思いましたがcount_bytes、そうではありませんでした。

言及されている問題は、以下の潜在的な問題であり、dd読み取り/書き込み呼び出しが何らかの理由で中断されると、データが失われます。ほとんどの場合、これは起こりそうにありません (パイプではなくファイルから読み取っているため、可能性はいくらか低くなります)。


およびオプションddなしでを使用するのはより困難です。skip_bytescount_bytes

in_file=1tb

start=12345678901
end=19876543212
block_size=4096

copy_full_size=$(( $end - $start ))
copy1_size=$(( $block_size - ($start % $block_size) ))
copy2_start=$(( $start + $copy1_size ))
copy2_skip=$(( $copy2_start / $block_size ))
copy2_blocks=$(( ($end - $copy2_start) / $block_size ))
copy3_start=$(( ($copy2_skip + $copy2_blocks) * $block_size ))
copy3_size=$(( $end - $copy3_start ))

{
  dd if="$in_file" bs=1 skip="$start" count="$copy1_size"
  dd if="$in_file" bs="$block_size" skip="$copy2_skip" count="$copy2_blocks"
  dd if="$in_file" bs=1 skip="$copy3_start" count="$copy3_size"
}

異なるブロックサイズを試すこともできますが、それほど劇的な効果は得られません。dd の bs パラメータの最適値を決定する方法はありますか?

答え2

bs=1dd一度に 1 バイトずつ読み取りと書き込みを行うように指示します。呼び出しreadごとにオーバーヘッドが発生するwriteため、処理が遅くなります。適切なパフォーマンスを得るには、ブロック サイズを大きくしてください。

ファイル全体をコピーする場合、少なくともLinuxでは、cpそしてcatより速いdd大きなブロック サイズを指定した場合でも、

ファイルの一部だけをコピーするには、tailにパイプします。これには、指定されたバイト数をコピーする必要headがある GNU coreutils またはその他の実装が必要です(は POSIX にありますが、そうではありません)。Linux での簡単なベンチマークでは、おそらくパイプが原因で、よりも遅いことがわかりました。head -ctail -chead -cdd

tail -c $((2345678901+1)) | head -c $((19876543212-2345678901))

問題dd信頼性が低い:部分的なデータをコピーできる私の知る限り、dd通常のファイルを読み書きする場合は安全です。dd はいつデータのコピーに適しているのでしょうか? (または、read() と write() が不完全なのはいつでしょうか)- しかし信号によって中断されない限りGNU coreutils ではfullblockフラグを使用できますが、これは移植性がありません。

のもう 1 つの問題はdd、スキップされたバイト数と転送されたバイト数の両方がブロック サイズの倍数である必要があるため、機能するブロック数を見つけるのが難しいことです。 を複数回呼び出すことができます。1dd回は最初の部分ブロックをコピーし、1 回は整列したブロックの大部分をコピーし、1 回は最後の部分ブロックをコピーします。詳細については、グレアムの答えシェル スニペット用。ただし、スクリプトを実行するときは、フラグを使用しない限り、すべてのデータがコピーされるfullblockことを確認する必要があることを忘れないでください。コピーが部分的である場合は 0 以外のステータスが返されるため、エラーを検出するのは簡単ですが、それを修復する実用的な方法はありません。dddd

POSIX には、シェル レベルではこれより優れたものはありません。私のアドバイスとしては、小さな専用 C プログラムを書くことです (実装内容に応じて、dd_done_rightまたはtail_headまたは と呼ぶことができますmini-busybox)。

答え3

dd

dd if=1tb skip=12345678901 count=$((19876543212-12345678901)) bs=1M iflags=skip_bytes,count_bytes

代わりに以下も使用できますlosetup:

losetup --find --show --offset 12345678901 --sizelimit $((19876543212-12345678901))

そしてdd、、、catループ装置。

答え4

これを行う方法は次のとおりです:

   i=$(((t=19876543212)-(h=12345678901)))
   { dd count=0 skip=1 bs="$h"
     dd count="$((i/(b=64*1024)-1))" bs="$b"
     dd count=1 bs="$((i%b))"
   } <infile >outfile

本当に必要なのはそれだけです。それ以上は必要ありません。まず第一に、通常のファイル入力をほぼ瞬時に処理dd count=0 skip=1 bs=$block_size1します。lseek()欠落データあるいは、それについて他のどんな嘘が言われていようと、希望する開始位置に直接シークすることができます。ファイル記述子はシェルによって所有され、 はddそれを継承しているだけなので、カーソル位置に影響を与え、段階的に進めることができます。これは本当に非常に簡単で、 以上にこのタスクに適した標準ツールはありませんdd

これは、多くの場合理想的な 64k のブロック サイズを使用します。一般に信じられていることとは反対に、ブロック サイズを大きくしてもdd作業は速くなりません。一方、小さなバッファも良くありません。ddシステム コールの時間を同期して、データをメモリにコピーして再びコピーするのを待つ必要がないようにするだけでなく、システム コールを待つ必要もありません。したがって、次の処理がread()最後の処理を待たずに済むように十分な時間をかけますが、必要以上に大きなサイズでバッファリングするほど長くはかけないようにする必要があります。

最初はddスタート位置までスキップします。ゼロその時点で他の任意のプログラムを呼び出してその標準入力を読み込むと、希望するバイトオフセットから直接読み込みが開始されますdd((interval / blocksize) -1)ブロック数を stdout にカウントします。

最後に必要なのは係数をコピーすることです(もしあれば)前回の分割操作の。これで終わりです。

ところで、証拠もなしに事実を顔だけで述べる人は信じないでください。はい、dd短い読み物でも可能です(ただし、正常なブロック デバイスから読み取る場合はそのようなことは不可能です。これが名前の由来です)ddこのようなことは、ブロック デバイス以外から読み取られたストリームを正しくバッファリングしない場合にのみ可能です。例:

cat data | dd bs="$num"    ### incorrect
cat data | dd ibs="$PIPE_MAX" obs="$buf_size"   ### correct

どちらの場合もddコピー全てデータの。最初のケースでは、(ただし、 ではありそうにないcatコピーされる出力ブロックの一部は、仕様によりdd「$num​​」バイトに等しくなります。ddのみコマンドラインでバッファが明示的に要求されたときに、バッファリングを一切行わないbs=最大ブロックサイズは目的ddリアルタイム I/O です。

2 番目の例では、出力ブロック サイズを明示的に指定し、dd完全な書き込みが行えるまで読み取りをバッファします。これは、count=入力ブロックに基づく には影響しませんが、そのためには別の が必要ですdd。それ以外の場合に提供される誤った情報は無視してください。

関連情報