「dd bs=サイズ count=1」で パイプから入力するとデータが途切れる罠に注意せよ！

## はじめに: データが途切れる問題

`dd` コマンドでパイプから `dd bs=サイズ count=1` でデータを読み取ると途中で途切れることがあります。知っている人にとっては有名な問題だと思いますが。

```console:100行出力しているのに、少ない行数（実行するたびに変わる）しか読み取れていない！？
$ seq -f '%0999g' 100 | dd bs=100000 count=1 | wc -l
0+1 records in
0+1 records out
49152 bytes (49 kB, 48 KiB) copied, 5.0325e-05 s, 977 MB/s
49

$ seq -f '%0999g' 100 | dd bs=100000 count=1 | wc -l
0+1 records in
0+1 records out
4096 bytes (4.1 kB, 4.0 KiB) copied, 0.000208169 s, 19.7 MB/s
4
```

```console:1GBの空ファイルを作成しようとしても64KBにしかならない
$ cat /dev/zero | dd bs=1000000000 count=1 > zero.dat
0+1 records in
0+1 records out
65536 bytes (66 kB, 64 KiB) copied, 0.000117953 s, 556 MB/s
```

なぜこうなるのかというと **`bs` は読み取るデータサイズではなく1回で読み取る（最大の）データサイズ**なので、パイプからの入力が溜まっているバッファにまだ少ししか溜まっていない時に読み取ると、溜まっていたデータサイズで1回の読み取りを終了してしまうからです。1回の読み取りで終了することは `count=1` で指定しているからです。パイプ間通信の間にはパイプのバッファがあるため、おそらくそのバッファサイズが一度に読み込める限界でしょう。

```console:Linuxではおそらく64KBが一度に読み込める限界
$ seq -f '%0999g' 100 > /tmp/seq.txt

$ cat /tmp/seq.txt | dd bs=100000 count=1 | wc -l
0+1 records in
0+1 records out
65536 bytes (66 kB, 64 KiB) copied, 0.000194987 s, 336 MB/s
65
```

補足ですが、上記の「records in」とかは dd コマンドが標準エラー出力に出力しているステータスです。削除したい場合はこちらの記事を参照してください。

https://qiita.com/ko1nksm/items/8600903988f0d40cb747

## パイプから読み込まなければ問題なし

この問題はファイル指定やリダイレクトで読み取る場合には発生しません。（厳密には実装によると思いますが、普通に考えたらそんな変な実装をするとは思えないので）

```console:パイプを使わずにファイルから読み取っていれば OK
$ seq -f '%0999g' 100 > /tmp/seq.txt

$ dd if=/tmp/seq.txt bs=100000 count=1 | wc -l
1+0 records in
1+0 records out
100000 bytes (100 kB, 98 KiB) copied, 0.000146945 s, 681 MB/s
100

$ dd bs=100000 count=1 < /tmp/seq.txt | wc -l
1+0 records in
1+0 records out
100000 bytes (100 kB, 98 KiB) copied, 0.000141933 s, 705 MB/s
100
```

```console:ファイルから読み取っていれば1GBの空ファイルだって作成できる
$ dd if=/dev/zero bs=1000000000 count=1 > zero.dat
1+0 records in
1+0 records out
1000000000 bytes (1.0 GB, 954 MiB) copied, 2.00641 s, 498 MB/s

（出力もリダイレクトも使わなくて良いと思う）
$ dd if=/dev/zero bs=1000000000 count=1 of=zero.dat
1+0 records in
1+0 records out
1000000000 bytes (1.0 GB, 954 MiB) copied, 2.16077 s, 463 MB/s

$ dd bs=1000000000 count=1 < /dev/zero > zero.dat
1+0 records in
1+0 records out
1000000000 bytes (1.0 GB, 954 MiB) copied, 2.02139 s, 495 MB/s
```

一般論としてファイルが既にあるのであれば（効率の悪い）パイプを使う必要はありません。ただ無駄に CPU を消費するだけのパイプよりもリダイレクト（またはファイル名指定）を使って直接ファイルから読み取りましょう

## iflag=fullblock で解決可能

ファイルからの読み取りであればこの問題は発生しないはずですが、コマンドからの出力の場合、それを一旦ファイルに出力するのもパフォーマンスが悪くなるので避けたい所です。この問題は `iflag=fullblock` を指定することで解決可能です。

```console
$ seq -f '%0999g' 100 | dd bs=100000 count=1 iflag=fullblock | wc -l
1+0 records in
1+0 records out
100000 bytes (100 kB, 98 KiB) copied, 0.000221097 s, 452 MB/s
100
```

`iflag=fullblock` は POSIX.1-2024 で標準化されました。現時点では GNU、FreeBSD、macOS が対応していますが、NetBSD、OpenBSD などでは対応していないので注意が必要です。

## 「dd bs=1 count=サイズ」でも解決可能だがパフォーマンスは悪い

理屈の上では、1 バイトの読み込みを 100000 回繰り返すことで同じ結果になりますが、この方法はパフォーマンスが悪いです。下記の例では 704 kB/s しか出ていません。

```console
$ seq -f '%0999g' 100 | dd bs=1 count=100000 | wc -l
100000+0 records in
100000+0 records out
100000 bytes (100 kB, 98 KiB) copied, 0.141962 s, 704 kB/s
100
```

## `head -c` でも解決可能

そもそも `dd` コマンドを使わずに `head -c` を使用するのがわかりやすくおすすめです。

```console
$ seq -f '%0999g' 100 | head -c 100000 | wc -l
100
```

`head` コマンドの `-c` オプションは POSIX.1-2024 で標準化されましたが、現時点では OpenBSD 7.5 や Solaris 11 などでは使用できません。（注意 記事公開時に `head -count` が使えると書いていましたが、これは `head -n count` の意味であり間違いでした）

`head` コマンドはテキストファイルを扱うため、バイナリデータ（特に `0x00`）は壊れるんじゃないの？と思うかもしれませんが、`-c` オプションを指定したときは任意のデータを扱えることが POSIX で規定されています。ただし AIX (7.2 / 7.3) では `head` コマンドは最後に余計な改行を付けてしまいます。

```console:AIXでは余計な改行を付けてくれる
$ echo abcdefg | head -c 3 | od -tx1
0000000  61 62 63 0a
0000004
```

## 「dd bs=サイズ」にすれば解決可能？

`count=1` を省略すれば解決したように見えるかもしれません。

```console
$ seq -f '%0999g' 100 | dd bs=100000 | wc -l
0+16 records in
0+16 records out
100000 bytes (100 kB, 98 KiB) copied, 0.000142729 s, 701 MB/s
100
```

これは実際には読み込み回数を省略 = 何度でも読み込むという意味になっているので **`cat` コマンドを実行しているのとなにも変わりません**。なんなら省略しても意味は同じです。

```console:全部同じじゃないですか！？
$ seq -f '%0999g' 100 | dd bs=100000 | wc -l
0+16 records in
0+16 records out
100000 bytes (100 kB, 98 KiB) copied, 0.000142729 s, 701 MB/s
100

（bs のサイズが小さかろうが、全部のデータを読み切る）
$ seq -f '%0999g' 100 | dd bs=10 | wc -l
10000+0 records in
10000+0 records out
100000 bytes (100 kB, 98 KiB) copied, 0.0158481 s, 6.3 MB/s
100

（cat を実行しているのと同じ）
$ seq -f '%0999g' 100 | cat | wc -l
100

（なんなら cat すら不要）
$ seq -f '%0999g' 100 | wc -l
100
```

ちなみに `bs` を省略したときのデフォルト値は 512 バイトなので大きな値を指定することには意味があります。デフォルト値が小さいのは当時のコンピュータの性能の基準によるものでしょう。

読み取りバッファサイズを制御できるという点で、何かしらの違いが生まれるかもしれませんが、基本的には **`dd bs=サイズ` なんて書く必要はありません**。これは**標準入力から指定したサイズのデータを入力するという意味ではなく、読み取りバッファサイズを指定した単なる `cat` です**。なんでそんな CPU を無駄に消費するだけのことをいちいち書いているのかまったくわかりませんね。

## さいごに

標準入力から指定したバイト数を読み取るのであれば、`head -c サイズ` （または `head -サイズ`）を使用しましょう。通常は `dd` コマンドを使って読み取る必要はないのではないでしょうか。私も基本的に `dd` コマンドは使用していないはずですが・・・、どこかでこの罠にハマってないといいな・・・。