Виртуальная файловая система (VFS)
В статье про VDS система виртуальных устройств стояла на самом верху и маршрутизировала числа FD (descriptors) к устройствам. VFS стоит этажом ниже в башне файловой системы, и хотя имена похожи, эти двое делают совершенно разное. Именно с этого различия лучше всего и начать.
Зачем вообще нужен VFS?
Представь, что VFS нет. Тогда каждая программа, которая хочет открыть файл, должна была бы сама знать: лежит он на FAT или на TriaFS? На hd0 или hd1? В каком разделе? На каком кластере, иноде(inode), секторе? Это было бы невыносимо, и каждую программу пришлось бы подправлять при появлении новой разновидности файловой системы.
VFS снимает это с тебя. Ты даёшь путь, и всё. Это слой, который принимает "открой /home/x.txt" и выясняет, на каком разделе, в какой файловой системе и в каком месте лежит файл, а затем зовёт нужный драйвер ФС. Путь на вход, файл на выход, всё, что между, не твоя забота.
Отличие от VDS
Это самый важный вопрос, поэтому решим его первым.
VDS спрашивает "какое устройство?". Его единица контроляов, это поток байт на числе FD (file descriptor - просто число), его состояние на каждый процесс, это fd_table, и он знает только имена устройств вроде console, eth0, hd0. О путях, каталогах и разделах VDS не знает ничего. Он стоит на самом верху и контролирует таблицу файловых дескрипторов, виртуальные устройства.
VFS спрашивает "какой файл на каком разделе?". Его единица контроля, это файл или каталог, адресуемый путём. Его состояние на процесс, это fs_context (текущий раздел плюс рабочий каталог). Он понимает пути вроде /dev/hd0p0:/docs/a.txt, разрешает их в раздел и затем делегирует абстрактному слою ФС.
Соединяет этих двоих устройство vfs из статьи про VDS - vds_open по пути файла попадает на устройство vfs, и оно вызывает функции VFS. Так верхняя маршрутизация дотягивается до файловой системы под собой.
Два API для двух нужд
VFS предлагает два стиля, и какой подходит, зависит от размера файла.
- API целого файла: vfs_read_file грузит файл целиком, vfs_save_file пишет его, плюс vfs_create_folder, vfs_list_folder, vfs_change_directory и vfs_stat для метаданных.
- API потоков (stream API): vfs_fopen/vfs_fread/vfs_fwrite/vfs_fseek/vfs_fclose читают по кусочку. К различию вернусь ниже.
Пути - три записи, одна цель
Путь может быть в трёх формах:
- полной с устройством - /dev/hd0p0:/docs/a.txt
- краткой с устройством - hd0p0:/docs/a.txt
- или вовсе без него абсолютный - /docs/a.txt
- или вовсе без него относительный - docs/a.txt
Функция vfs_resolve_path превращает любую форму в одну и ту же пару - номер раздела и абсолютный путь. Если устройство есть в пути, оно вытаскивается оттуда; если его нет, на подмену приходит fs_context.
Как это выглядит в настоящем коде, показывает vfs_read_file. По сути это три шага:
- взять контекст,
- разрешить путь,
- передать дальше на следующий слой:
uint8_t* vfs_read_file(const char* path, uint32_t* out_size) {
// 1. взять контекст текущего процесса
uint8_t current_partition = vfs_get_current_partition();
uint32_t base_location = vfs_get_current_directory_location();
char current_dir_path[PATH_MAX];
vfs_copy_virtual_path(current_dir_path, PATH_MAX); // потокобезопасная копия
// 2. разрешить путь -> (раздел, абсолютный путь)
uint8_t resolved_partition;
char abs_path[PATH_MAX];
vfs_resolve_path(path, &resolved_partition, abs_path,
current_partition, current_dir_path);
// 3. делегировать абстрактному слою ФС
return fs_read_file(resolved_partition, abs_path, base_location, out_size);
}
Что важно отметить, само разрешение пути идёт без блокировок. Оно работает только с переменными на стеке, не трогает общих структур данных. Потому ему не нужна блокировка, оно не попадёт в deadlock, и несколько потоков не мешают друг другу. Блокировка нужна лишь при доступе к fs_context, а это горстка инструкций.
fs_context - для каждого процесса
Что значит контекст процесса? Ровно то, что shell знает как рабочий каталог. Каждый процесс несёт с собой четыре поля:
uint8_t current_partition; // текущий раздел
uint32_t current_directory; // расположение каталога (для относительных путей)
char current_path[PATH_MAX]; // текущий путь строкой
Spinlock fs_context_lock; // защищает этот блок
В этом весь смысл - процесс A может стоять в hd0p0:/home, а процесс B одновременно в hd1p0:/data. Один и тот же относительный ввод a.txt у каждого попадает в разный файл, ведь у каждого своё "здесь". Команда shell cd меняет только свой блок и никогда чужой. И поскольку другие потоки могут читать тот же блок, каждый доступ идёт через блокировку:
uint8_t vfs_get_current_partition(void) {
Process* proc = get_current_process();
spinlock_acquire(&proc->fs_context_lock);
uint8_t partition = proc->current_partition;
spinlock_release(&proc->fs_context_lock);
return partition;
}
При fork дочерний процесс наследует раздел и каталог родителя через fs_context_copy, но получает свой собственный блок. Дальше они расходятся. Есть тут одно правило - сперва осуществить блокировку родителя, потом дочернего процесса, всегда в этом порядке, иначе грозит deadlock.
Трюк с двумя геттерами (get_....)
Чтение пути таит ловушку. Соблазнительно просто вернуть указатель на current_path, это быстро и без блокировки. Но если другой поток в этот момент меняет путь, вызывающий читает "мусор" из памяти. Именно это и случилось с моими процессами. С тех пор есть два способа:
быстро, но без блокировки: безопасно лишь когда никто другой не пишет
const char* vfs_get_virtual_path(void) {
Process* proc = get_current_process();
const char* colon = strchr(proc->current_path, ':');
return colon ? colon + 1 : proc->current_path;
}
безопасно - копирует путь под блокировкой в буфер вызывающего
uint32_t vfs_copy_virtual_path(char* out_buffer, uint32_t buffer_size) {
Process* proc = get_current_process();
spinlock_acquire(&proc->fs_context_lock);
/* ... скопировать часть пути под блокировкой в out_buffer ... */
spinlock_release(&proc->fs_context_lock);
return len;
}
Всё семейство функций из примера кода выше с момента фикса последовательно использует vfs_copy_virtual_path. Указательный геттер остаётся для случаев, когда наверняка только один (лучше сказать единственный) поток имеет доступ к контексту.
API потоков - почему не всё сразу?
vfs_read_file удобен, но он грузит весь файл в кучу. Для конфига или иконки это неважно. Для файла в 100 МБ это 100 МБ RAM разом, а cat выводящий всё сразу, переполняет очередь консоли. Именно для этого есть stream API - маленький буфер идёт сквозь файл, и в памяти всегда лишь один кусок.
Файловы поток сам помнит своё состояние:
struct vfs_file_stream {
void* fs_handle; // хэндл конкретной файловой системы
uint8_t partition;
uint32_t flags;
uint64_t position; // текущая позиция (закэширована)
uint64_t file_size; // закэшированный размер
uint8_t eof, error;
};
Открытие идёт теми же тремя шагами, что и выше, только в конце выходит хэндл вместо буфера: взять контекст, разрешить путь, вызвать fs_fopen на абстрактном слое ФС, обернуть результат в поток (stream):
vfs_file_stream_t* vfs_fopen(const char* path, uint32_t flags) {
uint8_t partition = vfs_get_current_partition();
uint32_t base_location = vfs_get_current_directory_location();
char current_dir_path[PATH_MAX];
vfs_copy_virtual_path(current_dir_path, PATH_MAX);
uint8_t resolved_partition; char abs_path[PATH_MAX];
vfs_resolve_path(path, &resolved_partition, abs_path, partition, current_dir_path);
fs_file_handle_t* h = fs_fopen(resolved_partition, abs_path, base_location, flags);
if (!h) return NULL;
vfs_file_stream_t* s = kcalloc(1, sizeof(*s));
s->fs_handle = h; s->partition = resolved_partition;
s->flags = flags; s->file_size = fs_fsize(h);
return s; // дальше: vfs_fread(s, buf, n) в цикле
}
Хэндл потока (fs_handle) намеренно не потокобезопасен. Если два потока работают с одним файлом, каждый открывает свой хэндл. Это проще и быстрее, чем защищать хэндл блокировками и для доступа к файлу это естественный путь.
Правда жизни или ...
VFS это один из более зрелых слоёв, но "указательный" баг выше был поучителен - "быстро" и "правильно" это не одно и то же, а указатель в чужую память, это тикающая бомба, пока другой поток (thread) вправе в неё писать. Разрешение пути без блокировок, напротив, безопасно именно потому, что не трогает ничего общего и это не противоречие а то же правило с другой стороны.
Что дальше
VFS разрешил путь и передал его в fs_read_file и fs_fopen. Эти функции живут в абстрактном слое ФС, следующая часть. Это слой, который спрашивает уже не "какой файл?", а "какая файловая система лежит на этом разделе и как с ней говорить?". Дальше идут разделы (partitions), слой Storage и драйверы IDE и AHCI.
Было бы интересно увидеть ваши комментарии и улучшить статьи.
◀ Предыдущая статья Содержание Следующая статья ▶
*Система не стоит на месте, поэтому в дальнейшем тексты могут не совпадать с реальным положением