2012/11/06

SSDかどうかをC#から判別する

ある物理的なドライブがSSDかどうかを判別する方法は色々あり得るが、Microsoftが示したアルゴリズム(Windows 7 Disk Defragmenter User Interface Overview)にしたがってNyaRuRuさんがC++のコードを書かれていたので(SSDなら動作を変えるアプリケーションを作る)、同じことをC#からWin32を使って行うコードを書いてみた。

内容的にはほぼNyaRuRuさんのコードのままで、以下の方法の2本立て。
  • Windows 7以降で使えて、管理者権限が不要な「no seek penalty」を利用する。
  • それ以前のOSでも使えるが、管理者権限が必要な「nominal media rotation rate」を利用する。
まずWin32の宣言は以下のとおり。使用するWin32の関数はCreateFileとDeviceIoControlで、後はそれに必要な定数と構造体。
// For CreateFile to get handle to drive
private const uint GENERIC_READ = 0x80000000;
private const uint GENERIC_WRITE = 0x40000000;
private const uint FILE_SHARE_READ = 0x00000001;
private const uint FILE_SHARE_WRITE = 0x00000002;
private const uint OPEN_EXISTING = 3;
private const uint FILE_ATTRIBUTE_NORMAL = 0x00000080;

// CreateFile to get handle to drive
[DllImport("kernel32.dll", SetLastError = true)]
private static extern SafeFileHandle CreateFileW(
    [MarshalAs(UnmanagedType.LPWStr)]
    string lpFileName,
    uint dwDesiredAccess,
    uint dwShareMode,
    IntPtr lpSecurityAttributes,
    uint dwCreationDisposition,
    uint dwFlagsAndAttributes,
    IntPtr hTemplateFile);

// For control codes
private const uint FILE_DEVICE_MASS_STORAGE = 0x0000002d;
private const uint IOCTL_STORAGE_BASE = FILE_DEVICE_MASS_STORAGE;
private const uint FILE_DEVICE_CONTROLLER = 0x00000004;
private const uint IOCTL_SCSI_BASE = FILE_DEVICE_CONTROLLER;
private const uint METHOD_BUFFERED = 0;
private const uint FILE_ANY_ACCESS = 0;
private const uint FILE_READ_ACCESS = 0x00000001;
private const uint FILE_WRITE_ACCESS = 0x00000002;

private static uint CTL_CODE(uint DeviceType, uint Function,
     uint Method, uint Access)
{
    return ((DeviceType << 16) | (Access << 14) |
    (Function << 2) | Method);
}

// For DeviceIoControl to check no seek penalty
private const uint StorageDeviceSeekPenaltyProperty = 7;
private const uint PropertyStandardQuery = 0;

[StructLayout(LayoutKind.Sequential)]
private struct STORAGE_PROPERTY_QUERY
{
    public uint PropertyId;
    public uint QueryType;
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 1)]
    public byte[] AdditionalParameters;
}

[StructLayout(LayoutKind.Sequential)]
private struct DEVICE_SEEK_PENALTY_DESCRIPTOR
{
    public uint Version;
    public uint Size;
    [MarshalAs(UnmanagedType.U1)]
    public bool IncursSeekPenalty;
}

// DeviceIoControl to check no seek penalty
[DllImport("kernel32.dll", EntryPoint = "DeviceIoControl",
   SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool DeviceIoControl(
    SafeFileHandle hDevice,
    uint dwIoControlCode,
    ref STORAGE_PROPERTY_QUERY lpInBuffer,
    uint nInBufferSize,
    ref DEVICE_SEEK_PENALTY_DESCRIPTOR lpOutBuffer,
    uint nOutBufferSize,
    out uint lpBytesReturned,
    IntPtr lpOverlapped);

// For DeviceIoControl to check nominal media rotation rate
private const uint ATA_FLAGS_DATA_IN = 0x02;

[StructLayout(LayoutKind.Sequential)]
private struct ATA_PASS_THROUGH_EX
{
    public ushort Length;
    public ushort AtaFlags;
    public byte PathId;
    public byte TargetId;
    public byte Lun;
    public byte ReservedAsUchar;
    public uint DataTransferLength;
    public uint TimeOutValue;
    public uint ReservedAsUlong;
    public IntPtr DataBufferOffset;
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)]
    public byte[] PreviousTaskFile;
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)]
    public byte[] CurrentTaskFile;
}

[StructLayout(LayoutKind.Sequential)]
private struct ATAIdentifyDeviceQuery
{
    public ATA_PASS_THROUGH_EX header;
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 256)]
    public ushort[] data;
}

// DeviceIoControl to check nominal media rotation rate
[DllImport("kernel32.dll", EntryPoint = "DeviceIoControl",
   SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool DeviceIoControl(
    SafeFileHandle hDevice,
    uint dwIoControlCode,
    ref ATAIdentifyDeviceQuery lpInBuffer,
    uint nInBufferSize,
    ref ATAIdentifyDeviceQuery lpOutBuffer,
    uint nOutBufferSize,
    out uint lpBytesReturned,
    IntPtr lpOverlapped);
これを受けた1つ目のメソッドが「no seek penalty」を利用する「HasNoSeekPenalty」で、物理ドライブ名を「\\\\.\\PhysicalDrive0」の形にした文字列を引数に取る。QA@ITで質問させていただいたもの(SSDかどうかをC#から判別する方法)と基本的に同じ。あまり似た例がなかったので、ほぼスクラッチ。
// Method for no seek penalty
private static void HasNoSeekPenalty(string sDrive)
{
    SafeFileHandle hDrive = CreateFileW(
        sDrive,
        0, // No access to drive
        FILE_SHARE_READ | FILE_SHARE_WRITE,
        IntPtr.Zero,
        OPEN_EXISTING,
        FILE_ATTRIBUTE_NORMAL,
        IntPtr.Zero);

    if (hDrive == null || hDrive.IsInvalid)
    {
        string message = GetErrorMessage(Marshal.GetLastWin32Error());
        Console.WriteLine("CreateFile failed. " + message);
    }

    uint IOCTL_STORAGE_QUERY_PROPERTY = CTL_CODE(
        IOCTL_STORAGE_BASE, 0x500,
        METHOD_BUFFERED, FILE_ANY_ACCESS); // From winioctl.h

    STORAGE_PROPERTY_QUERY query_seek_penalty =
        new STORAGE_PROPERTY_QUERY();
    query_seek_penalty.PropertyId = StorageDeviceSeekPenaltyProperty;
    query_seek_penalty.QueryType = PropertyStandardQuery;

    DEVICE_SEEK_PENALTY_DESCRIPTOR query_seek_penalty_desc =
        new DEVICE_SEEK_PENALTY_DESCRIPTOR();

    uint returned_query_seek_penalty_size;

    bool query_seek_penalty_result = DeviceIoControl(
        hDrive,
        IOCTL_STORAGE_QUERY_PROPERTY,
        ref query_seek_penalty,
        (uint)Marshal.SizeOf(query_seek_penalty),
        ref query_seek_penalty_desc,
        (uint)Marshal.SizeOf(query_seek_penalty_desc),
        out returned_query_seek_penalty_size,
        IntPtr.Zero);

    hDrive.Close();

    if (query_seek_penalty_result == false)
    {
        string message = GetErrorMessage(Marshal.GetLastWin32Error());
        Console.WriteLine("DeviceIoControl failed. " + message);
    }
    else
    {
        if (query_seek_penalty_desc.IncursSeekPenalty == false)
        {
            Console.WriteLine("This drive has NO SEEK penalty.");
        }
        else
        {
            Console.WriteLine("This drive has SEEK penalty.");
        }
    }
}
(注)GetErrorMessageはエラーメッセージを得るためのメソッド。以下同じ。

2つ目のメソッドが「nominal media rotation rate」を利用する「HasNominalMediaRotationRate」で、引数は同じ。同じようにしてドライブの情報を取得する例は結構あったので(例えば、Getting Hard disk drive info with DeviceIOControl)、かなり参考にさせていただいた。
// Method for nominal media rotation rate
// (Administrative privilege is required)
private static void HasNominalMediaRotationRate(string sDrive)
{
    SafeFileHandle hDrive = CreateFileW(
        sDrive,
        GENERIC_READ | GENERIC_WRITE, // Administrative privilege is required
        FILE_SHARE_READ | FILE_SHARE_WRITE,
        IntPtr.Zero,
        OPEN_EXISTING,
        FILE_ATTRIBUTE_NORMAL,
        IntPtr.Zero);

    if (hDrive == null || hDrive.IsInvalid)
    {
        string message = GetErrorMessage(Marshal.GetLastWin32Error());
        Console.WriteLine("CreateFile failed. " + message);
    }

    uint IOCTL_ATA_PASS_THROUGH = CTL_CODE(
        IOCTL_SCSI_BASE, 0x040b, METHOD_BUFFERED,
        FILE_READ_ACCESS | FILE_WRITE_ACCESS); // From ntddscsi.h

    ATAIdentifyDeviceQuery id_query = new ATAIdentifyDeviceQuery();
    id_query.data = new ushort[256];

    id_query.header.Length = (ushort)Marshal.SizeOf(id_query.header);
    id_query.header.AtaFlags = (ushort)ATA_FLAGS_DATA_IN;
    id_query.header.DataTransferLength =
        (uint)(id_query.data.Length * 2); // Size of "data" in bytes
    id_query.header.TimeOutValue = 3; // Sec
    id_query.header.DataBufferOffset = (IntPtr)Marshal.OffsetOf(
        typeof(ATAIdentifyDeviceQuery), "data");
    id_query.header.PreviousTaskFile = new byte[8];
    id_query.header.CurrentTaskFile = new byte[8];
    id_query.header.CurrentTaskFile[6] = 0xec; // ATA IDENTIFY DEVICE

    uint retval_size;

    bool result = DeviceIoControl(
        hDrive,
        IOCTL_ATA_PASS_THROUGH,
        ref id_query,
        (uint)Marshal.SizeOf(id_query),
        ref id_query,
        (uint)Marshal.SizeOf(id_query),
        out retval_size,
        IntPtr.Zero);

    hDrive.Close();

    if (result == false)
    {
        string message = GetErrorMessage(Marshal.GetLastWin32Error());
        Console.WriteLine("DeviceIoControl failed. " + message);
    }
    else
    {
        // Word index of nominal media rotation rate
        // (1 means non-rotate device)
        const int kNominalMediaRotRateWordIndex = 217;

        if (id_query.data[kNominalMediaRotRateWordIndex] == 1)
        {
            Console.WriteLine("This drive is NON-ROTATE device.");
        }
        else
        {
            Console.WriteLine("This drive is ROTATE device.");
        }
    }
}
全体を通したコードはこちらに載せた。 Visual Studio 2012で.NET Framework 4.0を対象として作成し、ThinkPad X61sにインストールしたWindows 8 ProとWindows 7上で、SSD(Intel X25-M G2)とHDDを判別できることを確認している。 まあC++のライブラリを作ってそれを利用する形でもいいが、.NETから直接使える方法があってもいいかな、ということで。

[追記] 物理ドライブ名の取得

論理ドライブ名から物理ドライブ名を取得するコードも書いてみた。WMIを使う方法もあるが、折角なのでWin32で。 まずWin32の宣言。
// For CreateFile to get handle to drive
private const uint FILE_SHARE_READ = 0x00000001;
private const uint FILE_SHARE_WRITE = 0x00000002;
private const uint OPEN_EXISTING = 3;
private const uint FILE_ATTRIBUTE_NORMAL = 0x00000080;

// CreateFile to get handle to drive
[DllImport("kernel32.dll", SetLastError = true)]
private static extern SafeFileHandle CreateFileW(
    [MarshalAs(UnmanagedType.LPWStr)]
    string lpFileName,
    uint dwDesiredAccess,
    uint dwShareMode,
    IntPtr lpSecurityAttributes,
    uint dwCreationDisposition,
    uint dwFlagsAndAttributes,
    IntPtr hTemplateFile);

// For control codes
private const uint IOCTL_VOLUME_BASE = 0x00000056;
private const uint METHOD_BUFFERED = 0;
private const uint FILE_ANY_ACCESS = 0;

private static uint CTL_CODE(uint DeviceType, uint Function,
                             uint Method, uint Access)
{
    return ((DeviceType << 16) | (Access << 14) |
            (Function << 2) | Method);
}

// For DeviceIoControl to get disk extents
[StructLayout(LayoutKind.Sequential)]
private struct DISK_EXTENT
{
    public uint DiskNumber;
    public long StartingOffset;
    public long ExtentLength;
}

[StructLayout(LayoutKind.Sequential)]
private struct VOLUME_DISK_EXTENTS
{
    public uint NumberOfDiskExtents;
    [MarshalAs(UnmanagedType.ByValArray)]
    public DISK_EXTENT[] Extents;
}

// DeviceIoControl to get disk extents
[DllImport("kernel32.dll", EntryPoint = "DeviceIoControl",
           SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool DeviceIoControl(
    SafeFileHandle hDevice,
    uint dwIoControlCode,
    IntPtr lpInBuffer,
    uint nInBufferSize,
    ref VOLUME_DISK_EXTENTS lpOutBuffer,
    uint nOutBufferSize,
    out uint lpBytesReturned,
    IntPtr lpOverlapped);
これを受けたメソッド。論理ドライブ名の文字を引数とし、始めにリムーバブルディスクなどではない固定ディスクかどうかのチェックを通す。
// Method for disk extents
private static void GetDiskExtents(char cDrive)
{
    DriveInfo di = new DriveInfo(cDrive.ToString());
    if (di.DriveType != DriveType.Fixed)
    {
        Console.WriteLine("This drive is not fixed drive.");
    }

    string sDrive = "\\\\.\\" + cDrive.ToString() + ":";

    SafeFileHandle hDrive = CreateFileW(
        sDrive,
        0, // No access to drive
        FILE_SHARE_READ | FILE_SHARE_WRITE,
        IntPtr.Zero,
        OPEN_EXISTING,
        FILE_ATTRIBUTE_NORMAL,
        IntPtr.Zero);

    if (hDrive == null || hDrive.IsInvalid)
    {
        string message = GetErrorMessage(Marshal.GetLastWin32Error());
        Console.WriteLine("CreateFile failed. " + message);
    }

    uint IOCTL_VOLUME_GET_VOLUME_DISK_EXTENTS = CTL_CODE(
        IOCTL_VOLUME_BASE, 0,
        METHOD_BUFFERED, FILE_ANY_ACCESS); // From winioctl.h

    VOLUME_DISK_EXTENTS query_disk_extents =
        new VOLUME_DISK_EXTENTS();

    uint returned_query_disk_extents_size;

    bool query_disk_extents_result = DeviceIoControl(
        hDrive,
        IOCTL_VOLUME_GET_VOLUME_DISK_EXTENTS,
        IntPtr.Zero,
        0,
        ref query_disk_extents,
        (uint)Marshal.SizeOf(query_disk_extents),
        out returned_query_disk_extents_size,
        IntPtr.Zero);

    hDrive.Close();

    if (query_disk_extents_result == false ||
        query_disk_extents.Extents.Length != 1)
    {
        string message = GetErrorMessage(Marshal.GetLastWin32Error());
        Console.WriteLine("DeviceIoControl failed. " + message);
    }
    else
    {
        Console.WriteLine("The physical drive number is: " +
                          query_disk_extents.Extents[0].DiskNumber);
    }
}
[修正] SafeFileHandleの解放

ドライブへのハンドルの解放にCloseHandle関数を使っていたが、SafeFileHandleにCloseHandleを使うと、その後にガベージコレクタがメモリを解放するときにエラーを起こすらしいと気づいたので、SafeHandle.Closeメソッドを使うように修正した。

この問題は外部コンポーネントで不規則に起こるので、なかなか原因を掴めなかったが、Stack Overflowにこれに触れたコメントがあった(How to resolve SEHException, VB .NET 2010)。MSDNなどでソースは見つけられなかったが、問題の状況によく合うし、修正してからは起こらなくなったので、当たっていたのだと思う。

2012/10/31

CrystalDiskMarkとTrim

SSDの速度を測る場合、その前の使い方(とくにライト)が影響するのはよく知られていて、Secure EraseやTrimでクリーンな状態にした後、ライトを繰り返して乱れた状態が一定以上に達すると速度低下が起こったりする。

そうした影響をなるべく防ぐための工夫は色々あり得るが、理由あって同じSSDを連続して計測する場合、その間にTrimをかけるのはどうかというアイデアを持っていた。で、最近はTrimもすっかり一般化したことだし、CrystalDiskMarkを使って試してみた(結果的には不発)。

1. 計測方法


CrystalDiskMarkを連続して実行し、その実行間にTrimをかける場合とかけない場合で差が出るかを見る。

環境
  • PC: ThinkPad X61s
  • SSD: Intel X25-M G2 80GB(OSなどで全容量の60%が埋まった状態)
  • OS: Windows 8 Pro 64bit(ドライバーはすべてOSの自動インストール)
方法
  • CrystalDiskMark(3.0.2)を4GBで空のNTFSのパーティションに対し、テスト種類All、テスト回数9、テストサイズ2000MBの設定で30回実行。
  • 実行間には1分の間隔を挟み、Trimをかける場合は前回の実行直後にこのパーティションを対象に実行(先に示した方法で)。
  • どちらの場合も最初の実行前にTrimをかける。
  • ライト量の確認のため、一連の実行の直前と直後にCrystalDiskInfo(5.1.0 RC2)でHost Writes(総書き込み量)を取得する。Host Writesはあくまでホスト側(PC本体側)から見たライト量なので、SSD内部のライト量には直結しないが、参考にはなる。なお、この間は他のライトを伴う作業はしない(OSが勝手に行うものは除く)。
SSDのX25-Mには今更感があるが、これまでの実績から見て余裕を大幅に残したまま引退となるのは確実なので、多少消耗させてもよしという事情も背景にあり。

また、こういう計測をきっちり進めていくのは結構面倒なものだが、そこはDiskMarkStreamで楽々(自画自賛)、というよりDiskMarkStreamがあったからやる気になったというべきか。

2. 計測結果


先にどれぐらいのライト量になったかを確認すると、まずTrimをかけなかった場合の実行前と実行後のCrystalDiskInfoの結果。

次にTrimをかけた場合の実行前と実行後。
(注)E9が96から95に落ちているが、96になったのはかなり以前のことなので、意味はない。

一番最初のHost Writesの生の値は76752で、この16進数を10進数に換算すると485202になる。この値は65536セクタ、すなわち65536×512÷1024^2=32(MiB)ごとに1増えていくので、容量としては以下のようになる。

485202×32=15526464(MiB)≒14.807(TiB)

同様にそれぞれの値を計算し、実行前と実行後の差から増加量を求めると、
生の値
換算後
増加量
16進数
10進数
(MiB)
(TiB)
(MiB)
(GiB)
Trimをかけなかった場合
実行前767524852021552646414.807984320961.25
実行後7DF7A5159621651078415.746
Trimをかけた場合
実行前7E15F5164471652630415.761978496955.56
実行後858D15470251750480016.694

これらの増加量はそれぞれ9×30=270回分のテストによるものなので、1回のテストの増加量は約3.54~3.56GiBとなるが、これはテストサイズが2GBということを考えるとこんなものかと思う。どちらも全増加量は1TiB近くに上っている。

ここで、X25-M G2で速度低下を起こした後にTrimで回復した例(SSDの性能低下とTrimの効き具合を大検証)を見ると、一旦容量の90%までライトで埋めている。

方法は違うが、この全増加量は全容量80GBの12倍に当たるので、変化を起こさせるに十分なライト量ではないかと思う。というか思った、が……。

結論から言うと、CrystalDiskMarkの結果は不発だった。

まずTrimをかけなかった場合、30回の実行中、ばらつきは多少あるが、有意な変化といえるものはなかった。以下は1回目、15回目、30回目のもの。

一応、次にTrimをかけた場合、同様に以下。

こちらも有意な変化はない(Trimが効果を発揮していれば変化はなくて問題ないわけだが)。

3. まとめ


1TiB近くのライトをもってして速度低下を起こせなかったのは誤算だったが、CrystalDiskMarkを連続実行しても再度Trimが必要になるような速度低下は起こらないことが確認できたので(X25-M限定でだが)、それはそれでよしとする。

しかし……、この結果を見るに、実使用でもTrimを頻繁にかける必要はない気がしてきた。1TiBなんて普通に使う限り数箇月たっても到達しない量だと思うし。

2012/10/27

CrystalDiskMarkと雫ちゃんと

Windows 8発売の26日(というか25日深夜)にCrystalDiskMarkにも3.0.2で
Shizuku Editionが登場したので、早速、当然にDiskMarkStreamで対応。

通常版との違い、サイズなどは自動で判別する。

しかし、この麗しい姿といい、青白基調の流麗なデザインといい、これを目にすると、殺伐としたベンチマークの世界(ついあれもこれもとデータを取ろうとして、環境を整えて計測しているうちに消耗戦になってきて、あまり美しい状況にはなり難い)に涼風が吹き込まれる、というより、まとめて毒気を飛ばされてしまう危険性がある……。

その意味では機械的なマクロツールであるDiskMarkStreamはそぐわない気もするが、多少矛盾をはらんでいた方が面白い、ということにしておこう。

なお、通常版のテーマのShizukuでもエッセンスは共通。

ちなみに、Windows 8ではSSDのドライブに対してコマンドプロンプトから簡単にTrimを発行できるので(Defragの/Lオプション)、DiskMarkStreamのバッチ実行機能を使えばテスト間にTrimをすることができる。


このためのバッチファイルの内容は以下のようなもの。対象のドライブはDで、結果は後から確認するためにresult.txtというファイルに出力している。
C:\Windows\System32\Defrag.exe D: /L >> C:\Work\result.txt

ただし、Windows 8が64bit版の場合、このパスではDefrag.exeがDiskMarkStreamから見えない問題がある。これは32bitのソフト(DiskMarkStreamは32bit)を64bitのWindows上で実行するときに起こり得る問題で、この場合は以下のようになる。
C:\Windows\Sysnative\Defrag.exe D: /L >> C:\Work\result.txt


どちらの場合もDefragは管理者として実行する必要があるので、DiskMarkStreamも起動時に管理者として実行する必要がある(DiskMarkStream自体は管理者として実行される必要はないが、バッチ実行を管理者として行うトリガーとし、かつバッチ実行の度にUACの承認を求められるのを避けるため)。

[追記]

Windows 8におけるTrimについて、Microsoftの担当者(Kiran Bangalore)が答えていた(Defragging SSDs a default?)。Windows 8では(Windows 7と同様に)ファイルの削除、移動の際にTrimを発行しているが、SSDの方がリアルタイムに処理できない場合を考えて、「ディスクの最適化」で定期的にTrimを発行するようになっている(これがデフォルト)とのこと。