ファイルを高速に列挙するために Windows マスター ファイル テーブル (MFT) を読み込もうとしています。これまで、これを行うための 2 つのアプローチを見てきました。
- DeviceIoControlを使用してJeffrey CoopersteinとJeffrey Richterが提案したように
- 一部のオープンソース ツールおよびNTFS パーサーライブラリで提示されている MFT の直接解析
私のプロジェクトでは、アプローチ [ 1 ] に焦点を当てています。私が直面している問題は、主に実行時間に関連しています。明確にするために、以下は私のシステムと開発環境です。
- IDE - ビジュアルスタジオ 2013
- 言語 - C++
- OS - Windows 7 プロフェッショナル x64
- C++ および .NET コード用に 32 ビットのバイナリが生成されます。
問題
[ 1 ]で言及されているバージョン(わずかに変更されています) と、codeplex で入手可能なVB.NET 実装を比較しました。問題は、内部ループのステートメントのコメントを外すと、C++ コードの実行時間が 7 ~ 8 倍に増加することです。C++ コード (VB コードで利用可能) でパス マッチングを実装していません。
Q1. C++ コードのパフォーマンスを改善する方法を教えてください。
私のマシンで C:\ ドライブを列挙するタイミング:
- C++ (内側のループでコメント化されていないステートメントを使用) -
21 seconds
- VB.NET (追加のパス マッチング コードを使用) -
3.5 seconds
より明確にするために、C++ と VB.NET のスニペットを次に示します。
C++
bool FindAll()
{
if (m_hDrive == NULL) // Handle of, for example, "\\.\C:"
return false;
USN_JOURNAL_DATA ujd = {0};
DWORD cb = 0;
BOOL bRet = FALSE;
MFT_ENUM_DATA med = {0};
BYTE pData[sizeof(DWORDLONG) + 0x10000] = {0};
bRet = DeviceIoControl(m_hDrive, FSCTL_QUERY_USN_JOURNAL, NULL, 0, &ujd, sizeof(USN_JOURNAL_DATA), &cb, NULL);
if (bRet == FALSE) return false;
med.StartFileReferenceNumber = 0;
med.LowUsn = 0;
med.HighUsn = ujd.NextUsn;
//Outer Loop
while (TRUE)
{
bRet = DeviceIoControl(m_hDrive, FSCTL_ENUM_USN_DATA, &med, sizeof(med), pData, sizeof(pData), &cb, NULL);
if (bRet == FALSE) {
break;
}
PUSN_RECORD pRecord = (PUSN_RECORD)&pData[sizeof(USN)];
//Inner Loop
while ((PBYTE)pRecord < (pData + cb))
{
tstring sz((LPCWSTR) ((PBYTE)pRecord + pRecord->FileNameOffset), pRecord->FileNameLength / sizeof(WCHAR));
bool isFile = ((pRecord->FileAttributes & FILE_ATTRIBUTE_DIRECTORY) != FILE_ATTRIBUTE_DIRECTORY);
if (isFile) m_dwFiles++;
//m_nodes[pRecord->FileReferenceNumber] = new CNode(pRecord->ParentFileReferenceNumber, sz, isFile);
pRecord = (PUSN_RECORD)((PBYTE)pRecord + pRecord->RecordLength);
}
med.StartFileReferenceNumber = *(DWORDLONG *)pData;
}
return true;
}
は次m_nodes
のように定義されていますtypedef std::map<DWORDLONG, CNode*> NodeMap;
VB.NET
Public Sub FindAllFiles(ByVal szDriveLetter As String, fFileFound As FileFound_Delegate, fProgress As Progress_Delegate, fMatch As IsMatch_Delegate)
Dim usnRecord As USN_RECORD
Dim mft As MFT_ENUM_DATA
Dim dwRetBytes As Integer
Dim cb As Integer
Dim dicFRNLookup As New Dictionary(Of Long, FSNode)
Dim bIsFile As Boolean
' This shouldn't be called more than once.
If m_Buffer.ToInt32 <> 0 Then
Console.WriteLine("invalid buffer")
Exit Sub
End If
' progress
If Not IsNothing(fProgress) Then fProgress.Invoke("Building file list.")
' Assign buffer size
m_BufferSize = 65536 '64KB
' Allocate a buffer to use for reading records.
m_Buffer = Marshal.AllocHGlobal(m_BufferSize)
' correct path
szDriveLetter = szDriveLetter.TrimEnd("\"c)
' Open the volume handle
m_hCJ = OpenVolume(szDriveLetter)
' Check if the volume handle is valid.
If m_hCJ = INVALID_HANDLE_VALUE Then
Console.WriteLine("Couldn't open handle to the volume.")
Cleanup()
Exit Sub
End If
mft.StartFileReferenceNumber = 0
mft.LowUsn = 0
mft.HighUsn = Long.MaxValue
Do
If DeviceIoControl(m_hCJ, FSCTL_ENUM_USN_DATA, mft, Marshal.SizeOf(mft), m_Buffer, m_BufferSize, dwRetBytes, IntPtr.Zero) Then
cb = dwRetBytes
' Pointer to the first record
Dim pUsnRecord As New IntPtr(m_Buffer.ToInt32() + 8)
While (dwRetBytes > 8)
' Copy pointer to USN_RECORD structure.
usnRecord = Marshal.PtrToStructure(pUsnRecord, usnRecord.GetType)
' The filename within the USN_RECORD.
Dim FileName As String = Marshal.PtrToStringUni(New IntPtr(pUsnRecord.ToInt32() + usnRecord.FileNameOffset), usnRecord.FileNameLength / 2)
'If Not FileName.StartsWith("$") Then
' use a delegate to determine if this file even matches our criteria
Dim bIsMatch As Boolean = True
If Not IsNothing(fMatch) Then fMatch.Invoke(FileName, usnRecord.FileAttributes, bIsMatch)
If bIsMatch Then
bIsFile = Not usnRecord.FileAttributes.HasFlag(FileAttribute.Directory)
dicFRNLookup.Add(usnRecord.FileReferenceNumber, New FSNode(usnRecord.FileReferenceNumber, usnRecord.ParentFileReferenceNumber, FileName, bIsFile))
End If
'End If
' Pointer to the next record in the buffer.
pUsnRecord = New IntPtr(pUsnRecord.ToInt32() + usnRecord.RecordLength)
dwRetBytes -= usnRecord.RecordLength
End While
' The first 8 bytes is always the start of the next USN.
mft.StartFileReferenceNumber = Marshal.ReadInt64(m_Buffer, 0)
Else
Exit Do
End If
Loop Until cb <= 8
If Not IsNothing(fProgress) Then fProgress.Invoke("Parsing file names.")
' Resolve all paths for Files
For Each oFSNode As FSNode In dicFRNLookup.Values.Where(Function(o) o.IsFile)
Dim sFullPath As String = oFSNode.FileName
Dim oParentFSNode As FSNode = oFSNode
While dicFRNLookup.TryGetValue(oParentFSNode.ParentFRN, oParentFSNode)
sFullPath = String.Concat(oParentFSNode.FileName, "\", sFullPath)
End While
sFullPath = String.Concat(szDriveLetter, "\", sFullPath)
If Not IsNothing(fFileFound) Then fFileFound.Invoke(sFullPath, 0)
Next
'// cleanup
Cleanup() '//Closes all the handles
If Not IsNothing(fProgress) Then fProgress.Invoke("Complete.")
End Sub
は次fFileFound
のように定義されます。
Sub(s, l)
If s.ToLower.StartsWith(sSearchPath) Then
lCount += 1
lstFileNames.Add(s.ToLower) '// Dim lstFileNames As List(Of String)
End If
End Sub
&の構造は次のとおりですFSNode
。CNode
//C++ version
class CNode
{
public:
//DWORDLONG m_dwFRN;
DWORDLONG m_dwParentFRN;
tstring m_sFileName;
bool m_bIsFile;
public:
CNode(DWORDLONG dwParentFRN, tstring sFileName, bool bIsFile = false) :
m_dwParentFRN(dwParentFRN), m_sFileName(sFileName), m_bIsFile(bIsFile){
}
~CNode(){
}
};
注- VB.NET コードは新しいスレッド (GUI があるため必要) を生成しますが、メイン スレッド (テスト用の単純なコンソール アプリケーション) で c++ 関数を呼び出しています。
アップデート
それは私の側からのばかげた間違いでした。DeviceIoControl
API は期待どおりに機能しています。ビルドDebug
はビルドよりも少し遅いですがRelease
。次の記事を参照してください。