Files
2026-05-18 17:22:11 +07:00

464 lines
36 KiB
Markdown

# Отчет по дисциплине «Операционные системы»
## РГР «Разработка простой файловой системы»
**Выполнил:** ____________________
**Группа:** ____________________
**Проверил:** ____________________
**Новосибирск, 2026**
## Оглавление
1. [Введение](#введение)
2. [Постановка задачи](#постановка-задачи)
3. [Выбранная архитектура файловой системы](#выбранная-архитектура-файловой-системы)
4. [Структура носителя и основные данные](#структура-носителя-и-основные-данные)
5. [Реализованные операции](#реализованные-операции)
6. [Алгоритмы работы](#алгоритмы-работы)
7. [Тестирование](#тестирование)
8. [Ограничения реализации](#ограничения-реализации)
9. [Вывод](#вывод)
## Введение
Цель работы — разработать учебную файловую систему на языке C, использующую обычный бинарный файл как виртуальный носитель произвольного доступа. Программа должна не только хранить файлы и каталоги, но и показывать основные принципы настоящих файловых систем: разметку носителя, учет свободного места, хранение метаданных, работу с путями и прикладные операции обслуживания.
В качестве ориентира использовался предоставленный пример реализации, однако приоритет был отдан требованиям задания. Поэтому итоговая программа сохраняет простую учебную архитектуру примера, но дополнительно реализует операции `open`, `close`, `seek`, `read`, `write`, `rename`, импорт и экспорт, дефрагментацию и сжатие.
> **Теоретическая основа.** Файловая система — это способ организации долговременного хранения данных, при котором содержимое носителя представляется не как сплошной массив байтов, а как совокупность именованных объектов, метаданных и правил размещения. Даже в упрощенной реализации должны присутствовать три ключевых слоя: физическое хранение, служебные структуры и пользовательский интерфейс доступа к файлам.
Уже в начале программы задаются основные параметры будущей файловой системы: сигнатура формата, размер кластера, число дескрипторов и ограничения на открытый файл.
```c
#define FS_MAGIC 0x53465331u
#define CLUSTER_SIZE 1024u
#define MAX_ENTRIES 128
#define MAX_FILE_CLUSTERS 128
#define MAX_OPEN_DATA (MAX_FILE_CLUSTERS * CLUSTER_SIZE)
```
## Постановка задачи
Согласно заданию необходимо разработать систему управления файлами, где разделом является бинарный файл произвольного доступа. Реализация должна включать:
- физический уровень хранения данных;
- структуру учета свободных блоков;
- базовый уровень с индексными метаданными;
- размещение содержимого файлов;
- систему каталогов и путей;
- символьные операции над файлами и каталогами;
- прикладную утилиту для форматирования, просмотра, импорта, экспорта, дефрагментации и сжатия.
> **Теоретическая основа.** При проектировании файловой системы важно разделять данные пользователя и служебные данные самой системы. Пользователь работает с именами файлов и каталогов, однако внутри реализации требуется хранить состояние носителя, сведения о свободном пространстве, метаданные объектов и текущий контекст работы.
В программе эта постановка отражается в центральной структуре состояния файловой системы. Она объединяет физический носитель, карту свободных блоков, таблицу дескрипторов и текущий каталог пользователя.
```c
typedef struct {
FILE *disk;
char image_path[MAX_PATH];
SuperBlock sb;
unsigned char *bitmap;
Entry entries[MAX_ENTRIES];
uint32_t current_dir;
bool mounted;
} FileSystem;
```
## Выбранная архитектура файловой системы
Файловая система разделена на уровни. Такое деление полезно не только для описания в отчете: каждый уровень решает свою задачу и скрывает детали от следующего. Пользователь работает с командами и путями, а не с номерами кластеров; функции чтения работают с файлами, а не с отдельными битами карты свободного пространства.
> **Теоретическая основа.** Многоуровневая архитектура уменьшает связность системы. Физический уровень отвечает за доступ к байтам носителя, базовый — за метаданные, логический — за каталоги и пути, а прикладной — за команды пользователя. Такое разделение облегчает развитие системы: например, способ учета свободных блоков можно изменить, почти не затрагивая синтаксис команд.
| Уровень | Реализация | Назначение |
| --- | --- | --- |
| Физический уровень | Кластеры фиксированного размера по 1024 байта | Делит носитель на одинаковые адресуемые блоки; смещение кластера вычисляется как `номер * 1024` |
| Учет свободного места | Битовая карта | Показывает, какие кластеры свободны, а какие уже принадлежат служебным областям или файлам |
| Базовый уровень | Индексный файл с дескрипторами объектов | Хранит метаданные файлов и каталогов отдельно от пользовательских данных |
| Размещение файлов | Список номеров кластеров в дескрипторе | Позволяет файлу занимать несколько несмежных кластеров и читать их в правильном порядке |
| Логический уровень | Неупорядоченный список имен | Представляет каталоги через связь «объект — родительский каталог» и выполняет линейный поиск по именам |
| Символьный уровень | Пути вида `/docs/file.txt`, буферизированные операции `open/close/seek/read/write` | Преобразует удобные человеку имена и позиции в обращения к дескрипторам и кластерам |
| Прикладной уровень | Интерактивная консольная утилита | Дает команды для форматирования, просмотра, копирования, импорта, экспорта, сжатия и дефрагментации |
### Физический уровень
Носителем является обычный бинарный файл. Он рассматривается как последовательность кластеров одинакового размера. Фиксированный размер выбран ради простоты: адрес любого кластера находится прямым умножением, а алгоритм выделения не обязан искать переменные по длине участки памяти. Недостатком является внутреннее фрагментирование: даже файл в несколько байт занимает целый кластер.
> **Теория.** Кластер — минимальная единица выделения дискового пространства. Чем больше размер кластера, тем меньше служебных записей требуется для описания больших файлов, но тем выше потери на внутреннее фрагментирование при хранении маленьких файлов.
Адрес кластера вычисляется напрямую по его номеру. Это видно при записи данных:
```c
fseek(fs->disk, (long)(entry->clusters[i] * CLUSTER_SIZE), SEEK_SET);
fwrite(data + written, 1, chunk, fs->disk);
```
### Уровень свободного пространства
Для каждого кластера хранится один бит. При форматировании служебные кластеры суперблока, битовой карты и индексного файла сразу помечаются занятыми. При записи файла функция выделения просматривает карту от начала области данных и выбирает первые свободные кластеры; при удалении файла соответствующие биты сбрасываются обратно в ноль.
> **Теория.** Битовая карта — одна из классических структур учета свободного пространства. Она компактна и проста в реализации: состояние каждого блока кодируется одним битом. Недостатком является необходимость линейного просмотра при поиске свободных блоков, если не используются дополнительные индексы или кеши.
```c
static bool bit_is_set(FileSystem *fs, uint32_t cluster) {
return (fs->bitmap[cluster / 8] & (1u << (cluster % 8))) != 0;
}
static int alloc_cluster(FileSystem *fs) {
for (uint32_t i = fs->sb.data_start_cluster; i < fs->sb.total_clusters; i++) {
if (!bit_is_set(fs, i)) {
bit_set(fs, i);
return (int)i;
}
}
return -1;
}
```
### Базовый уровень
Индексный файл — это таблица дескрипторов `Entry`. Один дескриптор описывает один файл или каталог: имя, тип, родителя, размеры и список кластеров. Корневой каталог хранится в нулевой записи. Благодаря индексному файлу метаданные можно прочитать сразу после монтирования носителя, не просматривая всю область данных.
> **Теория.** Метаданные отделяются от содержимого файлов, чтобы система могла быстро находить объект, определять его тип, размер и расположение без чтения пользовательских данных. В реальных файловых системах аналогичную роль играют, например, inode-структуры.
```c
typedef struct {
char name[MAX_NAME];
uint8_t type;
uint8_t compressed;
uint16_t reserved;
uint32_t parent;
uint32_t size;
uint32_t stored_size;
uint32_t cluster_count;
uint32_t clusters[MAX_FILE_CLUSTERS];
} Entry;
```
### Уровень размещения файлов
Каждый файл хранит массив номеров своих кластеров. Поэтому файл может лежать не непрерывно, а частями в разных местах виртуального диска. При чтении система идет по этому массиву и собирает содержимое в правильной последовательности. Дефрагментация перестраивает эти списки так, чтобы занятые кластеры снова располагались компактно.
> **Теория.** Несмежное размещение повышает гибкость выделения места: файл можно расширять даже при отсутствии одного большого непрерывного участка. Однако чтение таких файлов требует хранения списка блоков и может приводить к фрагментации.
```c
for (i = 0; i < entry->cluster_count; i++) {
uint32_t chunk = entry->stored_size - read_count;
if (chunk > CLUSTER_SIZE) chunk = CLUSTER_SIZE;
fseek(fs->disk, (long)(entry->clusters[i] * CLUSTER_SIZE), SEEK_SET);
fread(data + read_count, 1, chunk, fs->disk);
read_count += chunk;
}
```
### Логический и символьный уровни
Каталог реализован без отдельного файла содержимого: у каждого объекта есть поле `parent`, содержащее индекс родительского каталога. Путь разбивается по символу `/`, после чего система шаг за шагом ищет дочерний объект в текущем каталоге. Символьный уровень дает привычные операции над открытым файлом: `open`, `close`, `seek`, `read`, `write`; внутри программы они работают через буфер и скрывают детали физического размещения.
> **Теория.** Логический уровень преобразует человекоориентированные имена в внутренние идентификаторы объектов. Символьный уровень дополняет это абстракцией «открытого файла», где пользователь работает с текущей позицией чтения и записи, а не с физическими блоками.
```c
static int find_child(FileSystem *fs, uint32_t parent, const char *name) {
for (int i = 0; i < MAX_ENTRIES; i++) {
if (fs->entries[i].type != TYPE_FREE &&
fs->entries[i].parent == parent &&
strcmp(fs->entries[i].name, name) == 0) return i;
}
return -1;
}
```
```mermaid
flowchart LR
A[Команда пользователя] --> B[Путь /docs/a.txt]
B --> C[Поиск дескриптора]
C --> D[Список кластеров]
D --> E[Битовая карта]
D --> F[Байты в области данных]
```
## Структура носителя и основные данные
В начале виртуального диска находится суперблок. Он хранит сигнатуру файловой системы, размер диска, размер кластера, смещения служебных областей и индекс корневого каталога.
> **Теоретическая основа.** Суперблок — это точка входа в структуру файловой системы. После подключения носителя именно по нему программа понимает, корректный ли перед ней формат и где искать остальные области. Потеря суперблока делает интерпретацию содержимого носителя затруднительной даже при сохранности пользовательских данных.
```c
typedef struct {
uint32_t magic;
uint32_t disk_size;
uint32_t cluster_size;
uint32_t total_clusters;
uint32_t bitmap_offset;
uint32_t bitmap_size;
uint32_t index_offset;
uint32_t index_clusters;
uint32_t data_start_cluster;
uint32_t root_index;
} SuperBlock;
```
После суперблока располагается битовая карта. Каждый ее бит соответствует одному кластеру: `0` означает свободный кластер, `1` — занятый. Далее находится индексный файл, представленный массивом дескрипторов `Entry`.
```c
typedef struct {
char name[32];
uint8_t type;
uint8_t compressed;
uint32_t parent;
uint32_t size;
uint32_t stored_size;
uint32_t cluster_count;
uint32_t clusters[128];
} Entry;
```
Поле `size` хранит логический размер файла, видимый пользователю, а `stored_size` — фактическое количество байт на носителе. Это различие необходимо для поддержки сжатых файлов.
При форматировании эти области рассчитываются последовательно: после суперблока располагается битовая карта, затем индексный файл, после чего начинается пользовательская область данных.
```c
fs->sb.bitmap_offset = CLUSTER_SIZE;
fs->sb.bitmap_size = (fs->sb.total_clusters + 7) / 8;
bitmap_clusters = bytes_to_clusters(fs->sb.bitmap_size);
fs->sb.index_offset = (1 + bitmap_clusters) * CLUSTER_SIZE;
fs->sb.index_clusters = bytes_to_clusters((uint32_t)(sizeof(Entry) * MAX_ENTRIES));
fs->sb.data_start_cluster = 1 + bitmap_clusters + fs->sb.index_clusters;
```
```mermaid
flowchart TB
S[Суперблок] --> B[Битовая карта]
B --> I[Индексный файл]
I --> D[Область данных]
I --> R[Корневой каталог /]
R --> F[Дескрипторы файлов и каталогов]
F --> D
```
## Реализованные операции
Программа собирается из файла `simplefs.c` и запускается с именем файла-носителя:
```bash
gcc -std=c11 -Wall -Wextra -pedantic simplefs.c -o simplefs
./simplefs disk.img
```
Интерактивная оболочка поддерживает историю команд: в обычном терминале стрелки **Up** и **Down** перемещаются по ранее введенным строкам. Основные команды названы в стиле Unix, чтобы работа с учебной файловой системой напоминала привычную командную строку.
> **Теоретическая основа.** Набор операций файловой системы обычно разделяется на операции над объектами (`create`, `remove`, `rename`), операции навигации (`cd`, `ls`, `pwd`), операции передачи данных (`read`, `write`, `import`, `export`) и сервисные операции обслуживания носителя (`format`, `defrag`, `compress`).
### Справочник команд
| Команда | Синтаксис | Назначение |
| --- | --- | --- |
| Форматирование | `format <KiB>` | создает новый файл-носитель указанного размера |
| Информация | `info` | показывает размер диска, размер кластера и число свободных кластеров |
| Список | `ls [path]` | выводит содержимое текущего или указанного каталога |
| Текущий каталог | `pwd` | печатает текущий абсолютный путь |
| Переход | `cd <path>` | меняет текущий каталог |
| Создание каталога | `mkdir <path>` | создает новый каталог |
| Удаление каталога | `rmdir <path>` | удаляет только пустой каталог |
| Создание файла | `touch <path>` | создает пустой файл |
| Удаление файла | `rm <path>` | удаляет файл и освобождает его кластеры |
| Копирование внутри ФС | `cp <from> <to>` | копирует файл в новый путь или в существующий каталог |
| Перемещение/переименование | `mv <from> <to>` | переносит файл или каталог; если меняется только имя, работает как переименование |
| Запись текста | `write <path> <text>` | перезаписывает файл текстовой строкой |
| Просмотр файла | `cat <path>` | выводит логическое содержимое файла |
| Просмотр хранения | `rawcat <path>` | выводит физически сохраненные байты и объясняет пары RLE |
| Импорт файла | `import <host_file> <fs_path>` | копирует файл из основной ОС в учебную ФС |
| Экспорт файла | `export <fs_path> <host_file>` | копирует файл из учебной ФС в основную ОС |
| Импорт папки | `importdir <host_dir> <fs_path>` | рекурсивно импортирует каталог |
| Экспорт папки | `exportdir <fs_path> <host_dir>` | рекурсивно экспортирует каталог |
| Экспорт носителя | `savecarrier <host_file>` | сохраняет копию всего бинарного носителя |
| Импорт носителя | `loadcarrier <host_file>` | заменяет текущий носитель ранее сохраненной копией |
| Сжатие | `compress <path>` | применяет RLE-сжатие, если оно уменьшает файл |
| Распаковка | `decomp <path>` | восстанавливает файл в несжатом виде |
| Дефрагментация | `defrag` | заново записывает файлы подряд в область данных |
| Справка и выход | `help`, `exit`, `quit` | показывает команды или завершает программу |
На уровне программного интерфейса реализованы функции `fs_open`, `fs_close`, `fs_seek`, `fs_read` и `fs_write`. Открытый файл использует буфер в памяти, поэтому чтение и запись записей произвольной длины выполняются без прямой работы пользователя с кластерами.
```c
uint32_t fs_write(OpenFile *f, const unsigned char *buffer, uint32_t count) {
if (!f || !buffer || f->mode != MODE_WRITE ||
f->position + count > f->capacity) return 0;
memcpy(f->data + f->position, buffer, count);
f->position += count;
if (f->position > f->size) f->size = f->position;
f->dirty = true;
return count;
}
```
## Алгоритмы работы
В этом разделе приведены основные алгоритмы, которые обеспечивают корректное изменение состояния носителя. Они важны не только как детали реализации, но и как отражение базовых принципов файловых систем: инициализация структуры, выделение ресурсов, разрешение имен, преобразование данных и восстановление компактного размещения.
### Форматирование
При выполнении `format` создается бинарный файл нужного размера, вычисляются размеры служебных областей, резервируются занятые кластеры и создается корневой каталог `/`.
```c
for (i = 0; i < fs->sb.data_start_cluster; i++) bit_set(fs, i);
memset(fs->entries, 0, sizeof(fs->entries));
safe_copy(fs->entries[0].name, "/", MAX_NAME);
fs->entries[0].type = TYPE_DIR;
fs->entries[0].parent = 0;
```
### Выделение свободного кластера
Алгоритм линейно просматривает битовую карту, начиная с области данных. Первый свободный бит помечается как занятый, а номер соответствующего кластера возвращается вызывающей функции.
```c
static int alloc_cluster(FileSystem *fs) {
for (uint32_t i = fs->sb.data_start_cluster; i < fs->sb.total_clusters; i++) {
if (!bit_is_set(fs, i)) {
bit_set(fs, i);
return (int)i;
}
}
return -1;
}
```
### Работа с каталогами
Каталог не хранит отдельный массив дочерних элементов. Вместо этого у каждого дескриптора есть поле `parent`, содержащее индекс родительского каталога. Для поиска элемента программа просматривает индексный файл и сравнивает имя и родителя.
```c
bool fs_create_dir(FileSystem *fs, const char *path) {
int parent, index;
char name[MAX_NAME];
if (!split_parent_name(fs, path, &parent, name) ||
find_child(fs, parent, name) >= 0) return false;
index = free_entry_index(fs);
if (index < 0) return false;
memset(&fs->entries[index], 0, sizeof(Entry));
safe_copy(fs->entries[index].name, name, MAX_NAME);
fs->entries[index].type = TYPE_DIR;
fs->entries[index].parent = (uint32_t)parent;
fs_sync(fs);
return true;
}
```
### Сжатие
Команда `compress` использует простой алгоритм RLE: одинаковые подряд идущие байты заменяются парой «количество + значение». Если сжатый вариант не меньше исходного, файл не изменяется. При чтении сжатый файл автоматически восстанавливается в обычный вид. Для наблюдения за внутренним представлением предусмотрена команда `rawcat`: она показывает сохраненные байты в шестнадцатеричном виде и расшифровывает каждую RLE-пару. Команда `decomp` выполняет обратное преобразование и снова сохраняет файл без сжатия.
```c
while (i < size) {
unsigned char value = src[i];
uint32_t count = 1;
while (i + count < size && src[i + count] == value && count < 255) count++;
out[j++] = (unsigned char)count;
out[j++] = value;
i += count;
}
```
### Дефрагментация
Команда `defrag` временно считывает содержимое файлов, очищает карту размещения области данных и записывает файлы заново подряд. После операции используемые кластеры становятся компактно расположенными.
```c
memset(fs->bitmap, 0, fs->sb.bitmap_size);
for (c = 0; c < fs->sb.data_start_cluster; c++) bit_set(fs, c);
for (i = 0; i < MAX_ENTRIES; i++) {
if (fs->entries[i].type == TYPE_FILE) {
fs->entries[i].cluster_count = 0;
write_stored_bytes(fs, &fs->entries[i], copies[i], stored_sizes[i]);
}
}
```
## Тестирование
Пример базового сценария:
> **Теоретическая основа.** Тестирование файловой системы должно проверять не только отдельные команды, но и сохранение инвариантов: после записи файл должен читаться тем же содержимым, после удаления пространство должно освобождаться, после сжатия логические данные не должны меняться, а после дефрагментации содержимое должно сохраняться при изменении физического размещения.
```text
format 512
mkdir /docs
touch /docs/a.txt
write /docs/a.txt aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
cat /docs/a.txt
compress /docs/a.txt
ls /docs
rename /docs/a.txt b.txt
export /docs/b.txt exported.txt
defrag
info
```
Ожидаемые результаты:
| Проверка | Результат |
| --- | --- |
| Форматирование носителя | создается корректный виртуальный диск |
| Создание каталога и файла | объекты появляются в `ls` |
| Запись и чтение | `cat` выводит ранее записанный текст |
| Сжатие повторяющихся данных | `stored_size` становится меньше `size` |
| Переименование | файл доступен под новым именем |
| Экспорт | создается внешний файл с тем же содержимым |
| Дефрагментация | содержимое файлов сохраняется, кластеры уплотняются |
Для проверки сценария полезно опираться на сами команды интерактивной оболочки. Разбор пользовательского ввода связывает каждую строку с соответствующей функцией файловой системы:
```c
else if (strcmp(cmd, "mkdir") == 0 && a) printf(fs_create_dir(&fs, a) ? "ok\n" : "mkdir failed\n");
else if (strcmp(cmd, "write") == 0 && a && b) printf(fs_write_text(&fs, a, b) ? "ok\n" : "write failed\n");
else if (strcmp(cmd, "compress") == 0 && a) printf(fs_compress_file(&fs, a) ? "ok\n" : "compression did not reduce file\n");
else if (strcmp(cmd, "defrag") == 0) printf(fs_defrag(&fs) ? "ok\n" : "defrag failed\n");
```
## Ограничения реализации
| Ограничение | Причина |
| --- | --- |
| Не более 128 объектов | фиксированный размер индексного файла |
| Не более 128 кластеров на файл | фиксированный массив номеров кластеров |
| Длина имени до 31 символа | размер поля `name[32]` |
| Линейный поиск по каталогу | выбрана простая неупорядоченная структура |
| RLE эффективно не для всех данных | алгоритм выбран ради понятности реализации |
| Нет одновременного доступа нескольких процессов | учебная однопользовательская модель |
> **Теоретическая основа.** Ограничения учебной реализации обычно связаны с заранее фиксированными структурами данных. Это упрощает код и делает поведение системы предсказуемым, но уменьшает масштабируемость по сравнению с промышленными файловыми системами, где размеры таблиц, число блоков и механизмы синхронизации более гибкие.
Большинство ограничений заданы непосредственно константами программы. Это делает реализацию прозрачной для учебных целей, но одновременно фиксирует максимальные размеры структур:
```c
#define MAX_NAME 32
#define MAX_PATH 256
#define MAX_ENTRIES 128
#define MAX_FILE_CLUSTERS 128
```
## Вывод
В ходе работы была реализована простая файловая система, работающая поверх бинарного файла как виртуального носителя. Реализация охватывает все основные уровни, указанные в задании: физическое размещение, учет свободных блоков, индексные метаданные, каталоги, операции над файлами и сервисные команды управления носителем.
Полученная программа остается достаточно простой для изучения начинающим разработчиком на C, но при этом показывает полный путь данных: от пользовательской команды до изменения битовой карты, индексного дескриптора и байтов в кластерах виртуального диска.
> **Итоговое обобщение.** Разработанная система демонстрирует фундаментальный принцип файловых систем: удобные для человека операции над именованными объектами опираются на строго организованный набор служебных структур и алгоритмов управления блоками носителя.
Ключевая идея реализации хорошо видна в операции синхронизации: состояние файловой системы собирается из нескольких уровней и затем последовательно сохраняется на виртуальный носитель.
```c
static bool fs_sync(FileSystem *fs) {
fseek(fs->disk, 0, SEEK_SET);
fwrite(&fs->sb, sizeof(SuperBlock), 1, fs->disk);
fseek(fs->disk, fs->sb.bitmap_offset, SEEK_SET);
fwrite(fs->bitmap, 1, fs->sb.bitmap_size, fs->disk);
fseek(fs->disk, fs->sb.index_offset, SEEK_SET);
fwrite(fs->entries, sizeof(Entry), MAX_ENTRIES, fs->disk);
fflush(fs->disk);
return true;
}
```