Ядро Linux — монолитное. Это означает, что все его части работают в общем адресном пространстве. Однако, это не означает, что для добавления какой-то возможности необходимо полностью перекомпилировать ядро. Новую функциональность можно добавить в виде модуля ядра. Такие модули можно легко загружать и выгружать по необходимости прямо во время работы системы.
С помощью модулей можно реализовать свои файловые системы, причём со стороны пользователя такая файловая система ничем не будет отличаться от ext4 или NTFS. В этом задании мы с Вами реализуем упрощённый аналог NFS: все файлы будут храниться на удалённом сервере, однако пользователь сможет пользоваться ими точно так же, как и файлами на собственном жёстком диске.
Мы рекомендуем при выполнении этого домашнего задания использовать отдельную виртуальную машину: любая ошибка может вывести всю систему из строя, и вы можете потерять ваши данные.
Мы проверили работоспособность всех инструкций для дистрибутива Ubuntu 20.04 x64 и ядра версии 5.4.0-90. Возможно, при использовании других дистрибутивов, вы столкнётесь с различными ошибками и особенностями, с которыми вам придётся разобраться самостоятельно.
Выполните задание в ветке
networkfs.
Все файлы и структура директорий хранятся на удалённом сервере. Сервер поддерживает HTTP API, документация к которому доступна по ссылке.
Для получения токенов и тестирования вы можете воспользоваться консольной утилитой curl.
Сервер поддерживает два типа ответов:
- Бинарные данные: набор байт (
char*), который можно скастить в структуру, указанную в описании ответа. Учтите, что первое поле ответа (первые 8 байт) — код ошибки. - JSON-объект: человекочитаемый ответ. Для его получения необходимо передавать GET-параметр
json.
Формат JSON предлагается использовать только для отладки, поскольку текущая реализация функции connect_to_server работает только с бинарным форматом. Однако, вы можете её доработать и реализовать собственный JSON-парсер.
Для начала работы вам необходимо завести собственный бакет — пространство для хранения файлов, и получить токен для доступа к нему. Это делается следующим запросом:
$ curl https://nerc.itmo.ru/teaching/os/networkfs/v1/token/issue?json
{"status":"SUCCESS","response":"8c6a65c8-5ca6-49d7-a33d-daec00267011"}Строка 8c6a65c8-5ca6-49d7-a33d-daec00267011 и является токеном, который необходимо передавать во все последующие запросы. Количество токенов и размер файловой системы не ограничены, однако, мы будем вынуждены ограничить пользователей в случае злоупотребления данной возможностью.
Запускать user-space программы из kernel-space затруднительно, поэтому мы реализовывали для вас собственный HTTP-клиент в виде функции connect_to_server (utils.c:73):
int connect_to_server(const char *command, int params_count, const char *params[], const char *token, char *output_buf);const char *command— название метода (list,create,read,write,lookup,unlink,rmdir,link)int params_count— количество параметровconst char *params[]— список параметров — строки вида<parameter_name>=<value>const char *token— ваш токенchar *output_buf— буфер для сохранения ответа от сервера размером не менее 8 КБ
Функция возвращает 0, если запрос завершён успешно, и код соответствующей ошибки (utils.c:8) в случае неуспешного HTTP-запроса (если не удалось подключиться к серверу, прочитать ответ или если статус HTTP-ответа не равен 200).
Давайте научимся компилировать и подключать тривиальный модуль. Для компиляции модулей ядра нам понадобятся утилиты для сборки и заголовочные файлы. Установить их можно так:
$ sudo apt-get install build-essential linux-headers-`uname -r`Мы уже подготовили основу для вашего будущего модуля в файле networkfs.c. Познакомьтесь с ней.
Ядру для работы с модулем достаточно двух функций — одна должна инициализировать модуль, а вторая — очищать результаты его работы. Они указываются с помощью module_init и module_exit.
Важное отличие кода для ядра Linux от user-space-кода — в отсутствии в нём стандартной библиотеки libc. Например, в ней же находится функция printf. Мы можем печатать данные в системный лог с помощью функции printk.
Теперь напишем простой Makefile с инструкциями для сборки файла и очистки артефактов:
obj-m += networkfs.o
all:
make -C /lib/modules/$(shell uname -r)/build M=$(shell pwd) modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(shell pwd) cleanОбратите внимание, что перед командами должен быть символ табуляции: четыре пробела не подойдут.
Соберём модуль:
$ sudo makeЕсли наш код скомпилировался успешно, в текущей директории появится файл networkfs.ko — это и есть наш модуль. Осталось загрузить его в ядро:
$ sudo insmod networkfs.koОднако, мы не увидели нашего сообщения. Оно печатается не в терминал, а в системный лог — его можно увидеть командой dmesg:
$ dmesg
<...>
[ 123.456789] Hello, World!Для выгрузки модуля нам понадобится команда rmmod:
$ sudo rmmod networkfs
$ dmesg
<...>
[ 123.987654] Goodbye!Операционная система предоставляет две функции для управления файловыми системами:
register_filesystem— сообщает о появлении нового драйвера файловой системыunregister_filesystem— удаляет драйвер файловой системы
В этой части мы начнём работать с несколькими структурами ядра:
inode— описание метаданных файла: имя файла, расположение, тип файла (в нашем случае — регулярный файл или директория)dentry— описание директории: списокinodeвнутри неё, информация о родительской директории, …super_block— описание всей файловой системы: информация о корневой директории, …
Функции register_filesystem и unregister_filesystem принимают структуру с описанием файловой системы. Начнём с такой:
struct file_system_type networkfs_fs_type =
{
.name = "networkfs",
.mount = networkfs_mount,
.kill_sb = networkfs_kill_sb
};Для монтирования файловой системы в этой структуре мы добавили два поля. Первое — mount — указатель на функцию, которая вызывается при монтировании. Например, она может выглядеть так:
struct dentry* networkfs_mount(struct file_system_type *fs_type, int flags, const char *token, void *data)
{
struct dentry *ret;
ret = mount_nodev(fs_type, flags, data, networkfs_fill_super);
if (ret == NULL)
{
printk(KERN_ERR "Can't mount file system");
}
else
{
printk(KERN_INFO "Mounted successfuly");
}
return ret;
}Эта функция будет вызываться всякий раз, когда пользователь будет монтировать нашу файловую систему. Например, он может это сделать следующей командой (документация):
$ sudo mount -t networkfs <token> <path>Опция -t нужна для указания имени файловой системы — именно оно указывается в поле name. Также мы передаём токен, полученный в прошлой части, и локальную директорию, в которую ФС будет примонтирована. Обратите внимание, что эта директория должна быть пуста.
Мы используем функцию mount_nodev, поскольку наша файловая система не хранится на каком-либо физическом устройстве:
struct dentry* mount_nodev(struct file_system_type *fs_type, int flags, void *data, int (*fill_super)(struct super_block *, void *, int));Последний её аргумент — указатель на функцию fill_super. Эта функция должна заполнять структуру super_block информацией о файловой системе. Давайте начнём с такой функции:
int networkfs_fill_super(struct super_block *sb, void *data, int silent)
{
struct inode *inode;
inode = networkfs_get_inode(sb, NULL, S_IFDIR, 1000);
sb->s_root = d_make_root(inode);
if (sb->s_root == NULL)
{
return -ENOMEM;
}
printk(KERN_INFO "return 0\n");
return 0;
}Аргументы data и silent нам не понадобятся. В этой функции мы используем ещё одну (пока) неизвестную функцию — networkfs_get_inode. Она будет создавать новую структуру inode, в нашем случае — для корня файловой системы:
struct inode *networkfs_get_inode(struct super_block *sb, const struct inode *dir, umode_t mode, int i_ino)
{
struct inode *inode;
inode = new_inode(sb);
inode->i_ino = i_ino;
if (inode != NULL)
{
inode_init_owner(inode, dir, mode);
}
return inode;
}Давайте поймём, что эта функция делает. Файловой системе нужно знать, где находится корень файловой системы. Для этого в поле s_root мы записываем результат функции d_make_root), передавая ему корневую inode. На сервере корневая директория всегда имеет номер 1000.
Для создания новой inode используем функцию new_inode. Кроме этого, с помощью функции inode_init_owner зададим тип ноды — укажем, что это директория. указать тип этой inode как директорию. Все возможные значения umode_t, перечислены в документации — они позволяют задавать не только тип объекта, но и права доступа.
Второе поле, которое мы определили в file_system_type — поле kill_sb — указатель на функцию, которая вызывается при отмонтировании файловой системы. В нашем случае ничего делать не нужно:
void networkfs_kill_sb(struct super_block *sb)
{
printk(KERN_INFO "networkfs super block is destroyed. Unmount successfully.\n");
}Не забудьте зарегистрировать файловую систему в функции инициализации модуля, и удалять её при очистке модуля. Наконец, соберём и примонтируем нашу файловую систему:
$ sudo make
$ sudo insmod networkfs.ko
$ sudo mount -t networkfs 8c6a65c8-5ca6-49d7-a33d-daec00267011 /mnt/ctЕсли вы всё правильно сделали, ошибок возникнуть не должно. Тем не менее, перейти в директорию /mnt/ct не выйдет — ведь мы ещё не реализовали никаких функций для навигации по ФС.
Теперь отмонтируем файловую систему:
$ sudo umount /mnt/ctВ базовой версии задания все имена файлов и директорий состоят только из латинских букв, цифр, символов подчёркивания, точек и дефисов.
В прошлой части мы закончили на том, что не смогли перейти в директорию:
$ sudo mount -t networkfs 8c6a65c8-5ca6-49d7-a33d-daec00267011 /mnt/ct
$ cd /mnt/ct
-bash: cd: /mnt/ct: Not a directoryЧтобы это исправить, необходимо реализовать некоторые методы для работы с inode. Чтобы эти методы вызывались, в поле i_op нужной нам ноды необходимо записать структуру inode_operations. Например, такую:
struct inode_operations networkfs_inode_ops =
{
.lookup = networkfs_lookup,
};Первая функция, которую мы реализуем — lookup. Именно она позволяет операционной системе определять, что за сущность описывается данной нодой. Сигнатура функции должна быть такой:
struct dentry*
networkfs_lookup(struct inode *parent_inode, struct dentry *child_dentry, unsigned int flag);parent_inode— родительская нодаchild_dentry— объект, к которому мы пытаемся получить доступflag— неиспользуемое значение
Пока ничего не будем делать: просто вернём NULL. Если мы заново попробуем повторить переход в директорию, у нас ничего не получится — но уже по другой причине:
$ cd /mnt/ct
-bash: cd: /mnt/ct: Permission deniedРешите эту проблему. Пока сложной системы прав у нас не будет — у всех объектов в файловой системе могут быть права 777. В итоге должно получиться что-то такое:
$ ls -l /mnt/
total 0
drwxrwxrwx 1 root root 0 Oct 24 15:52 ctПосле этого мы сможем перейти в /mnt/ct, но не можем вывести содержимое директории. На этот раз нам понадобится не i_op, а i_fop — структура типа file_operations. Реализуем в ней первую функцию — iterate.
struct file_operations networkfs_dir_ops =
{
.iterate = networkfs_iterate,
};Эта функция вызывается только для директорий и выводит список объектов в ней (нерекурсивно): для каждого объекта вызывается функция dir_emit, в которую передаётся имя объекта, номер ноды и его тип.
Пример функции networkfs_iterate приведён ниже:
int networkfs_iterate(struct file *filp, struct dir_context *ctx)
{
char fsname[10];
struct dentry *dentry;
struct inode *inode;
unsigned long offset;
int stored;
unsigned char ftype;
ino_t ino;
ino_t dino;
dentry = filp->f_path.dentry;
inode = dentry->d_inode;
offset = filp->f_pos;
stored = 0;
ino = inode->i_ino;
while (true)
{
if (ino == 100)
{
if (offset == 0)
{
strcpy(fsname, ".");
ftype = DT_DIR;
dino = ino;
}
else if (offset == 1)
{
strcpy(fsname, "..");
ftype = DT_DIR;
dino = dentry->d_parent->d_inode->i_ino;
}
else if (offset == 2)
{
strcpy(fsname, "test.txt");
ftype = DT_REG;
dino = 101;
}
else
{
return stored;
}
}
dir_emit(ctx, fsname, strlen(fsname), dino, ftype);
stored++;
offset++;
ctx->pos = offset;
}
return stored;
}Попробуем снова получить список файлов:
$ ls /mnt/ct
ls: cannot access '/mnt/ct/test.txt': No such file or directory
test.txtЭта ошибка возникла из-за того, что lookup работает только для корневой директории — но не для файла test.txt. Это мы исправим в следующих частях.
Вам осталось реализовать iterate для корневой директории с запросом к серверу.
Теперь мы хотим научиться переходить по директориям. На этом шаге функцию networkfs_lookup придётся немного расширить: если такой файл есть, нужно вызывать функцию d_add, передавая ноду файла. Например, так:
struct dentry *networkfs_lookup(struct inode *parent_inode, struct dentry *child_dentry, unsigned int flag)
{
ino_t root;
struct inode *inode;
const char *name = child_dentry->d_name.name;
root = parent_inode->i_ino;
if (root == 100 && !strcmp(name, "test.txt"))
{
inode = networkfs_get_inode(parent_inode->i_sb, NULL, S_IFREG, 101);
d_add(child_dentry, inode);
}
else if (root == 100 && !strcmp(name, "dir"))
{
inode = networkfs_get_inode(parent_inode->i_sb, NULL, S_IFDIR, 200);
d_add(child_dentry, inode);
}
return NULL;
}Реализуйте навигацию по файлам и директориям, используя данные с сервера.
Теперь научимся создавать и удалять файлы. Добавим ещё два поля в inode_operations — create и unlink:
Функция networkfs_create вызывается при создании файла и должна возвращать новую inode с помощью d_add, если создать файл получилось. Рассмотрим простой пример:
int networkfs_create(struct inode *parent_inode, struct dentry *child_dentry, umode_t mode, bool b)
{
ino_t root;
struct inode *inode;
const char *name = child_dentry->d_name.name;
root = parent_inode->i_ino;
if (root == 100 && !strcmp(name, "test.txt"))
{
inode = networkfs_get_inode(parent_inode->i_sb, NULL, S_IFREG | S_IRWXUGO, 101);
inode->i_op = &networkfs_inode_ops;
inode->i_fop = NULL;
d_add(child_dentry, inode);
mask |= 1;
}
else if (root == 100 && !strcmp(name, "new_file.txt"))
{
inode = networkfs_get_inode(parent_inode->i_sb, NULL, S_IFREG | S_IRWXUGO, 102);
inode->i_op = &networkfs_inode_ops;
inode->i_fop = NULL;
d_add(child_dentry, inode);
mask |= 2;
}
return 0;
}Чтобы проверить, как создаются файлы, воспользуемся утилитой touch:
$ touch test.txt
$ ls
test.txt
$ touch new_file.txt
$ ls
test.txt new_file.txt
$Для удаления файлов определим ещё одну функцию — networkfs_unlink.
int networkfs_unlink(struct inode *parent_inode, struct dentry *child_dentry)
{
const char *name = child_dentry->d_name.name;
ino_t root;
root = parent_inode->i_ino;
if (root == 100 && !strcmp(name, "test.txt"))
{
mask &= ~1;
}
else if (root == 100 && !strcmp(name, "new_file.txt"))
{
mask &= ~2;
}
return 0;
}Теперь у нас получится выполнять и команду rm.
$ ls
test.txt new_file.txt
$ rm test.txt
$ ls
new_file.txt
$ rm new_file.txt
$ ls
$Обратите внимание, что утилита
touchпроверяет существование файла: для этого вызывается функцияlookup.
Следующая (и последняя из обязательных) часть нашего задания — создание и удаление директорий. Добавим в inode_operations ещё два поля — mkdir и rmdir. Их сигнатуры можно найти тут.
Если мы всё сделали правильно, теперь мы сможем запустить тесты. Для этого добавьте следующие таргеты в Makefile:
tests: all
python3 -m tests BasicTestCases -f
bonus-name: all
python3 -m tests NameTestCases -f
bonus-wr: all
python3 -m tests WRTestCases -f
bonus-link: all
python3 -m tests LinkTestCases -f
Для запуска тестов вам понадобится Python 3 и библиотека requests. Запустите тесты и проверьте, что ваше решение работает:
$ sudo make tests
<...>
Ran 12 tests in 32.323s
OKВ этот раз вы можете выполнить любое количество бонусных заданий — баллы суммируются.
Реализуйте возможность создания файлов и директорий, состоящих из любых печатных символов, кроме символа / и '. Пример команды, которая можно будет исполнить:
$ touch '!@#$%^&*()-+ '
$ ls
'!@#$%^&*()-+ "Если вы всё сделали правильно, пройдёт тестовый набор bonus-name:
$ sudo make bonus-name
<...>
Ran 3 tests in 1.814s
OKРеализуйте чтение из файлов и запись в файлы. Для этого вам понадобится структура file_operations не только для директорий, но и для обычных файлов.
В неё вам понадобится добавить два поля — read и write. Соответствующие функции имеют следующие сигнатуры:
ssize_t networkfs_read(struct file *filp, char *buffer, size_t len, loff_t *offset);
ssize_t networkfs_write(struct file *filp, const char *buffer, size_t len, loff_t *offset);Аргументы такие:
filp— файловый дескрипторbuffer— буфер в user-space для чтения и записи соответственноlen— длина данных для записиoffset— смещение
Обратите внимание, что просто так обратиться в buffer нельзя, поскольку он находится в user-space. Использкйте функцию get_user для чтения и put_user для записи.
В результате вы сможете сделать вот так:
$ cat file1
hello world from file1
$ cat file2
file2 content here
$ echo "test" > file1
$ cat file1
test
$Обратите внимание, что файл должен уметь содержать любые ASCII-символы с кодами от 0 до 127 включительно.
Если вы всё сделали правильно, пройдёт тестовый набор bonus-wr:
$ sudo make bonus-wr
<...>
Ran 5 tests in 1.953s
OKВам необходимо поддержать возможность сослаться из разных мест файловой системы на одну и ту же inode.
Обратите внимание: сервер поддерживает жёсткие ссылки только для регулярных файлов, но не для директорий.
Для этого добавьте поле link в структуру inode_operations. Сигнатура соответствующей функции выглядит так:
int networkfs_link(struct dentry *old_dentry, struct inode *parent_dir, struct dentry *new_dentry);После реализации функции вы сможете выполнить следующие команды:
$ ln file1 file3
$ cat file1
hello world from file1
$ cat file3
hello world from file1
$ echo "test" > file1
$ rm file1
$ cat file3
test
$Если вы не делали девятую часть, вы всегда можете проверить работу ссылок через запросы к серверу по HTTP: в свежесозданной ссылке будут те же данные, что и в изначальном файле. Тесты не проверяют, что вы умеете читать и писать в файлы.
Если вы всё сделали правильно, пройдёт тестовый набор bonus-link:
$ sudo make bonus-link
<...>
Ran 2 tests in 1.535s
OK