ファイルシステムのブロックサイズといえば、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 が発生するので、バッファリングするサイズが重要になる。)。