ファイルシステムのブロックサイズといえば、stat コマンドで表示されるこれ (IO Block: 4096
)。
どういうときにこの値がユーザ空間で利用されているかを調べた。
$ stat / File: ‘/’ Size: 270 Blocks: 0 IO Block: 4096 directory Device: ca01h/51713d Inode: 96 Links: 19 Access: (0555/dr-xr-xr-x) Uid: ( 0/ root) Gid: ( 0/ root) Access: 2022-04-28 19:55:24.684679391 +0000 Modify: 2022-05-30 00:34:54.501073199 +0000 Change: 2022-05-30 00:34:54.501073199 +0000 Birth: -
結論
cp や cat は、ファイルシステムのブロックサイズを基にバッファリングする I/O サイズを調整している。
動作確認
この調整はとくにネットワーク経由の I/O で効果的なため、NFS を例に動作確認する。
まずは nfs ファイルシステムを /mnt/nfs
にマウントする。
$ sudo mkdir /share $ sudo chmod 1777 /share $ sudo echo "/share 127.0.0.1(rw)" | sudo tee /etc/exports /share 127.0.0.1(rw) $ sudo systemctl start nfs-server $ sudo mkdir /mnt/nfs $ sudo mount -t nfs 127.0.0.1:/share /mnt/nfs/
いちおう nfs がマウントされていることと、適当に作成したファイルを stat してブロックサイズが IO Block: 131072
であることを確認する。
$ mount -t nfs4 127.0.0.1:/share on /mnt/nfs type nfs4 (rw,relatime,vers=4.1,rsize=131072,wsize=131072,namlen=255,hard,proto=tcp,timeo=600,retrans=2,sec=sys,clientaddr=127.0.0.1,local_lock=none,addr=127.0.0.1) $ touch /mnt/nfs/dummy $ stat /mnt/nfs/dummy File: ‘/mnt/nfs/dummy’ Size: 0 Blocks: 0 IO Block: 131072 regular empty file Device: 2ah/42d Inode: 54531515 Links: 1 Access: (0664/-rw-rw-r--) Uid: ( 1000/ec2-user) Gid: ( 1000/ec2-user) Access: 2022-06-01 00:04:32.547735818 +0000 Modify: 2022-06-01 00:04:32.547735818 +0000 Change: 2022-06-01 00:04:32.547735818 +0000 Birth: -
ここで、1M のファイルを作成し、nfs ファイルシステム配下に cp する。 すると、先ほどのブロックサイズ(131072) の単位で write されていることがわかる。
$ dd if=/dev/zero of=1m bs=1M count=1 1+0 records in 1+0 records out 1048576 bytes (1.0 MB) copied, 0.000975111 s, 1.1 GB/s $ sudo strace -e write cp 1m /mnt/nfs/ write(4, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 131072) = 131072 write(4, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 131072) = 131072 write(4, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 131072) = 131072 ...
また cat すると、131072 の単位で write されていることがわかる。
$ strace -e write cat /mnt/nfs/1m > /dev/null write(1, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 131072) = 131072 write(1, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 131072) = 131072 write(1, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 131072) = 131072 ...
で、なぜこうなっているかというと、coreutils の src/ioblksize.h で、io_blksize が次のように定義されているから。
IO_BUFSIZE(64*1024
= 65536) と stat で取得できる blksize のどちらか大きいほうが利用される。
enum { IO_BUFSIZE = 64*1024 }; static inline size_t io_blksize (struct stat sb) { return MAX (IO_BUFSIZE, ST_BLKSIZE (sb)); }
このように、とくに NFS や CIFS などのネットワーク経由のファイルシステムでブロックサイズを大きめに取ることで、とくに同期書き込みの場合に頻繁に I/O を発行することなく、処理ができるようになる(もう少し正確にいうと、ネットワーク経由で read/write する単位はそれぞれ rsize/wsize で調整する。ここでいうブロックサイズはユーザ空間でバッファリングするサイズ。ただし同期書き込みの場合は、read(2)/write(2) のたびにネットワーク経由の I/O が発生するので、バッファリングするサイズが重要になる。)。