Linuxシェルのパイプはメモリに優しい仕組みだった(バッファリングを学ぶ)

2021-07-03Bash

はじめに

先日、メンバからLinuxシェルにおけるパイプ機能について質問がありました。

  1. パイプで繋いだ前後のプロセスは同時に起動する?
  2. パイプで繋いだ前後のプロセスで、データのやり取りのためにどのぐらいのメモリを消費するのか?( 大量のデータを流した場合を懸念 )

回答したあとに、自分で言っていたことに少し自信がなかったので調べてみました。

特にメモリがどの程度使われるのか?について触れることがなかったので、実際にコマンドを実行しながら学習しました。

検証環境

$ uname -smoi
Darwin x86_64 MacBookPro16,1 Darwin

$ bash -version | head -n 1
GNU bash, version 4.1.2(1)-release (x86_64-redhat-linux-gnu)

$ seq --version | head -n 2
seq (GNU coreutils) 8.32
Copyright (C) 2020 Free Software Foundation, Inc.

Linux のパイプ処理の特徴についておさらい

command1 | command2 のようなパイプでつないだ処理の特徴をあげてみます。

  • command1command2 の両プロセスは同時に立ち上がる
  • command1 はメモリバッファに随時出力を書きだす
  • command2 はメモリバッファから随時入力を読み取る
  • カーネルで設定されているパイプのバッファサイズ分しかメモリを消費しない
  • command2 がバッファからデータを読み取り、バッファに空きができるまで command1 は書き出しを待機する

パイプのバッファサイズは、Linux 2.6.11以降デフォルトで 65536 bytes ( 4096 bytes/page * 16 pages ) となっています。

パイプにおけるバッファリングの理解は正しいのでしょうか?
大量データを処理したとしても、たったこれだけしかメモリを使用しないのでしょうか?

実際に確認してみます。

検証してみる

seq コマンドで 1 行 256 bytes のデータ ( 数値 255 bytes + 改行コード 1 bytes ) を、パイプ経由で随時送り出します。

パイプでのデータ受信先では、 while read 構文を使い 5 秒おきに 1 行 ( 256 bytes ) ずつ読み込んでいきます。

同時に別のターミナルで seq コマンドの状態がどう変化するかを監視します。

その1: 「パイプのバッファサイズちょうど」のデータサイズを次のコマンドに送り込む場合

256 bytes のデータを 256 行 送信した場合のデータ量は、パイプのバッファサイズと同じサイズの 65536 bytes となります。

# こんなデータを送り込んでみます。
# 先頭3行を確認。
$ seq -f '%0255g' 1 256 | head -n 3
000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001
000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002
000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003

# 末尾3行を確認。
$ seq -f '%0255g' 1 256 | tail -n 3
000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000254
000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000255
000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000256

# データサイズを確認
$ seq -f '%0255g' 1 256 \
  | wc -c
65536

# 実行
$ seq -f '%0255g' 1 256 \
  | while read f; do
    echo $f
    sleep 5
  done

コマンド実行中に別ターミナルを起動しておき、 watch コマンドを使って seq プロセスを監視しておきましたが、何も表示されません。

# seqコマンドプロセスが生まれると表示されるようになるはずだが、何も表示されない。
$ watch -n 1 "ps aux| grep [s]eq"

seq コマンドが送信したデータサイズは、パイプのバッファサイズに収まるサイズでした。
そのため、 seq プロセスは実行と同時にすべての出力ができたため、直ちに終了したものと思われます。

その2: 「パイプのバッファサイズ+1 行」のデータサイズを次のコマンドに送り込む場合

今度は、先程より 1 行だけ多い 257 行 送信してみます。
送信されるデータ量は、パイプのバッファサイズ ( 65536 bytes ) を超えた 65792 bytes となります。

# データサイズを確認
$ seq -f '%0255g' 1 257 \
  | wc -c
65792

# 実行
$ seq -f '%0255g' 1 257 \
  | while read f; do
    echo $f
    sleep 5
  done

先程同様、 watch コマンドを使って seq プロセスを監視しておきましたが、何も表示されません。

# seqコマンドプロセスが生まれると表示されるようになるはずだが、何も表示されない。
$ watch -n 1 "ps aux| grep [s]eq"

以下のような流れで処理が進んだと思われます。

  1. seq256 行 分のデータをバッファに書き込み
  2. ほぼ同時に while read1 行 分のデータを読み込み
  3. seq はバッファに空きができたため、更に 1 行分のデータをバッファに書き込みし、処理を終了

この場合でも、やはりすべての出力ができたため、 seq プロセスは直ちに終了したものと思われます。

その3: 「パイプのバッファサイズ+2 行」のデータサイズを次のコマンドに送り込む場合

最後に 256 bytes のデータを、更に 1 行 増やし、 258 行 送信してみます。

# データサイズを確認
$ seq -f '%0255g' 1 258 \
  | wc -c
66048

# 実行
$ seq -f '%0255g' 1 258 \
  | while read f; do
    echo $f
    sleep 5
  done

seq プロセス監視中のターミナルに変化が生じます

Every 1.0s: ps aux| grep [s]eq
502      32747  0.0  0.0 100920   716 pts/17   S+   08:00   0:00 seq -f %0255g 1 258

コマンド実行後、5秒立つと seq プロセスが終了し、 watch から見えなるなるはずです。

想定通りの挙動をしていて安心しました。

ひとこと

パイプでつないだ場合、大量データを処理させてもパイプのやり取りで消費するメモリはバッファサイズとなります。
メモリに優しい仕組みです。

2021-07-03Bash