как код попадает в работающее ядро без перезагрузкиhow code gets into a running kernel without a reboot
Загружаемый модуль — это обычный объектный код ядра, скомпилированный отдельно от основного образа и подключаемый в рантайме. Всё, что видно снаружи (modprobe, автозагрузка при подключении устройства, lsmod), — надстройка над одним и тем же механизмом: разрешением символов между модулем и уже загруженным ядром.A loadable module is ordinary kernel object code, compiled separately from the main image and attached at runtime. Everything visible from outside (modprobe, autoloading when a device is plugged in, lsmod) is tooling built on top of one and the same mechanism: symbol resolution between the module and the already-loaded kernel.
Модуль ядра — файл .ko (kernel object), физически лежащий в /lib/modules/$(uname -r)/, то есть в подпапке, привязанной к конкретной версии ядра — модуль, собранный под одну версию, как правило не подходит для другой. Формат файла — тот же ELF, что и у обычных исполняемых бинарников, но со специальными секциями: __ksymtab (список символов, которые модуль экспортирует для использования другими модулями) и таблица неразрешённых символов, которые модуль ожидает найти в уже загруженном ядре или в других модулях. Загрузка модуля — не копирование кода «как есть», а связывание (linking) в рантайме: адреса функций ядра, на которые ссылается модуль, разрешаются в момент вставки, а не заранее на этапе компиляции.A kernel module is a .ko (kernel object) file, physically living in /lib/modules/$(uname -r)/ — a subfolder tied to a specific kernel version, since a module built for one version generally doesn't fit another. The file format is the same ELF used by ordinary executable binaries, but with special sections: __ksymtab (the list of symbols the module exports for other modules to use) and a table of unresolved symbols the module expects to find in the already-loaded kernel or in other modules. Loading a module isn't copying code as-is — it's runtime linking: the kernel-function addresses the module references get resolved at insertion time, not ahead of time at compile time.
Ядро не открывает модулю доступ к произвольному коду — только к тому, что явно помечено макросом EXPORT_SYMBOL() (или EXPORT_SYMBOL_GPL() для символов, доступных только GPL-совместимому коду). При компиляции такие символы попадают в отдельную ELF-секцию __ksymtab; при загрузке ядра или модуля, экспортирующего символы, эта секция читается, и таблица символов ядра пополняется. Когда загружается новый модуль, insmod разрешает его неразрешённые ссылки именно по этой таблице — если нужного символа там нет (потому что он не экспортирован или ещё не загружен другой модуль, который его экспортирует), загрузка обрывается с ошибкой отсутствующего символа. Файл Module.symvers, генерируемый при сборке ядра, фиксирует контрольные суммы версий экспортируемых символов — механизм, защищающий от загрузки модуля, собранного под несовместимую версию структур ядра.The kernel doesn't open up arbitrary code to a module — only what's explicitly marked with the EXPORT_SYMBOL() macro (or EXPORT_SYMBOL_GPL() for symbols available only to GPL-compatible code). At compile time such symbols land in a separate ELF section, __ksymtab; when the kernel boots or a symbol-exporting module loads, that section gets read and the kernel's symbol table is populated from it. When a new module loads, insmod resolves its unresolved references against exactly this table — if the needed symbol isn't there (because it's not exported, or because another module that exports it hasn't loaded yet), loading aborts with a missing-symbol error. The Module.symvers file, generated during a kernel build, records version checksums for exported symbols — a mechanism guarding against loading a module built against an incompatible version of kernel structures.
Модуль может зависеть от символов, экспортированных другим модулем, а не только от символов самого ядра, — тогда порядок загрузки важен: зависимый модуль нужно вставить только после того, как загружен модуль, экспортирующий нужные ему символы. depmod заранее анализирует все модули в /lib/modules/$(uname -r)/ и строит файл modules.dep — список зависимостей каждого модуля от других. modprobe читает этот файл при запросе на загрузку и сам, без участия пользователя, вставляет сначала все требуемые зависимости через insmod, а затем — сам запрошенный модуль. Именно поэтому insmod и modprobe — не взаимозаменяемые синонимы: insmod вставляет ровно тот файл, что ему передали, и просто падает с ошибкой, если что-то из зависимостей не загружено; modprobe сначала смотрит в modules.dep и достраивает недостающую цепочку сам.A module can depend on symbols exported by another module, not just on the kernel's own symbols — in which case load order matters: a dependent module can only be inserted after the module exporting its needed symbols has loaded. depmod analyzes every module in /lib/modules/$(uname -r)/ ahead of time and builds a modules.dep file — a list of each module's dependencies on others. modprobe reads this file when asked to load something and, without any user involvement, inserts every required dependency via insmod first, then the requested module itself. This is exactly why insmod and modprobe aren't interchangeable synonyms: insmod inserts precisely the file it's handed and simply fails if a dependency isn't loaded; modprobe checks modules.dep first and fills in the missing chain itself.
Когда на шине (USB, PCI и др.) появляется новое устройство, ядро генерирует uevent — событие с идентификаторами устройства (vendor ID, product ID и т.д.). systemd-udevd перехватывает это событие, собирает из идентификаторов строку modalias определённого формата и передаёт её modprobe. Каждый модуль в своём коде указывает таблицу поддерживаемых устройств (через макрос вроде MODULE_DEVICE_TABLE), из которой при сборке модуля извлекается собственная строка modalias — modprobe сопоставляет строку из события с этими записями и загружает подходящий модуль автоматически, без участия пользователя. Это тот же самый механизм автозагрузки, которым объясняется, почему подключённая флешка или Wi-Fi-адаптер сразу заработали без ручной команды modprobe.When a new device appears on a bus (USB, PCI, and so on), the kernel emits a uevent — an event carrying the device's identifiers (vendor ID, product ID, etc.). systemd-udevd catches this event, builds a modalias string of a specific format from the identifiers, and hands it to modprobe. Every module's code declares a table of supported devices (via a macro like MODULE_DEVICE_TABLE), from which the module's own modalias string is extracted at build time — modprobe matches the event's string against these entries and loads the matching module automatically, with no user involvement. This is the exact same autoloading mechanism behind why a plugged-in flash drive or Wi-Fi adapter just works without a manual modprobe command.
Прежде чем собирать модуль, нужны заголовки ядра именно той версии, что реально запущена — не произвольные, а linux-headers-$(uname -r) (Debian/Ubuntu) или kernel-devel (RHEL/Fedora). Внутри этого пакета лежит подготовленное дерево сборки в /lib/modules/$(uname -r)/build — символическая ссылка на конфигурацию именно того ядра, для которого собирался пакет, со всеми нужными .config-настройками и заголовочными файлами. Собирать модуль без соответствия версии заголовков и запущенного ядра бессмысленно: он либо не соберётся, либо не загрузится из-за несовпадения контрольных сумм в Module.symvers (см. выше).Before building a module you need kernel headers for the exact version actually running — not just any headers, but linux-headers-$(uname -r) (Debian/Ubuntu) or kernel-devel (RHEL/Fedora). That package contains a prepared build tree at /lib/modules/$(uname -r)/build — a symlink into the configuration for that exact kernel, with all the needed .config settings and header files. Building a module without matching the running kernel's header version is pointless: it either won't build, or won't load due to a Module.symvers checksum mismatch (see above).
# Debian/Ubuntu
sudo apt install linux-headers-$(uname -r)
# RHEL/Fedora
sudo dnf install kernel-devel-$(uname -r)
Минимальный модуль, не привязанный ни к какому железу, — просто функции, вызываемые при загрузке и выгрузке:The most minimal module, not tied to any hardware at all — just functions called on load and unload:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
static int __init hello_init(void)
{
printk(KERN_INFO "hello: module loaded\n");
return 0; // 0 = успешная загрузка; ненулевой код откатит insmod
}
static void __exit hello_exit(void)
{
printk(KERN_INFO "hello: module unloaded\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL"); // не-GPL лицензия закроет доступ к EXPORT_SYMBOL_GPL()
MODULE_AUTHOR("you");
MODULE_DESCRIPTION("minimal example module");
module_init()/module_exit() регистрируют указанные функции как точки входа/выхода — insmod вызовет hello_init сразу после разрешения всех символов модуля, rmmod вызовет hello_exit перед выгрузкой. MODULE_LICENSE("GPL") — не формальность: немаркированный или проприетарный модуль не получает доступа к символам, экспортированным через EXPORT_SYMBOL_GPL(), и в логе ядра появляется предупреждение о «tainted kernel».module_init()/module_exit() register the given functions as entry/exit points — insmod calls hello_init right after resolving all of the module's symbols, rmmod calls hello_exit before unloading. MODULE_LICENSE("GPL") isn't a formality: an unmarked or proprietary module doesn't get access to symbols exported via EXPORT_SYMBOL_GPL(), and the kernel log records a "tainted kernel" warning.
Сборка модуля вне дерева ядра (out-of-tree) идёт не напрямую через gcc, а через тот же kbuild, что собирает само ядро — Makefile модуля не компилирует файл сам, а вызывает систему сборки ядра с параметром M=, указывающим, где лежат исходники модуля:Building a module outside the kernel source tree (out-of-tree) doesn't go through gcc directly — it goes through the same kbuild that builds the kernel itself; the module's Makefile doesn't compile the file itself, it invokes the kernel's build system with an M= parameter pointing at where the module's sources live:
obj-m += hello.o
KDIR := /lib/modules/$(shell uname -r)/build
all:
$(MAKE) -C $(KDIR) M=$(PWD) modules
clean:
$(MAKE) -C $(KDIR) M=$(PWD) clean
# собрать
make
# результат — hello.ko в текущей папке; загрузить и проверить
sudo insmod hello.ko
dmesg | tail -n 2
sudo rmmod hello
dmesg | tail -n 2
Пример посложнее — скелет PCI-драйвера, показывающий на практике именно то, что описано в разделе про modalias выше: таблица pci_device_id, помеченная MODULE_DEVICE_TABLE, — это и есть тот самый список, из которого при сборке извлекается modalias-запись, а udev потом сопоставляет с ней uevent от реального устройства.A slightly more involved example — a PCI driver skeleton, showing in practice exactly what the modalias section above described: the pci_device_id table, tagged with MODULE_DEVICE_TABLE, is exactly the list a modalias entry gets extracted from at build time, which udev later matches against a real device's uevent.
Реальный драйвер редко останавливается на «нашли устройство, вывели сообщение» — почти всегда ему нужна память, которую устройство сможет читать и писать напрямую через шину, в обход CPU (DMA). Расширим скелет: кастомное PCI-устройство, которому драйвер при обнаружении выделяет 1 КБ когерентной DMA-памяти, а при отключении — аккуратно освобождает.A real driver rarely stops at "found the device, printed a message" — it almost always needs memory the device can read and write directly over the bus, bypassing the CPU (DMA). Let's extend the skeleton: a custom PCI device that the driver allocates 1 KB of coherent DMA memory for on discovery, and carefully frees on removal.
#include <linux/module.h>
#include <linux/pci.h>
#include <linux/dma-mapping.h>
#define DMA_BUF_SIZE 1024 // 1 КБ, как в задаче
// список поддерживаемых устройств: vendor ID, device ID
static struct pci_device_id example_ids[] = {
{ PCI_DEVICE(0x10ec, 0x8168) }, // пример: Realtek RTL8168
{ 0, } // обязательный терминатор списка
};
MODULE_DEVICE_TABLE(pci, example_ids); // отсюда извлекается modalias при сборке
// приватные данные устройства: без них негде хранить DMA-адрес между probe и remove
struct example_dev {
void *dma_buf; // CPU-указатель — им пользуется драйвер
dma_addr_t dma_handle; // шинный адрес — его прописывают в регистр устройства
};
static int example_probe(struct pci_dev *pdev, const struct pci_device_id *id)
{
struct example_dev *edev;
int ret;
// devm_ = освободится сам при remove()/ошибке probe(), без ручного kfree
edev = devm_kzalloc(&pdev->dev, sizeof(*edev), GFP_KERNEL);
if (!edev)
return -ENOMEM;
ret = pci_enable_device(pdev); // включить устройство на шине
if (ret)
return ret;
ret = pci_request_regions(pdev, "example_pci"); // застолбить BAR за собой
if (ret)
goto err_disable;
pci_set_master(pdev); // разрешить устройству инициировать DMA-транзакции само
// сообщаем ядру диапазон адресов, которые устройство умеет адресовать по шине;
// без этого вызова dma_alloc_coherent() ниже может вернуть память,
// физически недостижимую для 32-битного DMA-движка устройства
ret = dma_set_mask_and_coherent(&pdev->dev, DMA_BIT_MASK(32));
if (ret)
goto err_release;
// coherent-память: и CPU, и устройство видят одни и те же данные
// без ручной синхронизации кэша — платим за это тем, что она не кэшируется
edev->dma_buf = dma_alloc_coherent(&pdev->dev, DMA_BUF_SIZE,
&edev->dma_handle, GFP_KERNEL);
if (!edev->dma_buf) {
ret = -ENOMEM;
goto err_release;
}
// dma_handle — это то, что реально прописывается в регистр устройства;
// dma_buf — это то, чем пользуется сам драйвер на стороне CPU
dev_info(&pdev->dev, "example: %d bytes DMA buffer at bus addr %pad\n",
DMA_BUF_SIZE, &edev->dma_handle);
pci_set_drvdata(pdev, edev); // сохранить указатель — remove() получит его обратно
return 0;
err_release:
pci_release_regions(pdev);
err_disable:
pci_disable_device(pdev);
return ret;
}
static void example_remove(struct pci_dev *pdev)
{
struct example_dev *edev = pci_get_drvdata(pdev);
// освобождать строго в обратном порядке от того, что делали в probe()
dma_free_coherent(&pdev->dev, DMA_BUF_SIZE, edev->dma_buf, edev->dma_handle);
pci_release_regions(pdev);
pci_disable_device(pdev);
dev_info(&pdev->dev, "example: device released\n");
}
static struct pci_driver example_driver = {
.name = "example_pci",
.id_table = example_ids,
.probe = example_probe,
.remove = example_remove,
};
// major-номер общий на весь драйвер, а не на конкретное устройство —
// получаем его один раз при загрузке модуля, а не в probe()
static int __init example_init(void)
{
example_major = register_blkdev(0, "exampleblk"); // 0 = ядро само выберет свободный major
if (example_major < 0)
return example_major;
return pci_register_driver(&example_driver);
}
static void __exit example_exit(void)
{
pci_unregister_driver(&example_driver);
unregister_blkdev(example_major, "exampleblk");
}
module_init(example_init);
module_exit(example_exit);
MODULE_LICENSE("GPL");
register_blkdev(0, ...) с нулём вместо конкретного номера просит ядро само подобрать свободный major — вручную закреплять конкретное число смысла нет, если устройство не должно совпадать с исторически зарезервированными номерами. Именно поэтому регистрация major вынесена в module_init(), а не в probe(): PCI-устройств с этим vendor/device ID может обнаружиться несколько, но major на все них должен быть общим — получать его повторно для каждого не нужно и небезопасно (второй вызов просто провалится, потому что имя уже занято).register_blkdev(0, ...) with zero instead of a specific number asks the kernel to pick a free major itself — manually pinning a specific number makes no sense unless the device needs to match a historically reserved one. This is exactly why major registration lives in module_init() rather than probe(): several PCI devices with this vendor/device ID might be discovered, but the major needs to be shared across all of them — requesting it again for each one is unnecessary and unsafe (the second call would simply fail because the name is already taken).
probe() вызывается ядром автоматически для каждого устройства, ID которого совпал с записью в example_ids — не нужно вручную искать устройство, PCI-подсистема ядра сама сопоставляет и вызывает. Порядок действий в probe() не произвольный: pci_enable_device() включает устройство физически, pci_request_regions() резервирует его BAR-регионы за этим драйвером (чтобы два драйвера не полезли в одну и ту же память одновременно), pci_set_master() разрешает устройству самому инициировать передачи по шине — без него DMA с устройства невозможен в принципе. dma_set_mask_and_coherent() — не формальность, а декларация: она говорит ядру, какие физические адреса DMA-движок устройства способен адресовать, и от неё зависит, откуда dma_alloc_coherent() возьмёт память. Сам dma_alloc_coherent() возвращает сразу два адреса одного и того же куска памяти: dma_buf — обычный указатель, которым пользуется код драйвера на CPU, и dma_handle — адрес в адресном пространстве шины, который нужно записать в регистр устройства, чтобы оно знало, куда писать/откуда читать. При отключении устройства — обязательно освобождать ресурсы в обратном порядке от того, как их занимали, и через dma_free_coherent(), а не обычный kfree(): DMA-память выделена не через стандартный аллокатор ядра.probe() is called by the kernel automatically for every device whose ID matches an entry in example_ids — there's no need to manually search for the device, the kernel's PCI subsystem matches and calls it itself. The order of operations in probe() isn't arbitrary: pci_enable_device() physically powers the device on, pci_request_regions() reserves its BAR regions for this driver (so two drivers can't reach into the same memory at once), pci_set_master() lets the device initiate bus transfers itself — without it, DMA from the device is impossible in principle. dma_set_mask_and_coherent() isn't a formality, it's a declaration: it tells the kernel which physical addresses the device's DMA engine can actually address, and it determines where dma_alloc_coherent() will pull memory from. dma_alloc_coherent() itself returns two addresses for the same chunk of memory at once: dma_buf — an ordinary pointer the driver's own CPU-side code uses, and dma_handle — the bus address space address that needs to be written into the device's register so it knows where to write to/read from. On device removal, resources must be freed in the reverse order they were acquired, and via dma_free_coherent(), not a plain kfree() — DMA memory wasn't allocated through the standard kernel allocator.
Выделенный DMA-буфер пока виден только самому драйверу — userspace ничего о нём не знает. Чтобы отдать эти же 1024 байта наружу как обычное блочное устройство (/dev/exampleblk, доступное через стандартный read()/write() или через dd), нужно зарегистрировать gendisk поверх той же самой памяти, на которую уже указывает edev->dma_buf — второй слой поверх уже существующего, не отдельная подсистема.The allocated DMA buffer is so far visible only to the driver itself — userspace knows nothing about it. To expose those same 1024 bytes outward as an ordinary block device (/dev/exampleblk, reachable via a regular read()/write() or via dd), a gendisk needs to be registered on top of the exact same memory edev->dma_buf already points to — a second layer on top of what already exists, not a separate subsystem.
#include <linux/blk-mq.h>
#include <linux/blkdev.h>
#define SECTOR_SIZE 512
#define NUM_SECTORS (DMA_BUF_SIZE / SECTOR_SIZE) // 1024 / 512 = 2 сектора
static int example_major;
// расширяем ранее объявленную структуру приватных данных устройства
struct example_dev {
void *dma_buf;
dma_addr_t dma_handle;
struct gendisk *disk; // то, что видно в /dev
struct blk_mq_tag_set tag_set; // очередь запросов blk-mq для этого диска
};
// вызывается ядром на каждый запрос чтения/записи к /dev/exampleblk
static blk_status_t example_queue_rq(struct blk_mq_hw_ctx *hctx,
const struct blk_mq_queue_data *bd)
{
struct request *rq = bd->rq;
struct example_dev *edev = rq->q->queuedata;
struct bio_vec bv;
struct req_iterator iter;
loff_t pos = blk_rq_pos(rq) * SECTOR_SIZE; // смещение внутри 1 КБ буфера
blk_mq_start_request(rq);
// один request может состоять из нескольких bio-сегментов — проходим по всем
rq_for_each_segment(bv, rq, iter) {
void *buf = kmap_local_page(bv.bv_page) + bv.bv_offset;
if (pos + bv.bv_len > DMA_BUF_SIZE) { // не выйти за пределы буфера
kunmap_local(buf);
blk_mq_end_request(rq, BLK_STS_IOERR);
return BLK_STS_IOERR;
}
if (rq_data_dir(rq) == WRITE)
memcpy(edev->dma_buf + pos, buf, bv.bv_len); // userspace → DMA-буфер
else
memcpy(buf, edev->dma_buf + pos, bv.bv_len); // DMA-буфер → userspace
pos += bv.bv_len;
kunmap_local(buf);
}
blk_mq_end_request(rq, BLK_STS_OK); // сообщить блочному слою: запрос выполнен
return BLK_STS_OK;
}
static const struct blk_mq_ops example_mq_ops = {
.queue_rq = example_queue_rq,
};
static const struct block_device_operations example_bdops = {
.owner = THIS_MODULE,
};
queue_rq() — прямой аналог функций чтения/записи символьного устройства, только вызывается блочным слоем ядра, а не напрямую через read()/write(). Один request может объединять несколько разрозненных страниц пользовательской памяти (bio_vec) — отсюда цикл rq_for_each_segment: буфер пользователя почти никогда не лежит одним сплошным куском в физической памяти, и обходить его нужно по сегментам. kmap_local_page() превращает физическую страницу в указатель, с которым может работать код ядра, — после копирования её обязательно освобождать через kunmap_local(). Направление копирования определяется через rq_data_dir(rq): WRITE — из bio в наш DMA-буфер (userspace пишет на устройство), иначе — из буфера в bio (userspace читает с устройства). В конце — обязательный blk_mq_end_request(): без него блочный слой будет вечно ждать завершения запроса, а процесс, вызвавший read()/write(), зависнет.queue_rq() is the direct analog of a character device's read/write functions, except it's called by the kernel's block layer rather than directly by read()/write(). One request can bundle several scattered pages of user memory (bio_vec) — hence the rq_for_each_segment loop: a user's buffer almost never sits as one contiguous chunk in physical memory, and it has to be walked segment by segment. kmap_local_page() turns a physical page into a pointer kernel code can work with — after copying, it must be released via kunmap_local(). The copy direction is decided via rq_data_dir(rq): WRITE means from the bio into our DMA buffer (userspace writing to the device), otherwise it's from the buffer into the bio (userspace reading from the device). At the end, blk_mq_end_request() is mandatory — without it the block layer waits forever for the request to complete, and the process that called read()/write() hangs.
Регистрация самого диска дописывается в example_probe(), сразу после успешного dma_alloc_coherent(), а отмена регистрации — в начало example_remove(), перед освобождением DMA-памяти (диск должен исчезнуть из /dev раньше, чем данные, на которые он указывает, станут недействительными):Registering the disk itself is added to example_probe() right after a successful dma_alloc_coherent(), and unregistering it goes at the start of example_remove(), before the DMA memory is freed (the disk needs to disappear from /dev before the data it points to becomes invalid):
// добавить в конец example_probe(), перед "return 0;"
edev->tag_set.ops = &example_mq_ops;
edev->tag_set.nr_hw_queues = 1; // одна аппаратная очередь — у нас не настоящее железо с несколькими
edev->tag_set.queue_depth = 4;
edev->tag_set.numa_node = NUMA_NO_NODE;
ret = blk_mq_alloc_tag_set(&edev->tag_set);
if (ret)
goto err_dma;
edev->disk = blk_mq_alloc_disk(&edev->tag_set, edev);
if (IS_ERR(edev->disk)) {
ret = PTR_ERR(edev->disk);
goto err_tagset;
}
edev->disk->major = example_major;
edev->disk->first_minor = 0;
edev->disk->minors = 1;
edev->disk->fops = &example_bdops;
snprintf(edev->disk->disk_name, 32, "exampleblk"); // имя в /dev
set_capacity(edev->disk, NUM_SECTORS); // размер в 512-байтных секторах
ret = add_disk(edev->disk); // с этого момента /dev/exampleblk существует
if (ret)
goto err_disk;
// добавить в начало example_remove(), перед dma_free_coherent()
del_gendisk(edev->disk); // убрать /dev/exampleblk из системы
put_disk(edev->disk);
blk_mq_free_tag_set(&edev->tag_set);
blk_mq_alloc_disk() лишь создаёт структуру диска в памяти — устройство пока не видно снаружи. Именно add_disk() делает его реальным: с этого вызова /dev/exampleblk появляется и на него можно писать через dd if=/dev/zero of=/dev/exampleblk или читать через hexdump /dev/exampleblk. set_capacity() задаётся в 512-байтных секторах — это исторический размер сектора блочного слоя ядра, отсюда пересчёт NUM_SECTORS = DMA_BUF_SIZE / SECTOR_SIZE. Порядок отмены регистрации в remove() зеркален порядку в probe и идёт до освобождения самой памяти: сначала диск исчезает из /dev (del_gendisk), затем освобождается структура диска (put_disk) и tag_set, и только после этого — сама DMA-память, на которую указывал диск.blk_mq_alloc_disk() only creates the disk structure in memory — the device isn't visible externally yet. It's add_disk() that makes it real: from that call on, /dev/exampleblk exists and can be written to via dd if=/dev/zero of=/dev/exampleblk or read via hexdump /dev/exampleblk. set_capacity() is given in 512-byte sectors — the block layer's historical sector size, hence the NUM_SECTORS = DMA_BUF_SIZE / SECTOR_SIZE calculation. The unregistration order in remove() mirrors the order in probe and comes before freeing the memory itself: the disk disappears from /dev first (del_gendisk), then the disk structure (put_disk) and tag_set are freed, and only after that is the DMA memory the disk pointed to actually released.
| командаcommand | что делаетwhat it does |
|---|---|
| lsmod | список загруженных модулей прямо сейчас, с указанием, какие другие модули от каждого зависят (столбец Used by)lists currently loaded modules, showing which other modules depend on each one (the Used by column) |
| modinfo имяname | метаданные конкретного .ko-файла — автор, лицензия, список параметров модуля, таблица поддерживаемых устройств (та самая, что используется для modalias)metadata for a specific .ko file — author, license, the module's parameter list, the supported-device table (the one used for modalias) |
| modprobe имяname | загружает модуль вместе со всеми зависимостями из modules.dep — обычный способ загрузить модуль вручнуюloads a module along with every dependency from modules.dep — the normal way to load a module by hand |
| modprobe -r имяname | выгружает модуль и всё, от чего он зависел, если они больше никем не используются — безопаснее, чем rmmod, который не смотрит на зависимостиunloads a module and everything it depended on, if nothing else is using them — safer than rmmod, which doesn't consider dependencies |
| depmod -a | пересобирает modules.dep для текущей версии ядра — нужно после ручной установки нового модуля вне обычного пакетного менеджераrebuilds modules.dep for the current kernel version — needed after manually installing a new module outside the regular package manager |
# /etc/modules-load.d/my-module.conf
my_module
# проверить, что реально было загружено systemd-modules-load при последнем старте
journalctl -b -u systemd-modules-load.service
# /etc/modprobe.d/blacklist-nouveau.conf
blacklist nouveau
# параметры конкретного модуля — тоже здесь
options nouveau modeset=0
Строка blacklist nouveau в /etc/modprobe.d/ говорит modprobe не грузить модуль автоматически, когда об этом просит udev по событию modalias. Она не мешает явному modprobe nouveau, запущенному вручную, и не мешает загрузке модуля как зависимости другого модуля, которому он нужен. Ожидание, что blacklist гарантированно и при любых условиях уберёт конкретный модуль из системы, — частая причина, почему «заблокированный» видеодрайвер всё равно оказывается загружен.A blacklist nouveau line in /etc/modprobe.d/ tells modprobe not to load the module automatically when udev asks based on a modalias event. It doesn't stop an explicit modprobe nouveau run by hand, and it doesn't stop the module loading as a dependency of another module that needs it. Expecting blacklist to guarantee, under all conditions, that a specific module never ends up loaded is a common reason a "blocked" graphics driver still shows up loaded anyway.
rmmod не проверяет, использует ли модуль что-то ещё, кроме простого счётчика ссылок ядра (который блокирует выгрузку, только если счётчик не нулевой) — но не защищает от ситуаций, где зависимость логическая, а не формально зарегистрированная. modprobe -r безопаснее именно потому, что явно учитывает граф зависимостей из modules.dep, а не полагается на голый счётчик использования.rmmod checks nothing beyond the kernel's simple reference counter (which blocks unloading only if the count is nonzero) — it doesn't protect against situations where the dependency is logical rather than formally registered. modprobe -r is safer precisely because it explicitly consults the dependency graph from modules.dep, rather than relying on a bare usage counter.
Module.symvers фиксирует контрольные суммы версий экспортируемых символов ядра на момент сборки модуля — если структуры данных ядра изменились между версиями (а они меняются регулярно), попытка загрузить старый модуль в новое ядро провалится с ошибкой несовпадения версии. Это не поломанная совместимость, а защита от загрузки кода, который может работать с уже устаревшей раскладкой памяти структур ядра, — из-за этого сторонние модули (например, проприетарные драйверы GPU) нужно пересобирать при каждом обновлении ядра.Module.symvers records version checksums for the kernel's exported symbols at the time the module was built — if the kernel's data structures changed between versions (and they change regularly), trying to load an old module into a new kernel fails with a version mismatch error. That's not broken compatibility — it's a safeguard against loading code that might operate on an already-outdated memory layout of kernel structures — which is exactly why third-party modules (say, proprietary GPU drivers) need rebuilding on every kernel update.
/lib/modules/$(uname -r)/ — привязан к конкретной версии ядра.a loadable kernel module's file, physically living in /lib/modules/$(uname -r)/ — tied to a specific kernel version./etc/modprobe.d/, запрещающая modprobe автоматически загружать конкретный модуль по запросу udev — не мешает явной ручной загрузке и не мешает загрузке как чужой зависимости.an entry in /etc/modprobe.d/ forbidding modprobe from automatically loading a specific module on udev's request — doesn't stop an explicit manual load or a load as someone else's dependency.