作成日:2012.04.28
更新日:2014.05.14
更新履歴
(2012.4.28) 2010年10月2日の日記と2011年2月2日の日記から作成。
(2014.2.20) Fedora 20 (kernel-3.12.8) で hole punch を確認。
(2014.5.14) 2.3 SEEK_DATA & SEEK_HOLE をつけて lseek の節を追加。
はじめに
UNIX には疎なファイル(sparse file)とか穴(hole)のあるファイルと呼ばれるが機構がある。 以下疎なファイルと呼ぶ。
疎なファイルは、ファイルの途中にディスクに割り付けられていない「穴」の領域があるファイルだ。
この領域は read()
すると 0 で埋められているように見える。
ファイルの穴の部分に write をした場合に初めて、ディスクのブロックが割り付けられる。
疎なファイルはコアダンプなどに使われる。 コアダンプは異常時にプロセスのメモリイメージを core.1932 などのファイルに書き出したものだが、プロセスのメモリマップは非連続的に飛び飛びにマップされている。 そのためプロセスのマップされていない部分までコアダンプすると無駄に大きくなってしまうから。
1. 疎なファイルの一般的な作り方
疎なファイルを作るのは簡単である。
lseek()
または truncate()
を使えば簡単に作成できる。
通常の write()
はオフセットアドレス 0 から順番に書き込みを行うが、lseek()
や truncate()
使うとオフセットを飛ばすことができるので「穴」があくことになる。
そのままファイルを close()
すると穴のあいたままのファイルができる。
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> int main(int argc, char** argv) { int fd = open("sparse-file.dump", O_CREAT|O_RDWR, S_IRUSR|S_IWUSR); /* 1MB の位置に移動する。 */ lseek(fd, 1024*1024, SEEK_SET); /* 最後の1バイトだけ書く。 */ char buf=0; write(fd, &buf, 1); close(fd); return 0; }
その他に以下のような手段でも作成できる。
- tar コマンドは S オプションをつけるとスパースファイルを穴があいた状態でアーカイブしてくれる。
ただしこれはファイルを
read()
したブロックが全部 0 だったら圧縮するというもので、スパースファイルでない全部 0 のファイルも同じように圧縮されてしまう。 - cp コマンドはファイルサイズと消費ブロックの差に注目し、コピー元がスパースファイルと判断した場合はコピー先をスパースファイルとして構成する。ただしこれもブロック単位の読み込みが全部 0 であったら穴をあけるというものなので、ところどころに全部 0 のブロックがあるスパースファイルを cp すると、完全なスパースファイルになってしまう。
2. 穴の検出方法
疎なファイルが使えるかどうかはファイルシステムによって決まるが、一部のファイルシステムは疎なファイルを 0 で埋め尽くされたブロックに展開してしまうものもある。
疎なファイルかどうかを調べるにはファイル長とファイルが占めているブロック長を比べればよい。 ls コマンドに -s オプションを付けると実際にディスクを占めているブロックを報告してくれる。
$ ls -sl sparse-file.dump normal-file.dump
1024 -rw-rw-r-- 1 nminoru nminoru 1048576 Apr 28 18:28 normal-file.dump
4 -rw------- 1 nminoru nminoru 1048577 Apr 28 18:27 sparse-file.dump
しかしファイルに穴があることは分かるが、実際にどの位置に穴があるかを調べる POSIX 準拠の方法はない。 しかし Linux は FIBMAP ioctl または FIEMAP ioctl を使えば穴を検出できる。
ただし全てのファイルシステムで FIBMAP ioctl や FIEMAP ioctl が有効なわけではない。 とりあえず ext4 では動作する。
2.1 FIBMAP
A Smachkernel of Opinion の FIBMAP ioctl example - get the file system block number of a fileのブログを参考にして確認すると、疎なファイルの穴の部分にあたる論理ブロックは、FIBMAP ioctl を使うと物理ブロック 0 を指していると回答される。
FIBMAP ioctl は穴があることをブロック単位で確認する。 また FIBMAP ioctl は実行には root 権限を必要する。
2.2 FIEMAP
Linux 2.6.38 では FIEMAP ioctl が使える。 この ioctl はファイルは連続領域をエクステントで返すことができる。 こちらを使うと FIBMAP ioctl よりも効率よく「穴」の位置を特定できる。
fiemap.c は FIEMAP ioctl を使ってファイルのエクステントを取得する例である。
4KBブロックで1論理ブロック目と3論理ブロック目が物理ブロックに割り当てられ、2論理ブロック目が穴になっているファイルの場合は以下のように見える。
$ ./fiemap sparse-file.dump FILE: # of extents=2, flags=1 Extent: 0 logical= 0, phy= 41221619712, len= 4096, flags=1000 Extent: 1 logical= 8192, phy= 41221623808, len= 4096, flags=1001
エクステントのフラグの意味は以下のようになっている。
FIEMAP_EXTENT_LAST | 0x0001 | Last extent in file. |
FIEMAP_EXTENT_UNKNOWN | 0x0002 | Data location unknown. |
FIEMAP_EXTENT_DELALLOC | 0x0004 | Location still pending. Sets EXTENT_UNKNOWN. |
FIEMAP_EXTENT_ENCODED | 0x0008 | Data can not be read while fs is unmounted. |
FIEMAP_EXTENT_DATA_ENCRYPTED | 0x0080 | Data is encrypted by fs. Sets EXTENT_NO_BYPASS. |
FIEMAP_EXTENT_NOT_ALIGNED | 0x0100 | Extent offsets may not be block aligned. |
FIEMAP_EXTENT_DATA_INLINE | 0x0200 | Data mixed with metadata. Sets EXTENT_NOT_ALIGNED. |
FIEMAP_EXTENT_DATA_TAIL | 0x0400 | Multiple files in block. Sets EXTENT_NOT_ALIGNED. |
FIEMAP_EXTENT_UNWRITTEN | 0x0800 | Space allocated, but no data (i.e. zero). |
FIEMAP_EXTENT_MERGED | 0x1000 | File does not natively support extents. Result merged for efficiency. |
FIEMAP_EXTENT_SHARED | 0x2000 | Space shared with other files. |
FIBMAP ioctl は実行に root 権限を必要としたが、FIEMAP ioctl はユーザ権限で実行可能である。
2.3 SEEK_DATA & SEEK_HOLE をつけて lseek
Linux 3.1 からは lseek()
の引数 whence に SEEK_DATA または SEEK_HOLE を指定すると、ファイルの「穴」の位置を特定できる。
seek_hole.c は SEEK_DATA または SEEK_HOLE を使ってファイルの穴を取得する例である。
SEEK_DATA | ファイルオフセットを offset 以上で次にデータのある位置に設定する。 offset がデータを指している場合には、ファイルオフセットは offset に設定される。 |
SEEK_HOLE | ファイルオフセットを、位置が offset 以上の次のホール(hole)に設定する。 offset がホールの内部にある場合は、ファイルオフセットは offset に設定される。 offset 以降にホール(hole)がない場合は、ファイルオフセットはファイルの末尾に設定される。 |
SEEK_DATA と SEEK_HOLE のマクロを使うには、_GNU_SOURCE を定義してからコンパイルする必要がある。
SEEK_DATA と SEEK_HOLE を使った lseek()
に対応する Linux カーネルほファイルシステムは以下のようになっている。
- Btrfs (Linux 3.1 以降)
- OCFS (Linux 3.2 以降)
- XFS (Linux 3.5 以降)
- ext4 (Linux 3.8 以降)
- tmpfs (Linux 3.8 以降)
3. ブロックが割り当て済みの位置に穴をあける
すでにブロックが割り付けられたファイルに対して穴を空ける(hole-punching)動作をしたいことがある。 例えば仮想マシンのディスクイメージのファイルなどである。
古典的なディスクは物理的に全てのブロックが存在し OS がブロックを使用していようがいまいが、物理的な実体が存在していた。 しかし SSD はディスク I/O から見える論理的なブロックと NAND フラッシュの実体がある物理的なブロックが異なり、ディスク I/O が実行され使用された論理ブロックに初めて物理ブロックがマッピングされる。 割り付けられない領域は未使用領域は GC に利用され、未使用領域が減ると SSD の性能が低下する。 また iSCSI や Fiber Channel でつながったストレージシステムも、実際の論理ブロックと物理ブロックが異なることがある。 このような場合、OS が使用していた領域を開放した場合に ATA の TRIM コマンド、SCSI の UNMAP コマンドなどを発行し使用しない旨を通知する。
仮想マシンはゲスト OS 側が TRIM や UNMAP を実行した場合、ゲストのディスクイメージファイルに「穴を空けて」ディスクの使用量の節約をしたい。
Linux の場合、fallocate()
システムコールに FALLOC_FL_PUNCH_HOLE のモードを指定すると既存のファイルに穴を開けることができる。
posix_fallocate(3)
ではなく Linux 固有の fallocate(2)
を使う。
#include <fcntl.h>
#include <linux/falloc.h>
fallocate(fd, FALLOC_FL_KEEP_SIZE | FALLOC_FL_PUNCH_HOLE, offset, len); /* FALLOC_FL_KEEP_SIZE も指定する必要がある */
hole-punch.c はサンプルプログラムである。
$ dd if=/dev/zero of=normal-file.dump bs=1024 count=1024
$ ./hole-punch normal-file.dump 4096 4096 ファイル名 穴を開けるオフセット 穴のサイズで指定する
./fiemap normal-file.dump
FILE: # of extents=2, flags=1
Extent: 0 logical= 0, phy= 10880024576, len= 4096, flags=0
Extent: 1 logical= 8192, phy= 10880032768, len= 1040384, flags=1
穴を空けるオフセットとサイズはファイルが存在する I/O ブロック境界に沿うこと。 デフォルトの ext4 だと 4096 バイトになる。
ファイルシステム別のテスト
Fedora16 の x86-64/Linux 3.3.2 カーネルでテストを行った結果である。
FS | Sparse Block Size | FIEMAP | FALLOC_FL_PUNCH_HOLE | Hole Block Size |
---|---|---|---|---|
ext2 | 4096 | OK | Not supported | --- |
ext3 | 4096 | OK | Not supported | --- |
ext4 | 4096 | OK | OK | 4096 |
btrfs | 4096 | OK | OK (Fedora 20 kernel-3.12.8 で確認) | 4096 |
xfs | 65536 | OK | OK | 4096 |
nilfs2 | 4096 | OK | Not supported | --- |
ramfs | 4096 | Not supported | Not supported | --- |
tmpfs | 4096 | Not supported | Not supported | --- |
表の項目の意味は以下の通り。
- Sparse Block Size は lseek などを使って最初に穴ができるサイズ。このブロックサイズよりも小さい穴はできない。
- FIEMAP が使えるかどうか。
- FALLOC_FL_PUNCH_HOLE が使えるかどうか。
- Hole Block Size は FALLOC_FL_PUNCH_HOLE を使って穴を空けることのできるサイズ。最初に lseek を使ってできる穴のサイズよりも小さくなることもあるようだ。