Linux で疎なファイル(sparse file)を使う

作成日: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;
}

その他に以下のような手段でも作成できる。

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 flags
FIEMAP_EXTENT_LAST 0x0001Last extent in file.
FIEMAP_EXTENT_UNKNOWN 0x0002Data location unknown.
FIEMAP_EXTENT_DELALLOC 0x0004Location still pending. Sets EXTENT_UNKNOWN.
FIEMAP_EXTENT_ENCODED 0x0008Data can not be read while fs is unmounted.
FIEMAP_EXTENT_DATA_ENCRYPTED0x0080Data is encrypted by fs. Sets EXTENT_NO_BYPASS.
FIEMAP_EXTENT_NOT_ALIGNED 0x0100Extent offsets may not be block aligned.
FIEMAP_EXTENT_DATA_INLINE 0x0200Data mixed with metadata. Sets EXTENT_NOT_ALIGNED.
FIEMAP_EXTENT_DATA_TAIL 0x0400Multiple files in block. Sets EXTENT_NOT_ALIGNED.
FIEMAP_EXTENT_UNWRITTEN 0x0800Space allocated, but no data (i.e. zero).
FIEMAP_EXTENT_MERGED 0x1000File does not natively support extents. Result merged for efficiency.
FIEMAP_EXTENT_SHARED 0x2000Space shared with other files.

FIBMAP ioctl は実行に root 権限を必要としたが、FIEMAP ioctl はユーザ権限で実行可能である。

2.3 SEEK_DATA & SEEK_HOLE をつけて lseek

Linux 3.1 からは lseek() の引数 whenceSEEK_DATA または SEEK_HOLE を指定すると、ファイルの「穴」の位置を特定できる。

seek_hole.cSEEK_DATA または SEEK_HOLE を使ってファイルの穴を取得する例である。

FIEMAP extent flags
SEEK_DATA ファイルオフセットを offset 以上で次にデータのある位置に設定する。 offset がデータを指している場合には、ファイルオフセットは offset に設定される。
SEEK_HOLE ファイルオフセットを、位置が offset 以上の次のホール(hole)に設定する。 offset がホールの内部にある場合は、ファイルオフセットは offset に設定される。 offset 以降にホール(hole)がない場合は、ファイルオフセットはファイルの末尾に設定される。

SEEK_DATASEEK_HOLE のマクロを使うには、_GNU_SOURCE を定義してからコンパイルする必要がある。 SEEK_DATASEEK_HOLE を使った lseek() に対応する Linux カーネルほファイルシステムは以下のようになっている。

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 カーネルでテストを行った結果である。

FSSparse Block SizeFIEMAPFALLOC_FL_PUNCH_HOLEHole Block Size
ext24096OKNot supported---
ext34096OKNot supported---
ext44096OKOK4096
btrfs4096OKOK (Fedora 20 kernel-3.12.8 で確認)4096
xfs65536OKOK4096
nilfs24096OKNot supported---
ramfs4096Not supportedNot supported---
tmpfs4096Not supportedNot supported---

表の項目の意味は以下の通り。

コメント

トラックバック   [Trackback URL: http://www.nminoru.jp/cgi-bin/tb.cgi/programming__sparse_file]
コメントを書き込む

TOP    掲示板    戻る
Written by NAKAMURA Minoru, Twitter:@nminoru_jp