Система виртуальных устройств (Virtual Device System)
В обзоре файловых систем VDS стоял на самом верху как маршрутизатор FD (file descriptors), и я обещал полную статью. Вот она. VDS, это центральный посредник системы - любое обращение программы к чему-либо, к экрану, к клавиатуре, к сетевой карте, к файлу, идёт через VDS. Это слой, который превращает "запиши вот сюда" в "вызови нужный драйвер устройства".
Основная идея - всё, это устройство
VDS заимствует старую идею Unix: всё, это устройство за одним и тем же интерфейсом. У меня каждое устройство, это vds_device_t, структура, которая есть не что иное, как имя, тип и горстка указателей на функции:
typedef struct vds_device {
const char* name; // "console", "null", "hd0", "eth0", "vfs"
vds_device_type_t type; // CHAR · BLOCK · NETWORK
int (*open) (struct vds_device*, int flags);
ssize_t (*read) (struct vds_device*, void* buf, size_t size);
ssize_t (*write)(struct vds_device*, const void* buf, size_t size, const fd_write_state_t*);
int (*ioctl)(struct vds_device*, int cmd, void* arg);
int (*close)(struct vds_device*);
/* только для блочных устройств, иначе NULL: */
off_t (*lseek)(struct vds_device*, off_t off, int whence);
uint32_t (*get_block_size)(struct vds_device*);
void* private_data;
struct vds_device* next; // связный реестр
} vds_device_t;
Будь то console, eth0 или файл, программа всегда зовёт лишь read или write.
Дескриптор файла и fd_table
Это число, это дескриптор файла (FD). open() возвращает FD и у каждого процесса своя таблица fd_table до 1024 записей. Первые три зарезервированы и знакомы по Unix: FD 0 это STDIN, FD 1 это STDOUT, FD 2 это STDINFO (аналог STDERR). Каждая запись говорит, на что указывает FD:
typedef struct {
int is_open; // CLOSED · OPEN · CLOSING
fd_source_t source; // устройство VDS ИЛИ поток VFS
union {
vds_device_t* device; // console, hd0, eth0 ...
struct vfs_file_stream* file_stream; // один открытый файл
} object;
uint32_t offset; // позиция чтения/записи
uint32_t flags; // O_RDONLY, O_APPEND, ...
} fd_entry_t;
Важно это "у каждого процесса" - FD 5 в одной программе и FD 5 в другой полностью независимы и могут указывать на совершенно разные вещи. Именно это позже делает возможным перенаправление.
Реестр - как устройство становится известным
Все устройства висят в одном связном списке, реестре. При старте каждый драйвер объявляет своё устройство через vds_register_device, а vds_find_device(name) потом находит его. Сделать новое устройство, это создать файл с минимальной структурой, вот /dev/null целиком:
static ssize_t null_write(vds_device_t* dev, const void* buf, size_t size,
const fd_write_state_t* st) {
return (ssize_t)size; // всё проглотить, ничего не происходит
}
static ssize_t null_read(vds_device_t* dev, void* buf, size_t size) {
return 0; // всегда EOF
}
static vds_device_t null_device = {
.name = "null", .type = VDS_DEVICE_CHAR,
.read = null_read, .write = null_write,
};
void null_device_init(void) { vds_register_device(&null_device); }
Вот и всё устройство - пара функций и регистрация. Любое более сложное устройство, консоль, сетевая карта, имеет ту же форму, только с большим содержимым в функциях.
Есть четыре семейства.
Символьные устройства отдают поток байт. Самое важное, это `console` и она прокси: настоящий backend сменный, при загрузке это `early_console`, который пишет прямо в видеопамять, позже `standard_console`, который идёт через очередь консоли. Рядом `null` (проглатывает всё), `kmsg` (буфер лога ядра, то, что показывает `dmesg`) и `console_session` (по консоли на каждое окно shell).
Сетевые устройства, это `eth0` … `ethN`, по одному на каждую найденную карту. Они не встроены жёстко, а регистрируются динамически соответствующим драйвером NIC (E1000, Realtek, PCnet) при старте. `write` отправляет пакет, `read` принимает.
Блочные устройства, это носители: `hd0`, `hd1` (IDE/AHCI/SATA), `usb0`, плюс `partition` и `optical`. Как блочное устройство VDS ещё не реализован полностью. Инфраструктура есть, устройства зарегистрированы, у них даже уже есть блочные функции `lseek` и `get_block_size`. Но тип `VDS_DEVICE_BLOCK` помечен в коде как "for future" и файловые системы сейчас зовут слой Storage напрямую, а не через эти блочные устройства VDS. Правильный доступ к ним "файловая система -> блок VDS -> Storage" существует на бумаге. Это одна из строек, что ещё впереди.
Устройство VFS, это та самая часть, которая действительно работает - `vfs` соединяет VDS с файловой системой. `open()` файла вроде `/home/x.txt` попадает на устройство `vfs`, и оно открывает поток VFS. Так файловый FD доходит до файловой системы через VDS, ровно та связь что я показывал в обзоре.
Взаимодействие - один printf и его путь
Теперь видно, почему VDS, это центральный посредник. Обычный путь вывода: `printf` форматирует строку, зовёт `vds_write` на STDOUT, VDS ищет FD 1 (указывает на `console`), консоль кладёт символы в очередь консоли на CPU, поток blit в ядре забирает их и рисует на экран через HAL.
Но поскольку STDOUT, это всего лишь запись в `fd_table`, он может указывать в другое место, и программа ничего не замечает. Укажи FD 1 на `/dev/null`, и вывод исчезает без следа. Укажи на `eth0`, и он уходит в сеть (удалённое логирование). Укажи на файл, и он попадает на диск, а это и есть `cmd > файл` в shell. Программа всегда зовёт лишь `printf`; цель определяется единственно записью в таблице. Эта развязка, это весь смысл VDS.
Что то знакомое - printf, где то я это уже видел... Библиотека stdio, но есть одно но! Это не тот printf. Как и "положено" я не использую стандартные библиотеки. Это просто название функции из моей собственной библиотеки и она могла бы называться print_formated() например. Но я как человек имеющий влияние от linux оставил так.
Проблема блокировки и трюк для ее решения
Куда же без них. Когда работает `read(fd)`, оно должно заблокировать `fd_table`, ведь второй поток того же процесса мог бы в это же время закрывать тот же FD. Но `read` обязан вызвать `dev->read()`, а устройство может работать долго или само устанавливать блокировки. Если бы VDS держал блокировку `fd_table` во время вызова устройства, получился бы гарантированный deadlock.
Решение, это фиксированный приём: заблокировать, скопировать указатель на устройство, сразу же разблокировать, только потом звать устройство, затем заблокировать снова и проверить, существует ли FD до сих пор.
ssize_t vds_read(int fd, void* buf, size_t size) {
spinlock_acquire(&process->fd_table_lock);
fd_entry_t* entry = &process->fd_table[fd];
if (entry->is_open != FD_STATE_OPEN) {
spinlock_release(&process->fd_table_lock);
return -1;
}
vds_device_t* dev = entry->object.device; // 1. скопировать указатель
spinlock_release(&process->fd_table_lock); // 2. отпустить блокировку
ssize_t result = dev->read(dev, buf, size); // 3. вызвать устройство (может блокировать)
spinlock_acquire(&process->fd_table_lock); // 4. заблокировать снова
if (fd < process->max_fds && process->fd_table[fd].is_open) // 5. ещё открыт?
process->fd_table[fd].offset += result; // (иначе close() уже закрыл FD)
spinlock_release(&process->fd_table_lock);
return result;
}
Шаг 5 - это тот, который легко забыть: пока устройство читало (без блокировки), другой поток мог закрыть FD. Слепо обновив тогда offset, пишешь в уже освобождённую структуру, use-after-free. Потому и перепроверка.
Честное пионерское место
VDS также был источником некоторых из моих ошибок. Самой пресловутой было зависание на простом `ls` - `printf` шёл при удерживаемой блокировке Storage, очередь консоли переполнялась, никто не мог её опустошить, потому что блокировка держалась, и вся система вставала (колом). Урок из этого стал одним из правил части про синхронизацию - никакого `printf` при удерживаемой блокировке. К этому добавилась гонка (race) FD сверху, `close()` посреди `read()`.
И история с блочным устройством остаётся открытой: красивая, полная "слоистость" :-), где и файловые системы идут через блочные устройства VDS, начата, но не закончена. Как часто бывает, каркас стоит, а внутренняя отделка ждёт.
Что дальше
VDS был первой отдельной частью иерархии файловой системы. В следующих частях спускаемся ниже - VFS (пути, потоки, контекст процесса), затем абстрактный слой, разделы, слой Storage и наконец драйверы IDE и AHCI.
Дежурная фраза - ставим лайки, подписываемся на канал... Нет, не так. Было бы интересно увидеть ваши комментарии и улучшить статьи.
◀ Предыдущая статья Содержание Следующая статья ▶
*Система не стоит на месте, поэтому в дальнейшем тексты могут не совпадать с реальным положением