Skip to content

Latest commit

 

History

History
475 lines (345 loc) · 30.7 KB

lab05-files.md

File metadata and controls

475 lines (345 loc) · 30.7 KB
title lang
СПО. ЛР № 5. Работа с файловой системой
ru

Цель работы

  1. Знать назначения, принципы, преимущества ключевых средств операционной системы для работы с файловой системой.
  2. Уметь применять средства Windows API для работы с файловой системой.

Введение

Работа с файловой системой в Windows API

Для файлового ввода-вывода (в/в) в Windows применяется несколько основных функций:

  • CreateFile() для создания или открытия файлов. При ее вызове задается режим работы с файлом (чтение и/или запись), возможность совместного доступа к файлу, настройки дальнейших операций ввода-вывода.

  • ReadFile() для чтения данных и WriteFile() для записи данных. Им передается дескриптор файла, полученный из CreateFile(), буфер с данными, размер буфера и некоторые другие параметры.

  • CloseHandle() используется для закрытия файла.

Поскольку каталоги являются особым видом файлов, функция CreateFile() применяется и для открытия каталогов, например, для получения их свойств.

Синхронный и асинхронный ввод-вывод

Файловый ввод-вывод может работать в синхронном режиме, когда каждая операция приводит к блокировке процесса до своего завершения, и в асинхронном режиме, когда некоторые вызовы функций в/в лишь инициируют (начинают) операции в/в, а процесс возвращается к работе, пока операции происходят в фоне.

Синхронный в/в использовался в предыдущих лабораторных работах.

Асинхронный в/в полезен:

  • Для выполнения нескольких операций в/в параллельно. Программа сообщает ОС сразу обо всех операциях, которые нужно выполнить, а ОС совершает их в оптимальном порядке. Например, запись на разные устройства может идти параллельно, а образения к одному устройству сгруппированы.

  • Если программа может выполнить полезную работу во время в/в. Например, если программа выполняет серию вычислительных задач, по завершении одной задачи можно асинхронно записывать результаты на диск и паралльльно выполнять следующую задачу.

  • Для программ с графическим интерфейсом (вариант предыдущего случая). Даже при сохранении большого файла, если делать это асинхронно, можно позволить пользователю работать с интерфейсом и видеть прогресс операции.

Для программирования асинхронного в/в Windows предоставляет несколько вариантов. Рассмотрим простейший из них, который подходит, когда параллельно с одной операцией в/в происходят вычисления или работа GUI.

  1. Файл или каталог открывается в асинхронном режиме, то есть с возможностью выполнения асинхронных операций, но с сохранением возможности синхронных.

  2. Операция в/в запускается как асинхронная. При этом используется специальная переменная типа OVERLAPPED, которая становится связана с запушенной асинхронной операцией. Вызов функции в/в завершается мгновенно, при этом операция не завершается, а идет в фоне.

  3. После начала асинхронной операции можно узнавать ее состояние (завершилась или нет, прогресс) или отменить её специальными функциями, которые оперируют OVERLAPPED-переменной.

  4. Имеется возможность дождаться окончания асинхронной операции, в том числе должаться окончания одной любой из нескольких таких операций. Это делается теми же примитивами, что и синхронизация потоков, или специальными функциями.

Асинхронный в/в принципиально лучше запуска в/в в нескольких потоках:

  1. Не тратятся ЦП и память на дополнительные потоки и управление ими.
  2. Не требуется синхронизация потоков в случае параллельного в/в.
  3. Во время операции можно узнавать ее состояние и прогресс; отменять ее.

На практике операционные системы применяют различные оптимизации для работы с диском: кэширование (сохранение в памяти недавно прочитанных данных), буферизацию (запись данных сначала в память с последующим сбросом на диск), упреждающее чтение (read-ahead). По этой причине зачастую асинхронный в/в имеет смысл применять только если нужны его особые свойства, а с оптимизацией зачастую справляется ОС.

Отслеживание изменений файловой системы

Иногда необходимо совершать действия при изменениях в файловой системе. Например, если файл был открыт в редакторе, а его изменила внешняя программа (или другой пользователь, если файл на сетевом диске), целесообразно оповестить пользователя редактора об этом как можно скорее — сразу после изменения. Другой пример, когда изменения нужно отслеживать сразу для многих файлов — автоматическая сборка проекта при изменении любого его файла.

Перебирать все интересующие файлы и проверять наличие изменений неэффективно и некорректно:

  1. Расходуется время ЦП и память на перебор и сравнение файлов.
  2. Создается постоянная нагрузка на диск и файловую систему.
  3. Есть риск, что изменения между опросами окажутся незамеченными.

Напротив, реализация отслеживания на уровне ОС эффективна и точна:

  1. При отсутствии изменений не делается никакой работы, процесс может находиться в состоянии ожидания.
  2. Когда происходит изменение, управление уже находится у ядра ОС, поэтому оповещение процесса делается без лишних системных вызовов.
  3. Изменения не совершаются в обход ОС и не могут быть ею пропущены.

Принцип отслеживания изменений схож в Windows и в POSIX-совместимых ОС: специальным системным вызовом процесс ожидает изменений (с возможностью фильтрации интересующих), а по пробуждении получает набор оповещений о произошедших событиях.

Операции над файловой системой. Атрибуты файлов и каталогов

Операционные системы предоставляют операции для манипуляций над файловой системой (ФС): копирования, перемещения, удаления файлов; для перебора файлов и каталогов; для получения свойств объектов ФС. Состав и структура этих API сильно отличается в разных ОС; также не для всех ФС поддерживаются все операции. Например:

  • Windows предоставляет функции для работы с томами (логическими дисками), а в *nix операции над ними делаются как над обычными файлами;
  • в каждой из типовых файловых систем Windows и Linux, соответственно NTFS и ext4, есть типы файлов, которые не поддерживает другая, например, жесткие ссылки на каталоги есть толкьо в NTFS, а файлы-сокеты — в ext4;
  • для файловой системы компакт-диска не работают операции записи, она доступна только для чтения.

У файлов есть не только имя и размер, но и атрибуты, набор и смысл которых также зависят от ФС. Например, в Windows файлы могут иметь атрибут, что они скрыты или являются системными; при этом другие ОС эти атрибуты обычно игнорируют. С другой стороны, каталог — это особый тип файла, и признак того, что данный объект ФС является каталогом, хранится в атрибуте, который обязаны учитывать все ОС, чтобы правильно отображать структуру каталогов.

Часто файловые системы хранят времена последнего доступа к файлу: его создания, изменения (для каталогов - создания файлов в них), чтения.

Крайне важными свойствами файла являются его атрибуты безопасности: информация о пользователе-владельце файла и о правах доступа к файлу. Состав, формат и интерпретация этих данных радикально отличаются между ОС. В связи с большим объемом, этот раздел выходит за рамки данной ЛР.

Задание

Вариант 1. Асинхронный ввод-вывод

Задание

Исследовать различия синхронного и асинхронного ввода-вывода на примере обработки большого файла по частям.

Обработка заключается в суммировании чисел из файла:

void process(const uint32_t* numbers, size_t count, uint32_t& result) {
    for (size_t i = 0; i < count; i++) {
        result += numbers[i];
    }
}

Для обработки всего файла нужно инициализировать переменную-сумму в 0, затем для каждой части файла вызвать process(), передавая переменную-сумму в качестве ссылки на результат.

В программе требуется:

  1. Создать массив из 100 млн. случайных целых чисел и записать их в двоичный файл numbers.dat (использовать Windows API необязательно).

  2. Последовательно считывая из numbers.dat блоки по 10 млн. чисел, обрабатывать каждый из них. Работать с файлом необходимо через Windows API в синхронном режиме. По окончании подсчета нужно вывести итоговую сумму и время выполнения пункта.

  3. Повторить пункт 2, но используя асинхронный ввод из файла: пока читается очередной блок из 10 млн. чисел, нужно обрабатывать предыдущий.

Необходимо добиться совпадения сумм, подсчитанных в пункте 2 и 3.

Добавить в process() задержку функцией Sleep(). Изменяя длительность задержки (функцией GetTickCount(), сравнить соотношения времен синхронного и асинхронного решения. Объяснить характер полученной зависимости.

Указания

Отключение буферизации ввода-вывода

Файл необходимо открывать для чтения с флагом FILE_FLAG_NO_BUFFERING как в синхронном, так и в асинхронном режиме, чтобы подсистема кэширования Windows не нивелировала разницу между режимами ввода-вывода. При этом необходимо, чтобы блок памяти, в который считываются данные, располагался в памяти по адресу, кратному размеру страницы. Выделить такой блок можно VirtualAlloc():

const auto block_size = 10'000'000;
const auto block_bytes = sizeof(uint32_t) * block_size;
const auto block = reinterpret_cast<uint32_t*>(
        ::VirtualAlloc(NULL, block_bytes, MEM_COMMIT, PAGE_READWRITE));

По окончании использования эту память нужно освобождать VirtualFree():

::VirtualFree(block, block_bytes, MEM_RELEASE);

Чтение в асинхронном режиме

Для асинхронных операций в функции ввода-вывода передается структура OVERLAPPED. Перед новой операцией нужно обнулить ее вызовом ZeroMemory(), а также выставить некоторые поля:

  • Поле Offset (и OffsetHigh для файлов больше 4 ГБ) задает смещение в файле, с которого будет производиться чтение (или запись). Размер области при этом задается параметром функции ввода-вывода.

  • Если будет делаться ожидание завершения операции, необходимо указать событие Windows API, которое будет использоваться для этого (hEvent). Событие создается функцией CreateEvent().

Ожидание окончания операции в/в делается функцией GetOverlappedResult() с параметром bWait равным TRUE.

В синхронном режиме под очередной блок отводится буфер, в который числа читаются, а затем обрабатываются. Чтобы читать и обрабатывать данные одновременно, одного буфера недостаточно, потому что нельзя обрабатывать числа из того же буфера, в который ОС пишет новые данные. Необходимо два буфера: в один из них считываются новые данные (назовем его входным буфером), а данные из другого буфера обрабатываются (назовем его рабочим буфером). Когда чтение очередного блока во входной буфер завершается, буферы меняются ролями: только что считанные данные становятся рабочим буфером, а уже обработанные данные становятся входным буфером, который будет перезаписан.

Схема реализации

Не приводится обработка ошибок и освобождение ресурсов.

// Создать событие для ожидания завершения асинхронной операции:
auto event = ::CreateEvent(NULL, TRUE, FALSE, NULL);

// Открыть файл в асинхронном режиме:
auto file = ::CreateFile(...);

auto input_buffer = ...;  // выделить память под входной буфер
auto work_buffer = ...;   // выделить память под рабочий буфер

uint32_t result = 0;

OVERLAPPED async;
for (int i = 0; i < 10; i++) {
    // Подготовить OVERLAPPED к новой операции и указать область файла,
    // которую необходимо считать.
    ::ZeroMemory(&async, sizeof(async));
    async.hEvent = event;
    async.Offset = i * block_bytes;

    // Начать чтение во входной буфер:
    ::ReadFile(file, ..., input_buffer, ..., &async);

    if (i != 0) {
        // Обработать рабочий буфер, чтение которого завершилось
        // на предыдущей итерации.
        process(work_buffer, block_size, result);
    }

    // Дождаться завершения чтения входного буфера.
    DWORD bytes_read;
    ::GetOverlappedResult(file, &async, &bytes_read, TRUE);

    // Поменять буферы ролями.
    std::swap(work_buffer, input_buffer);
}

process(work_buffer, block_size, result);  // обработать последний блок

Вариант 2. Отслеживание изменений файловой системы

Задание

Написать программу, отслеживающую любые изменения в заданном каталоге и печатающую сообщения о них вместе с временем обнаружения. Переименования необходимо выводить как одно событие с указанием старого и нового имени.

Для тестирования написать вторую программу, которая делает следующее:

  1. Открывает файл для записи (с очисткой содержимого).
  2. Записывает в файл один байт.
  3. Принудительно выполняет сброс кэша на диск для этого файла.
  4. Дописывает в файл строку длины 10 вместе с символом переноса строки.
  5. Дописывает в файл 500 нулевых байтов.
  6. Дописывает в файл 10 КБ нулевых байтов.
  7. Дописывает в файл один байт.
  8. Закрывает файл.
  9. Переименовывает файл функцией MoveFile().

Перед выполнением каждого пункта тестирующая программа должна выводить текущее время, а между пунктами делать задержку в одну секунду (например, функцией Sleep()).

Запустить следящую программу с каталогом, в котором находится файл тестирующей программы. Выполнить тестирующую программу. Зафиксировать, сравнить и объяснить вывод следящей и тестирующей программы.

Указания

Отслеживать изменения в каталоге следует функцией ReadDirectoryChanges(), которую нужно вызывать в бесконечном цикле — каждую итерацию будет возвращаться очередная порция событий:

std::vector<uint8_t> events(10'000);
while (true) {
    DWORD bytes_returned;
    ::ReadDirectoryChanges(
            hDirectory,
            &events[0], events.size(),
            FILE_NOTIFY_CHANGE_FILE_NAME | ...,
            &bytes_returned,
            NULL, NULL);

События записываются в байтовый буфер, а не в массив, поскольку размер каждого события отличается. Перебирать их необходимо с помощью указателей на FILE_NOTIFY_INFORMATION, пока у очередного элемента смещение до следующего не окажется равно 0:

    auto event = reinterpret_cast<const FILE_NOTIFY_INFORMATION*>(&events[0]);
    while (event->NextEntryOffset != 0) {
        switch (event->Action) {
            ...
        }
    }
}

Внимание. В примере не делается особой обработки переименований.

Текущее время можно получать функцией GetLocalTime():

SYSTEMTIME now;
::GetLocalTime(&now);
// гггг-мм-дд ЧЧ:ММ:СС.мс
printf("[%4d-%02d-%02d %02d:%02d:%02d.%03d]\n",
        now.wYear, ..., now.wMilliseconds);

Вариант 3. Работа с файловой системой

Задание

Написать программу, которая:

  1. Находит в заданном каталоге и его подкаталогах все файлы с расширением *.h, *.cpp, *.cc, *.exe и печатает для каждого найденного файла путь, размер, время последнего доступа и атрибуты.

  2. Для всех найденных файлов, кроме *.exe, создает резервные копии, добавляя к имени файла расширение .bak. Если резервная копия уже существует, её следует перезаписать.

Указания

Перебор файлов делается функциями FindFirstFile(), FindNextFile() и FindClose(), однако нужно иметь в виду:

  • Поиск по нескольким расширениям не поддерживается, а также нельзя задать, что ищутся только файлы, а не каталоги. Поэтому необходимо задать маску * (все файлы и подкаталоги) и для каждого найденного элемента проверять, чем он является, и какое у него имя.

  • Для поиска в подкаталогах необходимо запускать новый процесс поиска, то есть действовать рекурсивно.

Алгоритм для каталога C:\mydir:

  1. Начать поиск функцией FindFirstFile() по маске C:\mydir\*.
  2. Пока удается получить очередной элемент:
    1. Получить атрибуты элемента функцией GetFileAttributes().
    2. Получить времена доступа к файлу функцией GetFileTime().
    3. Напечатать имя элемента и его атрибуты.
    4. Проверить тип элемента — файл это или директория.
      • Для директории применить к ней тот же алгоритм.
      • Для файла — проверить его расширение, создать резервную копию функцией CopyFile().

Для печати времени доступа к файлу может быть полезной функция FileTimeToSystemTime().

Контрольные вопросы

#. В чем отличие синхронного и асинхронного в/в? #. В чем преимущества и недостатки асинхронного в/в? #. Каковы средства и порядок асинхронной работы с файлами в Windows API? #. Какие оптимизации применяет ОС для дискового в/в и в каких ситуациях они актуальны (чтение или запись, последовательная или произвольная)? #. Каково применение функциональности по отслеживанию изменений ФС? #. Каковы средства и порядок работы по отслеживанию изменений ФС в Windows API? #. Что произойдет, если программа не вычитывает сведения об изменениях ФС достаточно быстро (ответ обосновать документацией)? #. Для стандартной библиотеки языка, на котором выполнена ЛР, приведите три примера функций для работы с ФС и три примера, когда через Windows API для аналогичных действий можно получить большую функциональность. #. Назовите не менее четырех атрибутов файлов. Как они учитываются Windows? #. Изменяются ли времена доступа к файлу (создания, изменения, чтения) при его копировании и перемещении?