title | lang |
---|---|
СПО. ЛР № 5. Работа с файловой системой |
ru |
- Знать назначения, принципы, преимущества ключевых средств операционной системы для работы с файловой системой.
- Уметь применять средства Windows API для работы с файловой системой.
Для файлового ввода-вывода (в/в) в Windows применяется несколько основных функций:
-
CreateFile()
для создания или открытия файлов. При ее вызове задается режим работы с файлом (чтение и/или запись), возможность совместного доступа к файлу, настройки дальнейших операций ввода-вывода. -
ReadFile()
для чтения данных иWriteFile()
для записи данных. Им передается дескриптор файла, полученный изCreateFile()
, буфер с данными, размер буфера и некоторые другие параметры. -
CloseHandle()
используется для закрытия файла.
Поскольку каталоги являются особым видом файлов, функция CreateFile()
применяется и для открытия каталогов, например, для получения их свойств.
Файловый ввод-вывод может работать в синхронном режиме, когда каждая операция приводит к блокировке процесса до своего завершения, и в асинхронном режиме, когда некоторые вызовы функций в/в лишь инициируют (начинают) операции в/в, а процесс возвращается к работе, пока операции происходят в фоне.
Синхронный в/в использовался в предыдущих лабораторных работах.
Асинхронный в/в полезен:
-
Для выполнения нескольких операций в/в параллельно. Программа сообщает ОС сразу обо всех операциях, которые нужно выполнить, а ОС совершает их в оптимальном порядке. Например, запись на разные устройства может идти параллельно, а образения к одному устройству сгруппированы.
-
Если программа может выполнить полезную работу во время в/в. Например, если программа выполняет серию вычислительных задач, по завершении одной задачи можно асинхронно записывать результаты на диск и паралльльно выполнять следующую задачу.
-
Для программ с графическим интерфейсом (вариант предыдущего случая). Даже при сохранении большого файла, если делать это асинхронно, можно позволить пользователю работать с интерфейсом и видеть прогресс операции.
Для программирования асинхронного в/в Windows предоставляет несколько вариантов. Рассмотрим простейший из них, который подходит, когда параллельно с одной операцией в/в происходят вычисления или работа GUI.
-
Файл или каталог открывается в асинхронном режиме, то есть с возможностью выполнения асинхронных операций, но с сохранением возможности синхронных.
-
Операция в/в запускается как асинхронная. При этом используется специальная переменная типа
OVERLAPPED
, которая становится связана с запушенной асинхронной операцией. Вызов функции в/в завершается мгновенно, при этом операция не завершается, а идет в фоне. -
После начала асинхронной операции можно узнавать ее состояние (завершилась или нет, прогресс) или отменить её специальными функциями, которые оперируют
OVERLAPPED
-переменной. -
Имеется возможность дождаться окончания асинхронной операции, в том числе должаться окончания одной любой из нескольких таких операций. Это делается теми же примитивами, что и синхронизация потоков, или специальными функциями.
Асинхронный в/в принципиально лучше запуска в/в в нескольких потоках:
- Не тратятся ЦП и память на дополнительные потоки и управление ими.
- Не требуется синхронизация потоков в случае параллельного в/в.
- Во время операции можно узнавать ее состояние и прогресс; отменять ее.
На практике операционные системы применяют различные оптимизации для работы с диском: кэширование (сохранение в памяти недавно прочитанных данных), буферизацию (запись данных сначала в память с последующим сбросом на диск), упреждающее чтение (read-ahead). По этой причине зачастую асинхронный в/в имеет смысл применять только если нужны его особые свойства, а с оптимизацией зачастую справляется ОС.
Иногда необходимо совершать действия при изменениях в файловой системе. Например, если файл был открыт в редакторе, а его изменила внешняя программа (или другой пользователь, если файл на сетевом диске), целесообразно оповестить пользователя редактора об этом как можно скорее — сразу после изменения. Другой пример, когда изменения нужно отслеживать сразу для многих файлов — автоматическая сборка проекта при изменении любого его файла.
Перебирать все интересующие файлы и проверять наличие изменений неэффективно и некорректно:
- Расходуется время ЦП и память на перебор и сравнение файлов.
- Создается постоянная нагрузка на диск и файловую систему.
- Есть риск, что изменения между опросами окажутся незамеченными.
Напротив, реализация отслеживания на уровне ОС эффективна и точна:
- При отсутствии изменений не делается никакой работы, процесс может находиться в состоянии ожидания.
- Когда происходит изменение, управление уже находится у ядра ОС, поэтому оповещение процесса делается без лишних системных вызовов.
- Изменения не совершаются в обход ОС и не могут быть ею пропущены.
Принцип отслеживания изменений схож в Windows и в POSIX-совместимых ОС: специальным системным вызовом процесс ожидает изменений (с возможностью фильтрации интересующих), а по пробуждении получает набор оповещений о произошедших событиях.
Операционные системы предоставляют операции для манипуляций над файловой системой (ФС): копирования, перемещения, удаления файлов; для перебора файлов и каталогов; для получения свойств объектов ФС. Состав и структура этих API сильно отличается в разных ОС; также не для всех ФС поддерживаются все операции. Например:
- Windows предоставляет функции для работы с томами (логическими дисками), а в *nix операции над ними делаются как над обычными файлами;
- в каждой из типовых файловых систем Windows и Linux, соответственно NTFS и ext4, есть типы файлов, которые не поддерживает другая, например, жесткие ссылки на каталоги есть толкьо в NTFS, а файлы-сокеты — в ext4;
- для файловой системы компакт-диска не работают операции записи, она доступна только для чтения.
У файлов есть не только имя и размер, но и атрибуты, набор и смысл которых также зависят от ФС. Например, в Windows файлы могут иметь атрибут, что они скрыты или являются системными; при этом другие ОС эти атрибуты обычно игнорируют. С другой стороны, каталог — это особый тип файла, и признак того, что данный объект ФС является каталогом, хранится в атрибуте, который обязаны учитывать все ОС, чтобы правильно отображать структуру каталогов.
Часто файловые системы хранят времена последнего доступа к файлу: его создания, изменения (для каталогов - создания файлов в них), чтения.
Крайне важными свойствами файла являются его атрибуты безопасности: информация о пользователе-владельце файла и о правах доступа к файлу. Состав, формат и интерпретация этих данных радикально отличаются между ОС. В связи с большим объемом, этот раздел выходит за рамки данной ЛР.
Исследовать различия синхронного и асинхронного ввода-вывода на примере обработки большого файла по частям.
Обработка заключается в суммировании чисел из файла:
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()
, передавая переменную-сумму
в качестве ссылки на результат.
В программе требуется:
-
Создать массив из 100 млн. случайных целых чисел и записать их в двоичный файл
numbers.dat
(использовать Windows API необязательно). -
Последовательно считывая из
numbers.dat
блоки по 10 млн. чисел, обрабатывать каждый из них. Работать с файлом необходимо через Windows API в синхронном режиме. По окончании подсчета нужно вывести итоговую сумму и время выполнения пункта. -
Повторить пункт 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); // обработать последний блок
Написать программу, отслеживающую любые изменения в заданном каталоге и печатающую сообщения о них вместе с временем обнаружения. Переименования необходимо выводить как одно событие с указанием старого и нового имени.
Для тестирования написать вторую программу, которая делает следующее:
- Открывает файл для записи (с очисткой содержимого).
- Записывает в файл один байт.
- Принудительно выполняет сброс кэша на диск для этого файла.
- Дописывает в файл строку длины 10 вместе с символом переноса строки.
- Дописывает в файл 500 нулевых байтов.
- Дописывает в файл 10 КБ нулевых байтов.
- Дописывает в файл один байт.
- Закрывает файл.
- Переименовывает файл функцией
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);
Написать программу, которая:
-
Находит в заданном каталоге и его подкаталогах все файлы с расширением
*.h
,*.cpp
,*.cc
,*.exe
и печатает для каждого найденного файла путь, размер, время последнего доступа и атрибуты. -
Для всех найденных файлов, кроме
*.exe
, создает резервные копии, добавляя к имени файла расширение.bak
. Если резервная копия уже существует, её следует перезаписать.
Перебор файлов делается функциями FindFirstFile()
,
FindNextFile()
и FindClose()
, однако нужно иметь в виду:
-
Поиск по нескольким расширениям не поддерживается, а также нельзя задать, что ищутся только файлы, а не каталоги. Поэтому необходимо задать маску
*
(все файлы и подкаталоги) и для каждого найденного элемента проверять, чем он является, и какое у него имя. -
Для поиска в подкаталогах необходимо запускать новый процесс поиска, то есть действовать рекурсивно.
Алгоритм для каталога C:\mydir
:
- Начать поиск функцией
FindFirstFile()
по маскеC:\mydir\*
. - Пока удается получить очередной элемент:
- Получить атрибуты элемента функцией
GetFileAttributes()
. - Получить времена доступа к файлу функцией
GetFileTime()
. - Напечатать имя элемента и его атрибуты.
- Проверить тип элемента — файл это или директория.
- Для директории применить к ней тот же алгоритм.
- Для файла — проверить его расширение, создать резервную копию
функцией
CopyFile()
.
- Получить атрибуты элемента функцией
Для печати времени доступа к файлу может быть полезной функция
FileTimeToSystemTime()
.
#. В чем отличие синхронного и асинхронного в/в? #. В чем преимущества и недостатки асинхронного в/в? #. Каковы средства и порядок асинхронной работы с файлами в Windows API? #. Какие оптимизации применяет ОС для дискового в/в и в каких ситуациях они актуальны (чтение или запись, последовательная или произвольная)? #. Каково применение функциональности по отслеживанию изменений ФС? #. Каковы средства и порядок работы по отслеживанию изменений ФС в Windows API? #. Что произойдет, если программа не вычитывает сведения об изменениях ФС достаточно быстро (ответ обосновать документацией)? #. Для стандартной библиотеки языка, на котором выполнена ЛР, приведите три примера функций для работы с ФС и три примера, когда через Windows API для аналогичных действий можно получить большую функциональность. #. Назовите не менее четырех атрибутов файлов. Как они учитываются Windows? #. Изменяются ли времена доступа к файлу (создания, изменения, чтения) при его копировании и перемещении?