How's that again?

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 с выбором версии ядра при загрузке, нужно сделать так:

  1. Бэкапим etc/default/grub в /etc/default/grub.bak
  2. В /etc/default/grub выставляем:
GRUB_TIMEOUT_STYLE=countdown
GRUB_TIMEOUT=5
  1. Делаем 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.