/dev/nulll — устройство /dev/null Limited Edition

Материал из iRunner Wiki

Intro

С устройством /dev/null связано множество компьютерных шуток. Говорят, что информация, направленная в /dev/null, преобразуется процессором в тепловую энергию, и если интенсивно записывать данные в /dev/null, то может наступить перегрев [1]. В редких случаях система может выдать предупреждение пользователю, что он исчерпал 98% места на своём /dev/null и устройство нужно почистить.

Задание

Требуется реализовать внешний модуль для ядра Linux, который создаёт новое символьное устройство под названием /dev/nulll (три буквы L). По смыслу оно должно работать так же, как стандартное нулевое устройство. Но с двумя дополнительными возможностями.

Учёт размера данных

В отличие от обычного /dev/null, новое устройство должно запоминать, сколько байт информации в него было влито.

Возникает вопрос, как эту информацию можно у устройства узнать. К сожалению, стандартные утилиты ls и stat не показывают размер символьных устройств (потому что по смыслу он не определён). Мы будем получать размер в байтах через команду blockdev. Да, она предназначена для блочных устройств, но её легко заставить работать для символьного. Такой вызов

blockdev --getsize64 /dev/nulll

должен возвращать размер в байтах тех данных, которые были отправлены в /dev/nulll. Примечание: число 64 здесь не связано с разрядностью процессора (команда без 64 возвращает размер в блоках, а не в байтах).

На самом деле, внутри blockdev просто делается системный вызов ioctl(). Поэтому указанная команда эквивалентна такому коду на C:

#include <fcntl.h>
#include <linux/fs.h>
#include <stdio.h>
#include <sys/ioctl.h>
#include <unistd.h>
 
int main() {
    int fd;
    int ret = 0;
    unsigned long size = 0;
 
    fd = open("/dev/nulll", O_RDONLY);
    if (fd == -1) {
        perror("open");
        return 1;
    }
 
    if (ioctl(fd, BLKGETSIZE64, &size) == -1) {
        perror("ioctl");
        ret = 1;
    } else {
        printf("%lu\n", size);
    }
 
    if (close(fd) == -1) {
        perror("close");
        ret = 1;
    };
    return ret;
}

При выгрузке и повторной загрузке модуля счётчик «занятого места» должен обнуляться.

Ограничение «места на диске»

Если при загрузке модуля будет задан целочисленный параметр capacity, равный некоторому положительному числу x, то устройство /dev/nulll не даст записать в себя более x байт информации. Последующие записи будут приводить к ошибке ENOSPC ("No space left on device").

Пример

sobols@sobols-VirtualBox:~/hw/nulll$ sudo insmod nulll.ko capacity=12
sobols@sobols-VirtualBox:~/hw/nulll$ printf "hello" > /dev/nulll
sobols@sobols-VirtualBox:~/hw/nulll$ blockdev --getsize64 /dev/nulll
5
sobols@sobols-VirtualBox:~/hw/nulll$ printf "world!" > /dev/nulll
sobols@sobols-VirtualBox:~/hw/nulll$ blockdev --getsize64 /dev/nulll
11
sobols@sobols-VirtualBox:~/hw/nulll$ printf "more" > /dev/nulll
bash: printf: write error: No space left on device
sobols@sobols-VirtualBox:~/hw/nulll$ blockdev --getsize64 /dev/nulll
12
sobols@sobols-VirtualBox:~/hw/nulll$ sudo rmmod nulll

Должна поддерживаться конкурентная запись из многих процессов/потоков:

sobols@sobols-VirtualBox:~/hw/nulll$ sudo insmod nulll.ko
sobols@sobols-VirtualBox:~/hw/nulll$ seq 10 | xargs -P 10 -I{} dd if=/dev/zero of=/dev/nulll bs=1000 count=1000000
1000000+0 records in
1000000+0 records out
1000000000 bytes (1,0 GB, 954 MiB) copied, 1,79302 s, 558 MB/s
1000000+0 records in
1000000+0 records out
1000000000 bytes (1,0 GB, 954 MiB) copied, 1,90621 s, 525 MB/s
1000000+0 records in
1000000+0 records out
1000000000 bytes (1,0 GB, 954 MiB) copied, 1,92727 s, 519 MB/s
1000000+0 records in
1000000+0 records out
1000000000 bytes (1,0 GB, 954 MiB) copied, 2,00348 s, 499 MB/s
1000000+0 records in
1000000+0 records out
1000000000 bytes (1,0 GB, 954 MiB) copied, 1,92515 s, 519 MB/s
1000000+0 records in
1000000+0 records out
1000000000 bytes (1,0 GB, 954 MiB) copied, 1,91734 s, 522 MB/s
1000000+0 records in
1000000+0 records out
1000000000 bytes (1,0 GB, 954 MiB) copied, 2,07701 s, 481 MB/s
1000000+0 records in
1000000+0 records out
1000000000 bytes (1,0 GB, 954 MiB) copied, 2,04042 s, 490 MB/s
1000000+0 records in
1000000+0 records out
1000000000 bytes (1,0 GB, 954 MiB) copied, 2,01516 s, 496 MB/s
1000000+0 records in
1000000+0 records out
1000000000 bytes (1,0 GB, 954 MiB) copied, 2,06829 s, 483 MB/s
sobols@sobols-VirtualBox:~/hw/nulll$ blockdev --getsize64 /dev/nulll
10000000000

Технические детали

Язык программирования — C. Помимо исходного кода, нужно предоставить также Makefile для сборки модуля ядра (ko-файла).

Утверждается, что решение задачи свободно умещается в 100 строк кода.

Проверка будет осуществляться на 64-битной ОС Ubuntu 18.04.1 LTS со стандартным ядром 4.15.0. Поскольку ядро развивается, возможны небольшие нюансы с совместимостью. Например, функция misc_deregister() в ядрах до версии 4.3 возвращала int, затем тип возвращаемого значения был изменён на void [2]. При проверке мелкие ошибки компиляции, вызванные различием версий ядер, я готов поправить самостоятельно.

Литература

  1. You can be a kernel hacker! — мотивирующий пост от Джулии Эванс (читать по желанию).
  2. Write Your First Linux Kernel Module — статья про разработку модуля ядра, который создаёт устройство /dev/reverse, призванное менять порядок слов в строках на противоположный.