Ядро 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