Top.Mail.Ru
Мастера DELPHI, Delphi programming community Титульная страница Поиск, карта сайта Написать письмо 
| Новости | Поиск | FAQ | Статьи | Книги | Новости VCL |
| Форумы ЧАТ | Орешник | KOL и MCK |
Форумы -» Основная («Начинающим»)/ Базы / WinAPI / Компоненты / Сети / Media / Игры / Corba и COM / KOL / FreePascal / .Net / Прочее / RSDN.ORG

« предыдущая ветвь | форум | следующая ветвь »

Несколько ускоренный способ сканирования директории.


Страницы:   1 2

Riply ©   (2007-10-14 21:09:00) 

Здравствуйте !
Т.к. последнее время эта тема стала популярной на форуме, решила
попробовать посмотреть на нее с точки зрения Naitive API.
(Если честно, то  мне самой это понадобилось для сравнительного анализа
и контроля моих MFT-творений и изучения "как это делают другие" :)
Почитала, что по этому поводу говорят в интернете.
Во всех местах (которые я сумела найти) приходят к одному и тому-же
выводу: "Этим путем - не ускориться".

Давайте попробуем усомниться в этом ? :)
(Усомняться мы будем, подглядывая в исходники Игоря Шевченко :)
Сначала определим структуру в которой будем хранить данные
для каждого объекта. Она должна быть очень маленькой.
Ведь если мы сканируем диск с миллионом объектов, то увеличение ее размера
всего на один байт съест целый мегабайт памяти.

OBJ_DATA_INF0 = record
case Integer of
 0: (ChildCount: DWORD;
     FirstChildOffset: DWORD); // для директорий
 1: (SizeOfData: LONGLONG);    // для файлов и файловых потоков
 end;

_NAMED_QUERY_OBJECT = packed record
  OffsetNextEntry   : Word;  // Смещение на следующий блок
  ObjAttributes     : ULONG; // Атрибуты объекта
  ExternalData      : OBJ_DATA_INF0;
  ObjNameLength     : Word; // нилл-терминайт не считаем.
  ObjName           : array[0..0] of Byte;
 end;

(Здесь оставлен простор для уменьшения :)
Например, в ExternalData.SizeOfData предполагается хранить размер файла.
Но для него вполне хватит и 48 бит. Так что туда можно запихать ObjNameLength
и сэкономить аж целых два байта :) Так же можно уменьшить и ObjAttributes,
переопределив, какие и сколько нам надо.
"Оставляю это заинтересованным лицам" (с) Leonid Troyanovsky

И заодно пара функций для расчета размера записи и выравнивания его до SizeOf(DWord)
(Мудрые люди из Microsoft выравнивают структуру FILE_BOTH_DIR_INFORMATION
до SizeOf(Int64), ну а мы (т.к. плохо понимаем "зачем и почему") остановимся на DWord :)
function NT_NameAddedSizeB(const NameLength: integer): integer; inline;
begin
if NameLength >= SizeOf(WideChar)
 then Result := NameLength - SizeOf(Byte) else Result := -1;
end;

function QueryObj_EdgeSize(const BytesInName: integer): integer;
var
Value: integer;
begin
Value := SizeOf(_NAMED_QUERY_OBJECT) + NT_NameAddedSizeB(BytesInName);
Result := Value mod SizeOf(DWord);
if Result > 0 then Result := Value + SizeOf(DWord) - Result else inc(Result, Value);
end;


Ну а теперь можно и посканировать. Я приведу только схему кода,
оговорив параметры вызовов, т.к. кроме этого и обычной рекурсии в нем ничего нет :)
Используемые функции: NtQueryDirectoryFile, NtOpenFile и NtClose.

Нам понадобяться два блока памяти: для буффера, передаваемого в NtQueryDirectoryFile
и для списка наших NAMED_QUERY_OBJECT.

Попробуем оценить их размеры:
NtQueryDirectoryFile будем вызывать так:
ObjectAttr: POBJECT_ATTRIBUTES  - инициализируем хендлом открытого родителя
(он у нас будет, мы же в рекурсии) и только именем файла (без пути).
NtQueryDirectoryFile(..., @ObjectAttr(POBJECT_ATTRIBUTES),...
                   FileDirectoryInformation(FILE_INFORMATION_CLASS), - выбор класса важен.
                   False(ReturnSingleEntry),...);
Мы за один вызов будем пытаться получить все объекты (ReturnSingleEntry = False),
и экономить на размере буфера не будем, т.к. его нехватка потребует не только
перевыделения памяти, но и лишнего вызова NtQueryDirectoryFile.
Такого размера: FS_OBJ_QUERY_INC_SIZE = $10000 * 8 хватает, примерно на 5000 объектов,
лежащих в корне сканируемого.
Примерная работа:
function TNtQueryObjectFS._QueryDirectoryFileMem(const ObjHandle: THandle;
                                                const InfoClass: FILE_INFORMATION_CLASS;
                                                RetSngleEntry, RestartScan: LongBool): NTSTATUS;
var
IoBlock: TIoStatusBlock;
begin
Result := NtQueryDirectoryFile(ObjHandle, FhEvent, nil, nil, @IoBlock, FpObjInfo,
                               FcbObjInfo, InfoClass, RetSngleEntry, nil, RestartScan);
while True do
 case Result of
  STATUS_BUFFER_OVERFLOW, STATUS_INFO_LENGTH_MISMATCH:
   begin
    _ReallocQueryBuffer(FcbObjInfo + FcbObjInfo);
    Result := NtQueryDirectoryFile(ObjHandle, FhEvent, nil, nil, @IoBlock, FpObjInfo,
                                   FcbObjInfo, InfoClass, RetSngleEntry, nil,
                                   Result = STATUS_INFO_LENGTH_MISMATCH);

   end;
  else Exit;
 end;
end;

function TNtQueryObjectFS._QueryDirectoryFile(const aOffset: DWord; const ObjHandle: THandle;
                                             const InfoClass: FILE_INFORMATION_CLASS;
                                             RetSngleEntry: LongBool): NTSTATUS;
begin
Result := _QueryDirectoryFileMem(ObjHandle, InfoClass, RetSngleEntry, True);
while Result = STATUS_SUCCESS do
 begin
  FROOT_FIND_DATA.AddObjectsBlock(aOffset, FpObjInfo, InfoClass, FirstCall);<--Пишем наши данные
  Result := _QueryDirectoryFileMem(ObjHandle, InfoClass, RetSngleEntry, False);
 end;
end;


Riply   (2007-10-14 21:10:00) [21]

Примерно так выглядит тело рекурсии:
function TNtQueryObjectFS._QueryFileDirectoryRec(const aOffset: DWord; const NtScanRec: TNtScanRec;
                                                const DesAccess: ACCESS_MASK;
                                                const InfoClass: FILE_INFORMATION_CLASS;
                                                const RetSngle: Boolean) : NTSTATUS;
var
ObjectAttr: OBJECT_ATTRIBUTES;
IoBlock: TIoStatusBlock;
usSubObj: TUnicodeString;
_ScanRec: TNtScanRec;
Tmp, Offset, LastOffset: DWord;
begin
Tmp := NtScanRec.RootNameLen shr SHR_WCHAR;
InitializeObjectAttributes(@ObjectAttr, @NtScanRec.ObjName, OBJ_CASE_INSENSITIVE, NtScanRec.hParent, nil);
Result := NtOpenFile(@_ScanRec.hParent, DesAccess, @ObjectAttr, @IoBlock, ALX_FILE_SHARE_ALL,
                     FILE_SYNCHRONOUS_IO_NONALERT or FILE_DIRECTORY_FILE);
if FixStatus(Result) = STATUS_SUCCESS then
 try
  Us_InitNull(@_ScanRec.ObjName);
  Offset := FROOT_FIND_DATA.UsedSize;
  Result := _QueryDirectoryFile(aOffset, _ScanRec.hParent, InfoClass, RetSngle);
  if Result = STATUS_NO_MORE_FILES then
   begin
    LastOffset := FROOT_FIND_DATA.UsedSize;
    while Offset < LastOffset do
     begin
      with FROOT_FIND_DATA.Blocks(Offset)^ do
       begin
        inc(FMemCount, QueryObj_EdgeSize(Tmp + SizeOf(Char) + (ObjNameLength shr SHR_WCHAR)));
        if IsNotScannedDirectory then
         if GetNameU(@usSubObj) = STATUS_SUCCESS then
          begin
           Result := Us_Duplicate(@usSubObj, @_ScanRec.ObjName);
           if Result = STATUS_SUCCESS then
            try
             _ScanRec.RootNameLen := _ScanRec.ObjName.Length + SizeOf(WideChar) + NtScanRec.RootNameLen;
             _QueryFileDirectoryRec(Offset, _ScanRec, DesAccess, InfoClass, RetSngle);
            finally
             Us_FreeMem(@_ScanRec.ObjName);
            end;
          end;
        inc(Offset, FROOT_FIND_DATA.Blocks(Offset).OffsetNextEntry);
       end;
     end;
   end;
 finally
  Us_FreeMem(@_ScanRec.ObjName);
  Nt_Close(_ScanRec.hParent);
 end
else Err_ShowNT(Result, usSubObj.Buffer);
end;

В этой ф-ии мы записываем полученные результаты в виде:
имени(без пути)
смещения на следующий элемент
смещения на первого ребенка
атрибутов.
Заодно высчитываем точный размер будующей структуры в которую
конвертируем все это дело для удобства последующей работы с данными :)
После пояснений Игоря Шевченко и clickmaker`а о механизме выделения памяти в
Rtl ф-иях для UNICODE_STRING, я (в данной задаче) использовала "самописные"
ф-ии - аналоги Rtl ф-ий,только использующие GetMem, FreeMem.
(Мизерный выигрыш - а приятно :)

При оценке начального размера буфера для NAMED_QUERY_OBJECT, я поступаю так:
Если нас вызвали просканировать целый диск, то считываю значение MftValidDataLength из MFT.
(все равно нам открывать диск :) и поделив его на размер записи в MFT получаю
довольно точное число валидных объектов на диске.
Если же сканировать надо директорию, то начальный размер берем с потолка :)

Резюме:
Мы пытались выиграть на:
1. Уходе от постоянной ковертации больших строк из PChar в UNICODE_STRING и обратно.
2. Открытии всех объектов, используя Handle родителя.
3. Получении сразу всех "корневых" объектов за один запрос.
4. Хранении данных "в одном блоке памяти"

Сравнивалось заполнение TStrings при помощи FindFirst/NextFile, с одной стороны
и полный объем работ (подготовка, сканирование, конвертация результатов в
индексированную структуру, и последующее заполнение из нее TStrings) с другой стороны.

Тестирование показало выигрыш в скорости
на "индексированном" диске - в два раза
на не "индексированном" диске - на 20-25%.

Учитывая, что все это нуждается в доработке напильником, оптимизации и исправлении ошибок,
результат очень даже ничего :)

P.S.
Доводить это хозяйство "до ума" не буду, ибо данная методика
не идет ни в какое сравнение с работой через MFT, а любопытство свое я уже удовлетворила :)

P.P.S.
Уфф... :)


Страницы:   1 2

Наверх

Титульная страница Поиск, карта сайта Написать письмо