NTFS 疎なファイル (sparse file) [C++]



(2018.11.4) 新規作成。Samba の挙動を確認。

Windows では, NTFS ファイルシステム上に "疎 (そ) なファイル" sparse file を作ることができます。スカスカのファイルで、内容バイト列の実体が飛び飛びにしかありません。

ここでは、ディスクドライブの大きさよりはるかに大きいファイルを作ってみます。

NTFS の制限

NTFS でのファイルサイズの上限は, どれぐらいでしょうか? Web上には、相互に異なることを書いてあるページが多数あり、混乱します。

正しくは次のとおりです:

  1. 一つの volume の大きさが, クラスタの数が 232 まで, という制限を受ける. ここで, 新しい OS では 264 まで、といった記述はすべて誤りです。
  2. ディスクドライヴのクラスタの大きさによって, volume の大きさの上限が決まる. ここが Windows のバージョンによって異なります.
  3. ファイルサイズの上限は, volume の上限と同じ.

つまり, ファイルサイズの上限が、何か単独で決まるわけではありません。それから、ディスクドライヴのクラスタサイズによって上限が決まります。ここが明確になっていない解説が多いため、混乱します.

Windowsバージョン クラスタサイズの上限 volumeサイズの上限
Windows 7, Windows Server 2008 R2 以前 4KB 16 TB
Windows 8 〜 10 v1703, Windows Server 2012 〜 2016 64 KB 256 TB
Windows 10 v1709, Windows Server 2019以降 2 MB 8 PB

実際のクラスタの大きさを知るには, SysInternalsntfsinfo コマンドが簡単です。次のように表示されます (抜粋):

Allocation Size
----------------
Bytes per sector       : 512
Bytes per cluster      : 4096
Bytes per MFT record   : 0
Clusters per MFT record: 0

サンプル

DeviceIoControl()FSCTL_SET_SPARSE を与えると、疎らなファイルになります。

データを書き込まずに, SetFilePointerEx() でポインタを進めると, その間が穴になります。

C++
[RAW]
  1. #include "pch.h"
  2. #include <string>
  3. #include <assert.h>
  4. #include <winioctl.h> // FSCTL_SET_SPARSE
  5. #include <tchar.h>
  6. #include "../stream_common.h"
  7. using namespace std;
  8. extern void enum_stream_headers(LPCTSTR fname);
  9. int _tmain()
  10. {
  11. // NTFS は、ファイルサイズの制限はないが, volume sizeの制限がある。
  12. // 16 TB (4 KB Cluster Size) or 256 TB (64 KB Cluster Size).
  13. // Allocation units (clusters) の数が 2^32 に制約されているため.
  14. // => ファイルサイズもこれに引きずられる.
  15. // Web上は全然違うことを書いているページも多いが、
  16. // Windows 10 の実際の挙動は、上記のとおり。
  17. // 公式も同じ解説:
  18. // https://docs.microsoft.com/en-us/windows-server/storage/file-server/ntfs-overview
  19. // https://docs.microsoft.com/en-us/windows/desktop/fileio/filesystem-functionality-comparison
  20. LONG hi_size[] = { 1, // 4 GB
  21. 3900, // 15.6 TB
  22. 4100, // 16.4 TB
  23. 63000, // 252 TB
  24. 65000, // 260 TB
  25. -1 };
  26. for (int i = 0; hi_size[i] > 0; i++) {
  27. TCHAR fname[100];
  28. wsprintf(fname, L"large-%d", i + 1);
  29. HANDLE hFile = CreateFile(fname, GENERIC_WRITE,
  30. 0, // dwShareMode
  31. nullptr,
  32. CREATE_ALWAYS,
  33. 0, NULL);
  34. assert(hFile != INVALID_HANDLE_VALUE);
  35. DWORD bytes_returned = 0;
  36. BOOL r = ::DeviceIoControl(hFile, FSCTL_SET_SPARSE,
  37. NULL, 0, NULL, 0,
  38. &bytes_returned, NULL);
  39. assert(r != 0);
  40. // SetFilePointer() はエラーチェックが難しい.
  41. //DWORD rlo = SetFilePointer(h, 0, &dist, // 64bit
  42. // FILE_BEGIN);
  43. // 1バイト書き込む
  44. LARGE_INTEGER dist, pointer_ret;
  45. dist.LowPart = 5 * 1000;
  46. dist.HighPart = 0;
  47. r = SetFilePointerEx(hFile, dist, &pointer_ret, FILE_BEGIN);
  48. assert(r);
  49. BYTE buf[1];
  50. buf[0] = 59;
  51. DWORD written_ret = 0;
  52. r = WriteFile(hFile, buf, 1, &written_ret, NULL);
  53. assert(r != 0 && written_ret == 1);
  54. // 遠くまで進める
  55. dist.LowPart = 0;
  56. dist.HighPart = hi_size[i];
  57. r = SetFilePointerEx(hFile, dist, &pointer_ret, FILE_BEGIN);
  58. if (!r)
  59. error(wstring(L"SetFilePointerEx() failed: ") + to_wstring(i));
  60. r = SetEndOfFile(hFile); // こちらがエラーになる. マジか.
  61. if (!r) {
  62. _tprintf(L"SetEndOfFile() failed: %d\n", i);
  63. CloseHandle(hFile);
  64. break;
  65. }
  66. CloseHandle(hFile);
  67. }
  68. enum_stream_headers(L"large-1");
  69. return 0;
  70. }

実行結果::

> .\sparse1.exe
SetEndOfFile() failed: 2
ret_bytes = 20
dwStreamId = 1, dwStreamAttributes = 8, Size = 0, dwStreamNameSize = 0
stream name: (none)
ret_bytes = 20
dwStreamId = 9, dwStreamAttributes = 8, Size = 65544, dwStreamNameSize = 0
stream name: (none)
ret_bytes = 20
dwStreamId = 9, dwStreamAttributes = 8, Size = 8, dwStreamNameSize = 0
stream name: (none)
ret_bytes = 0

ストリームに「疎ら」の属性が付きます。dwStreamAttributesSTREAM_SPARSE_ATTRIBUTE フラグが立ちます.

一つのストリームの中で, dwStreamId = 1BACKUP_DATAです。この例だと, dwStreamId = 9BACKUP_SPARSE_BLOCK で, 65544 は 16進で 0x10008 です。

どうやら, 書き込んだのは1バイトだけですが, 64Kバイトを1ブロックとして実在バイトで埋められるようです。+ 8バイトの何か、という形式のようです。

このように, 巨大に見えるファイルができます。

> dir
Mode                LastWriteTime         Length Name
----                -------------         ------ ----
-a----       2018/11/04     19:56     4294967296 large-1
-a----       2018/11/04     19:56 16750372454400 large-2
-a----       2018/11/04     19:56           5001 large-3
-a----       2018/11/04     19:44         113152 sparse1.exe

ディスクドライブよりはるかに大きいファイルです。エクスプローラのプロパティで見ると、実際には64KBしか消費していません。

Samba

疎らなファイルは、Samba上にも作ることができます。ただし、設定によります。

先ほどのサンプルを Sambaドライブを宛先にして走らせます。実行結果は, 上とほとんど同じです。こちらは4Kバイトが1ブロックのようです。

SetEndOfFile() failed: 2
ret_bytes = 20
dwStreamId = 1, dwStreamAttributes = 8, Size = 0, dwStreamNameSize = 0
stream name: (none)
ret_bytes = 20
dwStreamId = 9, dwStreamAttributes = 8, Size = 4104, dwStreamNameSize = 0
stream name: (none)
ret_bytes = 20
dwStreamId = 9, dwStreamAttributes = 8, Size = 8, dwStreamNameSize = 0
stream name: (none)
ret_bytes = 0

単に ls で表示すると、非常に巨大です。実際の消費は 8Kバイトだけです。

$ ls -l
-rw-r--r--.  1 hori hori     4294967296 Nov  4 20:22  large-1
-rw-r--r--.  1 hori hori 16750372454400 Nov  4 20:22  large-2
-rw-r--r--.  1 hori hori           5001 Nov  4 20:22  large-3
$ ls -lhs
8.0K -rw-r--r--.  1 hori hori 4.0G Nov  4 20:22  large-1
8.0K -rw-r--r--.  1 hori hori  16T Nov  4 20:22  large-2
8.0K -rw-r--r--.  1 hori hori 4.9K Nov  4 20:22  large-3