Linux Kernel Development
https://www.amazon.com/Linux-Kernel-Development-Robert-Love/dp/0672329468
Introduction to the Linux Kernel
Отличия ядра Linux от классических Unix-систем:
- Линукс поддерживает динамическую загрузку модулей ядра, несмотря на то что ядро монолитное
- Ядро Linux является вытесняющим. Это означает, что даже процессы ядра могут быть остановлены и запущены заново (т.е. у них тоже бывает context switch). В случае не-вытесняющих ядер все процессы в контексте ядра работают, пока не завершается.
- Линукс не делает различий между тредами и процессами. Для Линукса это всё процессы, просто некоторые еще и могут шарить между собой ресурсы.
- Линукс игнорирует некоторые фичи Unix, потому что разработчики Линукса решили что они плохо спроектированы, например, концепт STREAMS, или стандарты, которые невозможно нормально реализовать.
Как сбилдить ядро
Конфигурация
Сначала нужно настроить билд. Самый базовый (но не простой) способ это:
make config
Но он наверняка займет много времени, потому что консольный. Есть псевдографический способ:
make menuconfig
И самый простой - когда автомаитчески подбирается подходящая конфигурация для ващей текущей архитектуры:
make defconfig
Конфигурация сохраняется в корне сорцов в файле .config. Его можно редактировать и вручную, но тогда лучше перед билдом провалидировать конфиг командой:
make oldconfig
Опция CONFIGIKCONFIGPROC кладет конфигурацию билда в /proc/config.gz
. Можно использовать текущую конфигурацию для нового билда вот так:
zcat /proc/config.gz > .config
make oldconfig
Билд
Все просто:
make -j32
Установка
В случае убунты делаем так, сначала устанавливаем модули:
make modules_install
Модули установятся в /lib/modules в папки, соответствующие версии ядра, поэтому можно не бояться сломать текущее состояние ОС.
Затем устанавливаем само ядро:
make install
Эта команда установит файл ядра в /boot
, например, /boot/vmlinuz-5.14.0-rc6+
, а так же сама поправит конфиги grub, чтобы при следующей загрузке машины автоматически загружалось самое последнее ядро. Если мы ходим загружать другое ядро, то после экрана BIOS/UEFI надо 1 раз нажать ESC, и появится меню grub. Это не всегда удобно, поэтому посмотрим как поменять поведение grub при загрузке.
Настройка GRUB
Конфиг grub лежит в /boot/grub/grub.cfg
, но его трогать нельзя. Этот конфиг собирается утилитой update-grub
при модификации файлов в /etc/grub.d
и /etc/default/grub
.
Чтобы включить автопоказ меню grub с выбором версии ядра при загрузке, нужно сделать так:
- Бэкапим
etc/default/grub
в/etc/default/grub.bak
- В
/etc/default/grub
выставляем:
GRUB_TIMEOUT_STYLE=countdown
GRUB_TIMEOUT=5
- Делаем
sudo update-grub
.
Все, теперь при запуске машины в определенный момент на экране появится обратный отсчет 5 секунд, нужно нажать ESC, выбрать Advanced и затем свою версию ядра.
Еще можно поменять GRUB_DEFAULT (туда нужно указать не порядковый номер, а id для menuentry из grub.cfg), но у меня почему-то не вышло.
И еще можно автоматически запускать ту же версию, что и в прошлый раз (я пока не пробовал):
GRUB_DEFAULT=saved
GRUB_SAVEDEFAULT=true
Менеджмент процессов
Новый процесс создается системным вызовов fork()
. Если нужно запустить другую программу, то новый поток сразу после этого запускает exec()
. Этот вызов создает новое адресное пространство и загружает в него программу. В совр еменных ядрах Линукса fork()
реализован через вызов clone()
.
По завершении программа вызывает exit()
, который завершает процесс и освобождает все ресурсы. Родительский процесс дожидается окончания дочернего вызовом wait4()
. Когда процесс завершается, он становится зомби, пока родитель не вызовет wait()
или waitpid()
.
Разница между wait()
и wait4()
в том, что ядро реализует только wait4()
, а все остальные wait()
, waitpid()
, wait3()
, и собственно wait4()
это функции С, т.е. врапперы над ядерным wait4()
. Ну и еще некоторые отличия по семантике есть, wait4()
возращает некоторую статистику по процессу, а wait()
- нет.
Список процессов хранится в зацикленном двойном связанном списке task list. Каждый элемент списка это дескриптор процессе, описываемый структурой task_struct
(см. <linux/sched.h>
). У каждого потока в конце стэка лежит структура thread_info
, которая ссылается на дескриптор процесса task_struct.
thread_info
описан в arch/x86/include/asm/thread_info.h
.
Шедулинг процессов
Мультитаскинг бывает кооперативный, а бывает вытесняющий (preemptive).
Кооперативный - процессы сами решают, когда им передать управление другим процессам (это называется yielding).
Вытесняющий - шедулер решает, останавливает и возобновляет процессы.
До версии 2.5 в линуксе был какой-то совсем простой шедулер, обычно называемый O(n) scheduler. O(n) означает, что для выбора следующего процесса шедулеру приходилось просматривать все запущенные процессы. Соответственно с накоплением количества процессов переключение происходило все медленнее.
В 2.6 O(n) scheduler заменен на O(1) scheduler. Он, соответственно, шедулит процессы всегда за константное время. Его основной проблемой были сложные эвристики, при помощи которых он пытался распознать интерактивные процессы, чтобы возобновлять их с меньшей задержкой. Сложность эвристик неизбежно приводила к ошибкам и интерактивные приложения все равно тормозили.
В 2.6.23 на смену пришел CFS - Completely Fair Scheduler. https://developer.ibm.com/tutorials/l-completely-fair-scheduler/
Linux scheduler algorithm
Все процессы делятся на разные типы и для разных типов могут работать разные алгоритмы, так называемые scheduler classes. Каждый класс шедулера имеет приоритет, а основной код шедулера в каждой итерации выбирает тот шедулер, который имеет наивысший приоритет и работающий процесс. Выбранный шедулер и решает, кто будет запущен следующим.
CFS - это класс шедулера для процессов с приоритетом SCHED_NORMAL. Он описан в kernel/sched/fair.c
.
Чтобы помнить, какой процесс сколько уже проработал, CFS использует структуру sched_entity
(<linux/sched.h>
). В дескрипторе процесса (task_struct
) эта структура находится в поле se
.
Одно из полей sched_entity
- vruntime. Это время работы процесса, нормализованное по количеству процессов. Измеряется в наносекундах. Цель шедулера - расшедулить так, чтобы vruntime у всех процессов был одинаковый. Для этого CFS поддерживает красно-черное дерево процессов и выбирает всегда тот, у которого наименьший vruntime.