1 of 23

Модули ядра Linux

Проектирование цифровых вычислительных систем

Прутьянов В. В.

2 of 23

Ядро Linux

3 of 23

Модули ядра Linux

  • Linux – модульное ядро ОС, его функциональность можно расширять с помощью программ, называемых модулями ядра
  • Исходный код модуля пишется на языке C или Rust (с 2022 г.) и компилируется GCC или Clang в ELF-файл с расширением .ko
  • Во время работы ОС модуль можно загрузить командой insmod
  • Все загруженные модули можно отобразить командой lsmod
  • Модуль работает, пока не будет выгружен командой rmmod или не завершится исполнение самого ядра
  • Код всех загруженных модулей исполняется в том же виртуальном адресном пространстве, что и код ядра

4 of 23

Сборка модуля

  • Модуль – это обычная программа на C, использующая API ядра, но она не самодостаточна и должна собираться в контексте исходного кода конкретного ядра в виде in-tree или out-of-tree модуля
  • In-tree модули находятся в дереве исходников ядра и могут быть собраны как отдельные загружаемые модули (m), так и статически (built-in) в один ELF-файл вместе с ядром (y), выбор определятся настройками в конфигурационном файле (.config)
  • Out-of-tree модули всегда собираются отдельно и загружаются пользователем во время работы ядра
  • Сборка происходит через систему kbuild ядра – Makefile модуля делегирует сборку внутренней системе сборки ядра, которая знает все флаги, пути и зависимости
  • Исходный код ядра предоставляет заголовочные файлы и макросы (module_init, printk и т.д.)
  • Получается ELF-файл специального формата (.ko) с дополнительными секциями (.modinfo, .gnu.linkonce.this_module, .init.text, .exit.text) для загрузчика модулей
  • Модуль тесно привязан к версии ядра, т. к. он использует API ядра, который может меняться между версиями, поэтому модуль обычно работает только с тем ядром, под которое он скомпилирован

5 of 23

Пример out-of-tree модуля

#include <linux/init.h> // For __init, __exit

#include <linux/module.h> // For any module

#include <linux/kernel.h> // For printing

MODULE_LICENSE("GPL");

MODULE_VERSION("1.0");

static int __init hello_init(void) // Entry point

{

pr_info(KBUILD_MODNAME ": ""Hello World!\n");

return 0; // Success

}

static void __exit hello_exit(void)

{

pr_info(KBUILD_MODNAME ": ""Goodbye World!\n");

}

module_init(hello_init);

module_exit(hello_exit);

obj-m := hello.o

KERNELDIR ?= /lib/modules/$(shell uname -r)/build

PWD := $(shell pwd)

all:

$(MAKE) -C $(KERNELDIR) M=$(PWD) modules

clean:

$(MAKE) -C $(KERNELDIR) M=$(PWD) clean

Makefile

hello.c

$ make

$ sudo insmod hello.ko

$ sudo rmmod hello.ko

$ dmesg

[173917.672037] hello: Hello World!

[173918.903805] hello: Goodbye World!

6 of 23

Kernel vs. User

  • Нет доступа к библиотекам языка C – в ядре отсутствуют какие-либо стандартные библиотеки, используются вспомогательные функции, экспортируемые самим ядром или другими модулями
  • Нет изоляции внутри ядра – ядро и все модули находятся в общем адресном пространстве
  • Отсутствует защита памяти – некорректный доступ к памяти может привести к kernel panic
  • Запрещены вычисления с плавающей точкой – FP-вычисления требуют сохранения и восстановления состояния, что усложнило бы переключение контекста между ядром и пользовательским пространством, поэтому используются целочисленные вычисления
  • Ограничен размер стека – каждому потоку ядра выделяется небольшой фиксированный стек (обычно 8-16 КБ), переполнение приводит к повреждению данных, в т.ч. ядра или других модулей

7 of 23

Изоляция пространств пользователя и ядра

В целях безопасности, ядро и пользовательский код работают на разных уровнях привилегий

  • Privilege Levels (PL) в ARMv7, Exception Levels (EL) в ARMv8
  • Protection Rings 0/3 в x86

8 of 23

System Call Interface

Программы взаимодействуют с ядром через интерфейс системных вызовов (system call interface), предоставляющий контролируемый доступ к аппаратным ресурсам и привилегированным операциям.

  1. Программа помещает в регистры и стек номер системного вызова и аргументы
  2. Программа выполняет специальную инструкцию (syscall на x86, svc в ARM), вызывающую переход CPU в привилегированный режим и исполнение архитектурно-зависимого кода ядра
  3. Ядро сохраняет контекст и по номеру определяет адрес обработчика системных вызовов в read-only таблице sys_call_table, затем переходит по адресу и исполняет код обработчика
  4. Ядро помещает код возврата в регистр, восстанавливает сохраненный контекст и производит возврат в пользовательское пространство (sysret на x86, eret в ARM64)

9 of 23

Способы обмена информацией с пространством ядра

  • dmesg (/dev/kmsg) передача текстовых сообщений логирования и отладки через кольцевой буфер
  • Файл символьного устройства в /devмеханизм взаимодействия через файловые операции POSIX API, такие как open, read, write, mmap, ioctl, poll и т.д.
  • sysfs/debugfs/procfs – двустороннее взаимодействие через системные вызовы к представленным ядром или модулями файлам и директориям на одной из псевдо-ФС
  • Сигналы – пользовательское приложение может получать сигналы из ядра
  • netlinkдвунаправленный IPC-канал между ядром и пользовательским пространством через сокеты AF_NETLINK, используется для обмена структурированными данными и сетевыми настройками
  • eBPFмодуль может запустить предоставленный пользовательским процессом eBPF-байткод в JIT-интерпретаторе после статической верификации, и обмениваться с ним данными через словари

10 of 23

Файл устройства в /dev

  • Устройства с интерфейсом последовательного доступа называются символьными устройствами (character device), в отличие от блочных устройств (block device) с произвольным доступом
  • Примеры: /dev/zero, /dev/random, /dev/null, /dev/kmsg, /dev/tty*
  • Файловые операции (POSIX API) над символьным устройством обрабатываются его драйвером
  • Указатели на обработчики файловых операций передаются через struct file_operations
  • Все символьные устройства в системе имеют уникальную пару номеров – major и minor
  • Есть несколько способов зарегистрировать класс символьных устройств и само устройство

11 of 23

Miscellaneous Device

  • Наиболее просто сделать устройство класса Miscellaneous Device с динамическим minor
  • Такое устройство получает major=10 и некоторый minor по выбору ядра из числа свободных
  • Для регистрация есть функция misc_register, для освобождение – misc_deregister
  • При регистрации нужно передать структуру struct miscdevice, содержащую указатель на struct file_operations, с указателями на функции-обработчики файловых операций

12 of 23

Изоляция пространств пользователя и ядра

  • CPU обращается в память по виртуальным адресам, доступ напрямую по физ. адресам невозможен
  • У ядра в общем случае нет доступа к user-space памяти, т.к. user-страницы могут быть не отображены в пространство ядра, в т.ч. вытеснены на диск (kernel-страницы в Linux никогда не вытесняются)
  • Конкретная реализация изоляции между kernel и user пространствами зависит от архитектуры
  • Для копирования данных из/в user-страницы есть вызовы copy_from_user/copy_to_user
  • Для примитивных типов данных есть упрощенные варианты – get_user/put_user
  • Эти функции производят необходимые проверки, если нужно, подгружают страницу с диска, запрещают swapping и настраивают временное отображение, затем производят копирование
  • Также существуют zero-copy способы обменяться данными с user-space

13 of 23

Пример: символьное устройство в /dev

#include <linux/module.h>

#include <linux/miscdevice.h>

#include <linux/fs.h>

#include <linux/uaccess.h>

static const char msg[] = "Hello from device!\n";

static ssize_t hello_read(struct file *filp,

char __user *buf, size_t count, loff_t *pos)

{

if (*pos > 0 || count < sizeof(msg)-1)

return 0;

if (copy_to_user(buf, msg, sizeof(msg)-1))

return -EFAULT;

return sizeof(msg)-1;

}

static int hello_open(struct inode *inode, struct file *filp)

{

pr_info(KBUILD_MODNAME ": ""device open\n");

return 0;

}

static int hello_release(struct inode *inode, struct file *filp)

{

pr_info(KBUILD_MODNAME ": ""device close\n");

return 0;

}

static const struct file_operations hello_fops = {

.owner = THIS_MODULE,

.read = hello_read,

.open = hello_open,

.release = hello_release,

};

static struct miscdevice hello_miscdev = {

.minor = MISC_DYNAMIC_MINOR, // Use free number

.name = KBUILD_MODNAME, // Device name

.fops = &hello_fops, // File operations

.mode = 0444, // Read only

};

static int __init hello_init(void)

{

return misc_register(&hello_miscdev);

}

static void __exit hello_exit(void)

{

misc_deregister(&hello_miscdev);

}

module_init(hello_init);

module_exit(hello_exit);

MODULE_LICENSE("GPL");

14 of 23

Пример: символьное устройство в /dev

$ sudo insmod hello.ko

$ file /dev/hello

/dev/hello: character special (10/123)

$ head -n3 /dev/hello

Hello from device!

Hello from device!

Hello from device!

$ dmesg

....................

[186671.007614] hello: device open

[186671.007657] hello: device close

$ sudo rmmod hello

$ file /dev/hello

/dev/hello: cannot open `/dev/hello' (No such file or directory)

15 of 23

Структура file_operations

struct file_operations {

struct module *owner;

fop_flags_t fop_flags;

loff_t (*llseek) (struct file *, loff_t, int);

ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);

ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);

ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);

ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);

__poll_t (*poll) (struct file *, struct poll_table_struct *);

long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);

long (*compat_ioctl) (struct file *, unsigned int, unsigned long);

int (*mmap) (struct file *, struct vm_area_struct *);

int (*open) (struct inode *, struct file *);

int (*flush) (struct file *, fl_owner_t id);

int (*release) (struct inode *, struct file *);

int (*fsync) (struct file *, loff_t, loff_t, int datasync);

int (*fasync) (int, struct file *, int);

int (*flock) (struct file *, int, struct file_lock *);

long (*fallocate)(struct file *file, int mode, loff_t offset, loff_t len);

int (*fadvise)(struct file *, loff_t, loff_t, int);

int (*uring_cmd)(struct io_uring_cmd *ioucmd, unsigned int issue_flags);

int (*uring_cmd_iopoll)(struct io_uring_cmd *, struct io_comp_batch *, unsigned int poll_flags);

.........................

}

16 of 23

ioctl

  • Часто модулю нужен интерфейс, не несущий однозначный смысл записи/чтения данных
  • Есть системный вызов ioctl и соответствующий ему обработчик:

long (*unlocked_ioctl) (struct file *filp, unsigned int cmd, unsigned long arg);

  • ioctl позволяет передать команду (cmd) и указатель на произвольные данные (arg)
  • Для объявления команд существует 4 специальных макроса, в зависимости от того, что обработчик делает с пользовательскими данными по переданному указателю:

#define _IO(type,nr) _IOC(_IOC_NONE,(type),(nr),0)

#define _IOR(type,nr,argtype) _IOC(_IOC_READ,(type),(nr),(_IOC_TYPECHECK(argtype)))

#define _IOW(type,nr,argtype) _IOC(_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(argtype)))

#define _IOWR(type,nr,argtype) _IOC(_IOC_READ|_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(argtype)))

17 of 23

Пример: ioctl

#include "cmds.h"

static atomic_t num = {0};

static long hello_ioctl(struct file *filp,

unsigned int cmd, unsigned long arg)

{

switch (cmd) {

case INC_NUM:

atomic_inc(&num);

return 0;

case GET_NUM:

return put_user(atomic_read(&num),

(int __user *)arg);

default:

return -ENOTTY;

}

}

static struct file_operations hello_fops = {

.owner = THIS_MODULE,

.unlocked_ioctl = hello_ioctl,

};

#include "cmds.h"

int main() {

int old = -1, new = -1;

int fd = open("/dev/hello", O_RDWR);

ioctl(fd, GET_NUM, &old);

ioctl(fd, INC_NUM);

ioctl(fd, GET_NUM, &new);

printf("%d -> %d\n", old, new);

return 0;

}

#define INC_NUM _IO('k', 0x40)

#define GET_NUM _IOR('k', 0x41, int)

$ sudo ./test

0 -> 1

$ sudo ./test

1 -> 2

hello.c

test.c

cmds.h

18 of 23

Управление памятью

19 of 23

Управление памятью

20 of 23

Управление памятью

  • kmalloc
    • Выделяет физически непрерывную память
    • Может выделить достаточно ограниченный объем памяти
    • void *kmalloc(size_t size, gfp_t flags)
  • vmalloc
    • Выделяет физически разрывную память
    • Может выделять большие объемы виртуальной памяти
    • void *vmalloc(unsigned long size)
  • SLAB/SLOB/SLUB (аллокаторы объектов)
    • Выделяет память для часто используемых типов из кэша
    • struct kmem_cache *kmem_cache_create(const char *name, size_t size, size_t align, unsigned long flags, void (*ctor)(void *))
    • kmem_cache_alloc(struct kmem_cache *cachep, gfp_t flags)
  • Page Allocator (Buddy Allocator)
    • Выделяет физически непрерывные блоки размером 2N страниц
    • __get_free_pages(gfp_t flags, unsigned order)

21 of 23

Логические адреса

  • Часть виртуальных адресов в пространстве ядра являются т.н. логическими адресами
  • Логические адреса отличаются от соответствующих физических на константное смещение PAGE_OFFSET
  • Преобразования логических адресов в физические и обратно осуществляется через virt_to_phys и phys_to_virt
  • kmalloc возвращает логические адреса

Источник изображения: elinux.org/images/7/77/Intro-to-memory-management.pdf

22 of 23

Задание

  1. Разработать модуль ядра Linux, выделяющий заданный объем памяти с помощью kmalloc и vmalloc по команде, получаемой из user-space через символьное устройство
  2. Используя модуль, выяснить, какой максимальный объем можно выделить через kmalloc/vmalloc

23 of 23

Литература

  • Цирюлик О., “Расширения ядра Linux”
  • P. J. Salzman, M. Burian, O. Pomerantz, B. Mottram, J. Huang “The Linux Kernel Module Programming Guide”
  • https://docs.kernel.org/
  • https://elixir.bootlin.com/linux/latest/source