Уроки D2-D7 (14 файлов): деревья/куча, epoll, потоки, gdb, графы, L2-L3, ДП, TCP, bash, ядро, docker, мок-интервью + cards_src 613
This commit is contained in:
@@ -0,0 +1,380 @@
|
||||
# D6, часть 1. Ядро и embedded обзорно (урок)
|
||||
|
||||
Уровень этого урока — «уверенно отвечать словами на собеседовании», а не «написать драйвер
|
||||
с нуля». Идём от границы user space/kernel space к модулю, от модуля к драйверу символьного
|
||||
устройства, от драйвера к железу (device tree, cross-compile, загрузка платы). Опорные
|
||||
источники для самостоятельного углубления: docs.kernel.org, The Linux Kernel Module
|
||||
Programming Guide (LKMPG), `man 2`/`man 3`/`man 7` (системные вызовы, библиотечные функции,
|
||||
конвенции ядра).
|
||||
|
||||
## 1. User space vs kernel space и системный вызов как переход границы
|
||||
|
||||
Процессор x86 поддерживает уровни привилегий (кольца защиты): **кольцо 0** — режим ядра,
|
||||
полный доступ к железу и памяти; **кольцо 3** — режим пользователя, обычные программы, без
|
||||
прямого доступа к физической памяти чужих процессов или портам ввода-вывода. Ядро работает в
|
||||
кольце 0, все обычные процессы — в кольце 3. Разделение существует ради защиты и стабильности:
|
||||
если бы любая программа могла напрямую писать в память другого процесса или в регистры
|
||||
диска, ошибка или злой умысел в одной программе обрушивали бы всю систему.
|
||||
|
||||
Чтобы попросить ядро что-то сделать (открыть файл, выделить память, создать процесс),
|
||||
пользовательская программа не может просто вызвать функцию ядра — она делает **системный
|
||||
вызов**: специальную инструкцию процессора, которая переключает CPU в привилегированный режим
|
||||
и передаёт управление фиксированному обработчику в ядре, а после выполнения запроса управление
|
||||
возвращается программе обратно в непривилегированном режиме. На x86-64 эту инструкцию зовут
|
||||
**`syscall`**, на ARM — **`svc`** (supervisor call). Оба случая — не обычный вызов функции
|
||||
(`call`), а специальная инструкция именно потому, что она обязана сменить уровень привилегий
|
||||
процессора, а не просто передать управление по адресу.
|
||||
|
||||
**Факты для карточек**
|
||||
- base | В каком кольце защиты x86 работает ядро Linux? — в кольце 0 (пользовательские процессы — в кольце 3)
|
||||
- base | Какая инструкция делает системный вызов на x86-64? — `syscall` (на ARM — `svc`)
|
||||
- core | Почему системный вызов — отдельная инструкция процессора, а не обычный `call`? — обычный `call` не меняет уровень привилегий CPU, а переход в кольцо 0 требует именно смены режима процессора
|
||||
|
||||
Почему дальше: код, работающий в кольце 0, можно добавлять в ядро двумя разными способами —
|
||||
разберём, чем модуль ядра отличается от кода, встроенного в ядро на этапе сборки.
|
||||
|
||||
## 2. Модуль ядра vs встроенный в ядро
|
||||
|
||||
Функциональность можно либо **встроить в ядро** на этапе сборки (код компилируется прямо в
|
||||
образ ядра, доступен сразу при загрузке, но требует пересборки и перезагрузки при любом
|
||||
изменении), либо оформить как **загружаемый модуль ядра** (`.ko`-файл, подключается и
|
||||
отключается в работающей системе без перезагрузки). Встроенным делают то, что нужно с самого
|
||||
первого момента загрузки (например, драйвер корневой файловой системы); модулем — то, что
|
||||
может понадобиться позже или не понадобиться вовсе (драйвер конкретного периферийного
|
||||
устройства), чтобы не раздувать образ ядра и не грузить лишний код.
|
||||
|
||||
Инструменты управления модулями:
|
||||
- **`insmod <path.ko>`** — загружает модуль по прямому пути к файлу, без разрешения
|
||||
зависимостей от других модулей.
|
||||
- **`rmmod <name>`** — выгружает модуль по имени (если счётчик использования равен нулю).
|
||||
- **`modprobe <name>`** — загружает модуль по имени, сам находит файл в
|
||||
`/lib/modules/$(uname -r)/` и подгружает зависимости по карте `modules.dep` (строится
|
||||
утилитой `depmod`) — на практике используется чаще `insmod`, именно из-за автоматических
|
||||
зависимостей.
|
||||
- **`lsmod`** — список загруженных сейчас модулей (по сути читает `/proc/modules`), с
|
||||
размером и счётчиком использования.
|
||||
- **`modinfo <name|path>`** — метаданные модуля (лицензия, описание, параметры, зависимости)
|
||||
без его загрузки в ядро.
|
||||
|
||||
**Факты для карточек**
|
||||
- base | Чем `insmod` отличается от `modprobe`? — `insmod` грузит модуль по прямому пути без разрешения зависимостей, `modprobe` находит модуль по имени и сам подгружает зависимости
|
||||
- base | Какая команда показывает метаданные модуля, не загружая его? — `modinfo`
|
||||
- core | Откуда `modprobe` берёт карту зависимостей модулей? — из файла `modules.dep`, который строит утилита `depmod`
|
||||
|
||||
Почему дальше: раз модуль — это отдельно загружаемый код, у него должна быть точка входа и
|
||||
точка выхода из ядра — разберём структуру простейшего модуля.
|
||||
|
||||
## 3. Структура простейшего модуля
|
||||
|
||||
```c
|
||||
#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 — успех; ненулевой код отменяет загрузку модуля
|
||||
}
|
||||
|
||||
static void __exit hello_exit(void)
|
||||
{
|
||||
printk(KERN_INFO "hello: module unloaded\n");
|
||||
}
|
||||
|
||||
module_init(hello_init); // вызывается ядром при insmod/modprobe
|
||||
module_exit(hello_exit); // вызывается ядром при rmmod
|
||||
MODULE_LICENSE("GPL");
|
||||
```
|
||||
|
||||
`module_init`/`module_exit` — не обычные вызовы функций, а макросы, которые регистрируют
|
||||
функции как точки входа/выхода: сам код внутри них ядро вызывает автоматически в момент
|
||||
`insmod`/`rmmod`, программист их напрямую не зовёт. `MODULE_LICENSE("GPL")` обязателен: без
|
||||
него или с иной лицензией ядро помечает себя как **tainted** (загрязнённое) и закрывает
|
||||
модулю доступ к символам, экспортированным только для GPL-кода (`EXPORT_SYMBOL_GPL`).
|
||||
|
||||
`printk` — это `printf` уровня ядра, пишет не в консоль процесса, а в кольцевой буфер ядра.
|
||||
Первым аргументом обычно идёт уровень важности — восемь уровней от `KERN_EMERG` (0,
|
||||
критично) до `KERN_DEBUG` (7, отладочная информация); сообщения с уровнем выше порога
|
||||
консоли попадают только в буфер, но не печатаются на экран сразу. Прочитать буфер целиком —
|
||||
команда **`dmesg`**.
|
||||
|
||||
**Факты для карточек**
|
||||
- base | Какой макрос ядра — аналог `printf`? — `printk`
|
||||
- base | Команда для чтения буфера сообщений ядра? — `dmesg`
|
||||
- core | Сколько уровней важности у `printk` и какие крайние? — 8 уровней, от `KERN_EMERG` (0) до `KERN_DEBUG` (7)
|
||||
- core | Что произойдёт с модулем без `MODULE_LICENSE("GPL")`? — ядро станет tainted и закроет модулю доступ к символам `EXPORT_SYMBOL_GPL`
|
||||
- core | Кто вызывает функции, зарегистрированные `module_init`/`module_exit`? — ядро автоматически, при `insmod`/`modprobe` и `rmmod` соответственно, а не сам программист
|
||||
|
||||
Почему дальше: модулю часто нужно принимать настройки снаружи ещё до его собственной логики —
|
||||
разберём, как модуль объявляет параметры запуска.
|
||||
|
||||
## 4. Параметры модуля: `module_param`
|
||||
|
||||
```c
|
||||
static int count = 1;
|
||||
module_param(count, int, S_IRUGO); // третий аргумент — права доступа в sysfs
|
||||
```
|
||||
|
||||
`module_param(имя, тип, права)` (заголовок `<linux/moduleparam.h>`) делает переменную
|
||||
настраиваемой при загрузке модуля — значение можно передать прямо в `insmod modname.ko
|
||||
count=5`. Третий аргумент — режим доступа файла в sysfs: `0` означает, что параметр доступен
|
||||
только при загрузке и не публикуется отдельным файлом, ненулевое значение (например
|
||||
`S_IRUGO` — чтение всем) создаёт файл `/sys/module/<имя_модуля>/parameters/<count>`, из
|
||||
которого параметр можно прочитать (а при подходящих правах — и переписать) уже после загрузки.
|
||||
|
||||
**Факты для карточек**
|
||||
- base | Как передать параметр модулю при загрузке? — `insmod modname.ko имя_параметра=значение`
|
||||
- core | Что означает третий аргумент `module_param`, если он ненулевой? — параметр публикуется файлом в `/sys/module/<имя>/parameters/<имя>` с заданными правами доступа
|
||||
|
||||
Почему дальше: файл параметра в sysfs — частный случай общего механизма, которым ядро вообще
|
||||
разговаривает с пользовательским пространством через файловую систему, — `/proc` и `/sys`.
|
||||
|
||||
## 5. `/proc` и `/sys` как интерфейс к ядру
|
||||
|
||||
**`/proc`** (procfs) — виртуальная файловая система, изначально созданная для информации о
|
||||
процессах (`/proc/<pid>/...`), позже расширенная общей информацией о ядре (`/proc/cpuinfo`,
|
||||
`/proc/meminfo`, `/proc/modules`); файлы внутри часто содержат несколько значений свободным
|
||||
текстом. **`/sys`** (sysfs, с ядра 2.6) — более новый и структурированный интерфейс,
|
||||
отражающий модель устройств и драйверов ядра (шины, устройства, классы), с соглашением «один
|
||||
файл — одно значение», что упрощает и чтение скриптами, и программную запись настроек. Оба
|
||||
это не диски, а генерируются ядром на лету при каждом обращении — `cat /proc/meminfo`
|
||||
формирует ответ в момент чтения, а не читает файл с диска.
|
||||
|
||||
**Факты для карточек**
|
||||
- base | Чем отличается соглашение о содержимом файлов `/sys` от `/proc`? — в `/sys` одно значение на файл, в `/proc` файл может содержать несколько значений свободным текстом
|
||||
- core | Откуда `cat /proc/meminfo` берёт данные? — ядро формирует ответ на лету в момент чтения, это не файл на диске
|
||||
|
||||
Почему дальше: `/proc` и `/sys` — интерфейсы общего назначения; когда нужно управлять
|
||||
конкретным устройством операциями `open`/`read`/`write`, пишут символьный драйвер.
|
||||
|
||||
## 6. Символьный драйвер: `file_operations`, major/minor, барьер user/kernel
|
||||
|
||||
Драйвер символьного устройства регистрирует набор функций-обработчиков в структуре
|
||||
`struct file_operations` — как минимум `.open`, `.read`, `.write`, `.release` (плюс,
|
||||
например, `.unlocked_ioctl`); ядро вызывает нужный обработчик, когда пользовательский процесс
|
||||
делает соответствующий системный вызов над файлом устройства в `/dev`. Регистрация —
|
||||
`register_chrdev(major, name, &fops)`: если `major` передан как `0`, ядро само подбирает
|
||||
свободный старший номер.
|
||||
|
||||
Устройство идентифицируется парой **major:minor**: **major** (старший номер) определяет
|
||||
драйвер, обслуживающий устройство, **minor** (младший) — конкретный экземпляр внутри этого
|
||||
драйвера (например, второй последовательный порт того же типа). `dev_t` в современном ядре —
|
||||
32-битное число: 12 бит под major (до 4095) и 20 бит под minor (до 1 048 575).
|
||||
|
||||
Внутри `.read`/`.write` драйвер **не может напрямую разыменовать указатель, пришедший из
|
||||
пользовательского пространства** — это чужое адресное пространство, страница может быть не
|
||||
загружена в память или указатель вообще некорректен, а прямое разыменование либо уронит
|
||||
ядро (kernel oops), либо (на CPU с SMAP/SMEP) вызовет аппаратный запрет. Вместо этого
|
||||
обязательны **`copy_to_user`**/**`copy_from_user`** — они безопасно копируют данные через
|
||||
границу, сами обрабатывают отсутствующую страницу и возвращают число байт, которые
|
||||
**не** удалось скопировать (0 — полный успех).
|
||||
|
||||
**Ловушки**
|
||||
- Разыменовать пользовательский указатель напрямую в `.read`/`.write` → крах ядра (oops) или
|
||||
аппаратный запрет на SMAP/SMEP-системах → видно как немедленный крэш при обращении к
|
||||
устройству, а не как «иногда неверные данные».
|
||||
- Забыть проверить возвращаемое значение `copy_to_user`/`copy_from_user` → часть данных не
|
||||
скопирована, а код считает операцию успешной → приложение получает частично мусорный буфер
|
||||
без явной ошибки.
|
||||
|
||||
**Факты для карточек**
|
||||
- base | Какие 4 обработчика минимально нужны в `file_operations` символьного драйвера? — `.open`, `.read`, `.write`, `.release`
|
||||
- core | Что означают major и minor номера устройства? — major определяет драйвер, minor — конкретный экземпляр устройства внутри этого драйвера
|
||||
- core | Сколько бит под major и minor в `dev_t`? — 12 бит major, 20 бит minor (32-битное число целиком)
|
||||
- core | Почему в драйвере нельзя напрямую разыменовать указатель из user space? — это чужое адресное пространство, страница может быть не загружена или указатель некорректен; прямое разыменование роняет ядро или блокируется SMAP/SMEP
|
||||
|
||||
Почему дальше: `read`/`write` подходят для потока байт, но не для команд, которые не
|
||||
укладываются в чтение/запись (настроить режим устройства, запросить статус) — для этого есть
|
||||
`ioctl`.
|
||||
|
||||
## 7. `ioctl`: команды, которые не являются чтением или записью
|
||||
|
||||
`ioctl` — обработчик `.unlocked_ioctl` в `file_operations`, принимающий числовой код команды
|
||||
и один аргумент (`unsigned long`, часто указатель на структуру в user space). Используется,
|
||||
когда операция над устройством не укладывается в модель «поток байт» — например, «сообщи
|
||||
текущую скорость порта» или «переведи устройство в другой режим». Коды команд собирают
|
||||
макросами **`_IO`**, **`_IOR`**, **`_IOW`**, **`_IOWR`** (кодируют направление передачи данных,
|
||||
«магическое число» драйвера и номер команды) — это соглашение снижает риск, что два разных
|
||||
драйвера случайно используют одинаковый числовой код команды.
|
||||
|
||||
**Факты для карточек**
|
||||
- base | В какой функции `file_operations` реализуется `ioctl`? — `.unlocked_ioctl`
|
||||
- core | Зачем коды ioctl-команд собирают через `_IO`/`_IOR`/`_IOW`/`_IOWR`, а не пишут произвольным числом? — макросы кодируют направление передачи, магическое число драйвера и номер команды, снижая риск коллизии кодов между разными драйверами
|
||||
|
||||
Почему дальше: и `file_operations`, и `ioctl` работают с уже существующим, известным
|
||||
устройством — а откуда ядро вообще узнаёт, какое железо есть на конкретной плате, особенно в
|
||||
embedded, где плат много и они разные?
|
||||
|
||||
## 8. Device tree: описание железа без хардкода
|
||||
|
||||
**Device tree** — текстовое описание аппаратной конфигурации платы (`.dts`, компилируется
|
||||
утилитой `dtc` в бинарный `.dtb`): адреса регистров периферии, линии прерываний, доступные
|
||||
шины, строки `compatible`, по которым ядро сопоставляет узел дерева с подходящим драйвером.
|
||||
Загрузчик передаёт `.dtb` ядру вместе с образом ядра при старте.
|
||||
|
||||
Смысл — один и тот же бинарник ядра должен уметь работать на разных платах с разной
|
||||
периферией и разными адресами регистров без перекомпиляции под каждую плату: без device tree
|
||||
адреса и конфигурация железа были бы зашиты прямо в код ядра (так называемые board files),
|
||||
и под каждую новую плату требовалась бы правка и пересборка самого ядра. Device tree выносит
|
||||
это описание из кода в данные, которые загрузчик просто подкладывает рядом с ядром.
|
||||
|
||||
**Факты для карточек**
|
||||
- base | Во что компилируется `.dts` и какой утилитой? — в бинарный `.dtb`, утилитой `dtc`
|
||||
- core | Зачем device tree вообще нужен, если можно было бы прописать адреса регистров прямо в коде драйвера? — один и тот же бинарник ядра работает на разных платах без пересборки под каждую; хардкод адресов требовал бы правки и компиляции ядра под каждую конкретную плату
|
||||
|
||||
Почему дальше: чтобы вообще собрать ядро (и модуль) под плату, у которой процессор отличается
|
||||
от машины разработчика, обычную сборку компилятором хоста использовать нельзя — нужен
|
||||
кросс-компилятор.
|
||||
|
||||
## 9. Cross-compile: тулчейн под целевую архитектуру
|
||||
|
||||
Сборка ведётся на машине разработчика (обычно x86-64), а результат должен исполняться на
|
||||
целевом процессоре платы (например, ARM) — обычный компилятор хоста генерирует машинный код
|
||||
под архитектуру хоста, целевая плата такой код исполнить не сможет. Нужен **кросс-компилятор**
|
||||
— тулчейн, генерирующий код именно под целевую архитектуру: например `arm-linux-gnueabihf-gcc`.
|
||||
|
||||
- **`CROSS_COMPILE`** — переменная окружения/аргумент сборки (используется, например, в
|
||||
Makefile ядра и Buildroot), задающая префикс имени инструментов тулчейна
|
||||
(`CROSS_COMPILE=arm-linux-gnueabihf-`), чтобы вызывался `arm-linux-gnueabihf-gcc`, а не
|
||||
системный `gcc`.
|
||||
- **`-march`** — флаг компилятора, задающий конкретный набор инструкций целевого процессора
|
||||
(например `-march=armv7-a`); несовпадение с реальным железом даёт крах «illegal
|
||||
instruction» на плате или отказ собраться, если код использует расширения, которых у
|
||||
целевого процессора нет.
|
||||
- **Статическая сборка** — линковка всех библиотек прямо в бинарник вместо динамических
|
||||
`.so`; в embedded это снимает риск несовпадения версии библиотек (например, glibc) в
|
||||
минимальном корневом ФС платы с версией, под которую собирался бинарник, ценой большего
|
||||
размера самого файла.
|
||||
|
||||
**Факты для карточек**
|
||||
- base | Что задаёт переменная `CROSS_COMPILE`? — префикс имени инструментов тулчейна (например `arm-linux-gnueabihf-`)
|
||||
- core | Что произойдёт при запуске бинарника, собранного с неверным `-march`, на реальной плате? — крах «illegal instruction» (процессор не поддерживает часть использованных инструкций) или отказ сборки
|
||||
- core | Какую проблему в embedded снимает статическая линковка? — несовпадение версии динамических библиотек (например glibc) на целевой плате с версией сборки
|
||||
|
||||
Почему дальше: тулчейн даёт бинарники ядра и модулей под плату, но сама плата должна ещё
|
||||
дойти от включения питания до работающей системы — разберём цепочку загрузки.
|
||||
|
||||
## 10. Загрузка платы: u-boot → ядро+dtb → initramfs → init
|
||||
|
||||
Типичная последовательность:
|
||||
1. Boot ROM процессора (зашит в кристалл, неизменяем) загружает первый этап загрузчика.
|
||||
2. **U-Boot** — распространённый загрузчик embedded-плат — инициализирует минимально
|
||||
необходимое железо (память, консоль) и находит образ ядра и `.dtb` (раздел 8) на носителе.
|
||||
3. U-Boot загружает **ядро** и **`.dtb`** в память и передаёт управление точке входа ядра.
|
||||
4. Ядро инициализируется и монтирует **initramfs** — временную корневую файловую систему,
|
||||
целиком находящуюся в оперативной памяти, — и запускает из неё `/init`.
|
||||
5. `/init` в initramfs делает раннюю настройку (загружает нужные модули, находит настоящий
|
||||
диск), затем переключается на настоящую корневую файловую систему (`pivot_root`/
|
||||
`switch_root`) и запускает уже настоящий **init** (PID 1 — например `systemd` или
|
||||
BusyBox init) на постоянном разделе.
|
||||
|
||||
initramfs нужен потому, что к моменту, когда ядро только загрузилось, оно ещё может не знать,
|
||||
как смонтировать настоящий диск (нужный драйвер файловой системы или контроллера диска может
|
||||
сам быть модулем, который ещё не загружен) — временная ФС в памяти даёт минимальную среду,
|
||||
достаточную, чтобы загрузить эти модули и уже потом перейти на постоянное хранилище.
|
||||
|
||||
**Факты для карточек**
|
||||
- base | В каком порядке идёт цепочка загрузки платы? — Boot ROM → U-Boot → ядро+dtb → initramfs → переключение на настоящий rootfs → init (PID 1)
|
||||
- core | Зачем нужен initramfs, если ядро уже загружено и работает? — ядру для монтирования настоящего диска может понадобиться драйвер, который сам является модулем и ещё не загружен; initramfs даёт минимальную среду в памяти, чтобы его подгрузить перед переходом на постоянный rootfs
|
||||
|
||||
Почему дальше: после того как система загрузилась, embedded-разработчик работает с реальным
|
||||
железом напрямую через память — а компилятор по умолчанию считает, что содержимое памяти
|
||||
меняется только его собственным кодом. Разберём, что с этим не так.
|
||||
|
||||
## 11. `volatile`, `mmap` регистров и барьеры памяти
|
||||
|
||||
Обычная оптимизация компилятора — закэшировать значение переменной в регистре процессора и не
|
||||
перечитывать его из памяти повторно, если по коду программы оно «не могло измениться».
|
||||
Регистр памяти-отображённого устройства (MMIO) может измениться сам, независимо от кода
|
||||
программы (например, статус-регистр меняется самим железом) — без `volatile` компилятор
|
||||
законно уберёт «повторное» чтение как избыточное, и драйвер будет вечно видеть устаревшее
|
||||
значение. `volatile` заставляет компилятор реально выполнять каждое обращение к памяти по
|
||||
этому адресу, не кэшируя и не убирая «дублирующиеся» чтения/записи.
|
||||
|
||||
Важная ловушка: `volatile` **не даёт атомарности и не расставляет барьеры памяти** — он
|
||||
касается только компилятора и только одной переменной, а не порядка операций между
|
||||
несколькими адресами и не защищает от гонки между несколькими ядрами процессора (для этого в
|
||||
пользовательском C++ есть `std::atomic`, раздел про многопоточность).
|
||||
|
||||
Доступ к регистрам устройства из драйвера обычно идёт не через прямой физический адрес, а
|
||||
через **`ioremap()`** — функция ядра, отображающая физический адрес MMIO-региона в виртуальное
|
||||
адресное пространство ядра, после чего к регистру обращаются как к обычному указателю.
|
||||
|
||||
**Барьеры памяти** (`mb()`, `rmb()`, `wmb()`) принудительно фиксируют порядок операций чтения
|
||||
и записи в памяти, как его видит железо — нужны потому, что и компилятор, и сам процессор
|
||||
вправе переставлять инструкции местами, если это не меняет результат с точки зрения
|
||||
однопоточной программы, а DMA-контроллер или другое устройство читает память независимо от
|
||||
этого порядка. Типичный случай: драйвер записывает данные в дескриптор DMA-кольца
|
||||
(`tasks/03_ring`, задача этого набора про кольцевой буфер — та же структура head/tail лежит
|
||||
в основе DMA-колец), а затем «звонит в дверной звонок» — пишет в регистр, запускающий
|
||||
устройство; без `wmb()` между этими двумя записями устройство может увидеть сигнал запуска
|
||||
раньше, чем реально записанные данные дескриптора, и прочитать старое содержимое.
|
||||
|
||||
**Ловушки**
|
||||
- Забыть `volatile` на указателе на регистр устройства → компилятор кэширует значение в
|
||||
регистре CPU и не видит изменений железа → драйвер «зависает», читая одно и то же старое
|
||||
значение, хотя устройство давно сменило статус.
|
||||
- Считать, что `volatile` защищает от гонки между несколькими ядрами процессора → в
|
||||
многоядерном коде это не так, нужны барьеры памяти или атомики → гонка, не ловится
|
||||
однопоточным тестированием.
|
||||
|
||||
**Факты для карточек**
|
||||
- base | Что делает `volatile` с точки зрения компилятора? — заставляет реально выполнять каждое обращение к памяти по адресу, не кэшируя значение в регистре и не убирая повторные чтения/записи
|
||||
- core | Даёт ли `volatile` атомарность или барьер памяти между несколькими ядрами CPU? — нет, только запрещает компилятору кэшировать/убирать обращения к конкретной переменной
|
||||
- core | Какая функция ядра отображает физический адрес MMIO-региона в виртуальный адрес для доступа как к указателю? — `ioremap()`
|
||||
- deep | Зачем нужен `wmb()` между записью DMA-дескриптора и записью в регистр запуска устройства? — без барьера порядок этих двух записей, как его видит железо, не гарантирован, и устройство может прочитать старое содержимое дескриптора раньше, чем увидит новые данные
|
||||
|
||||
Ссылки на задачи этого набора: `tasks/07_epoll` — то, как ядро уведомляет процесс о готовых
|
||||
событиях на файловых дескрипторах (тот же принцип «пользовательский процесс не опрашивает
|
||||
железо/сеть напрямую, а получает уведомление от ядра», что и в syscall-границе раздела 1);
|
||||
`tasks/03_ring` — кольцевой буфер head/tail, структурно совпадающий с DMA-кольцами в
|
||||
драйверах (раздел 11).
|
||||
|
||||
<details>
|
||||
<summary>Проверь себя</summary>
|
||||
|
||||
1. Почему нельзя просто разыменовать указатель, пришедший из пользовательской программы,
|
||||
прямо внутри `.read` драйвера?
|
||||
<details><summary>Ответ</summary>Это указатель в чужом (пользовательском) адресном
|
||||
пространстве: соответствующая страница памяти может быть не загружена или указатель
|
||||
вообще некорректен. Прямое разыменование либо роняет ядро (oops), либо блокируется
|
||||
аппаратно на CPU с SMAP/SMEP. Нужно использовать `copy_from_user`/`copy_to_user`, которые
|
||||
безопасно обрабатывают этот переход.</details>
|
||||
|
||||
2. На плате обновили дистрибутив ядра, но модуль устройства продолжает работать без
|
||||
пересборки под новую хардкод-конфигурацию регистров. За счёт какого механизма это
|
||||
возможно и что бы сломалось без него?
|
||||
<details><summary>Ответ</summary>Device tree: адреса регистров и конфигурация железа
|
||||
вынесены в `.dtb`, отдельный от кода ядра, и подгружаются загрузчиком при старте. Без
|
||||
device tree адреса были бы зашиты в код драйвера (board files), и под каждое изменение
|
||||
конфигурации платы пришлось бы переписывать и пересобирать сам код ядра.</details>
|
||||
|
||||
3. Драйвер читает статус-регистр устройства в цикле, ожидая, пока значение изменится, но
|
||||
переменная объявлена без `volatile` — цикл не завершается, хотя железо статус давно
|
||||
сменило. В чём причина и как это чинится?
|
||||
<details><summary>Ответ</summary>Компилятор решил, что раз в теле цикла переменная кодом
|
||||
программы не изменяется, можно прочитать её из памяти один раз и дальше сверяться с
|
||||
закэшированным в регистре значением — оптимизация, законная для обычной переменной, но
|
||||
ломающая логику для регистра, который меняет само железо. Нужно объявить указатель на
|
||||
регистр как `volatile`, чтобы компилятор перечитывал память при каждой
|
||||
итерации.</details>
|
||||
|
||||
4. Почему для системного вызова на x86-64 нужна специальная инструкция `syscall`, а не
|
||||
обычный вызов функции ядра через `call` по известному адресу?
|
||||
<details><summary>Ответ</summary>Обычный `call` не меняет уровень привилегий процессора —
|
||||
пользовательская программа в кольце 3 так и осталась бы в кольце 3, не получив доступа к
|
||||
привилегированным операциям ядра. `syscall` — специальная инструкция, которая одновременно
|
||||
переключает CPU в кольцо 0 и передаёт управление фиксированному обработчику ядра, что
|
||||
обычный переход по адресу сделать не может.</details>
|
||||
|
||||
</details>
|
||||
|
||||
## Материалы
|
||||
|
||||
- sysprog21.github.io, Linux Kernel Module Programming Guide (LKMPG) — https://sysprog21.github.io/lkmpg/
|
||||
- docs.kernel.org, sysfs — https://docs.kernel.org/filesystems/sysfs.html
|
||||
- docs.kernel.org, "volatile" Considered Harmful — https://docs.kernel.org/process/volatile-considered-harmful.html
|
||||
- man7.org, `syscall(2)` — https://man7.org/linux/man-pages/man2/syscall.2.html
|
||||
- man7.org, `ioctl(2)` — https://man7.org/linux/man-pages/man2/ioctl.2.html
|
||||
- man7.org, `proc(5)` — https://man7.org/linux/man-pages/man5/proc.5.html
|
||||
Reference in New Issue
Block a user