Как вы можете понять из названия, в этой части будет рассмотрена интересная и важная концепция ядра Linux, которая называется initcall
. Мы уже видели такие определения:
early_param("debug", debug_kernel);
или
arch_initcall(init_pit_clocksource);
в некоторых частях ядра Linux. Прежде чем мы увидим, как этот механизм реализован в ядре Linux, мы должны знать, что это такое и как его использует ядро Linux. Подобные определения представляют собой функцию обратного вызова, которая будет вызываться сразу после инициализации ядра Linux. Собственно, основной задачей механизма initcall
является определение правильного порядка инициализации встроенных модулей и подсистем. Например, давайте посмотрим на следующую функцию:
static int __init nmi_warning_debugfs(void)
{
debugfs_create_u64("nmi_longest_ns", 0644,
arch_debugfs_dir, &nmi_longest_ns);
return 0;
}
из файла исходного кода arch/x86/kernel/nmi.c. Как мы видим, он просто создает файл nmi_longest_ns
debugfs в каталоге arch_debugfs_dir
. На самом деле этот файл debugfs
может быть создан только после того, как будет создан arch_debugfs_dir
. Создание этого каталога происходит во время инициализации ядра Linux в зависимости от архитектуры. На самом деле этот каталог будет создан функцией arch_kdebugfs_init
из файла [arch/x86/kernel/kdebugfs.c](https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/kernel/kdebugfs. в) файл исходного кода. Обратите внимание, что функция arch_kdebugfs_init
также помечена как initcall
:
arch_initcall(arch_kdebugfs_init);
Ядро Linux вызывает все специфичные для архитектуры initcalls
перед initcalls
, связанными с fs
. Итак, наш файл nmi_longest_ns
будет создан только после создания каталога arch_kdebugfs_dir
. На самом деле ядро Linux предоставляет восемь уровней основных initcalls
вызовов:
early
;core
;postcore
;arch
;subsys
;fs
;device
;late
.
Все их имена представлены массивом initcall_level_names
, который определен в файле исходного кода init/main.c:
static char *initcall_level_names[] __initdata = {
"early",
"core",
"postcore",
"arch",
"subsys",
"fs",
"device",
"late",
};
Все функции, помеченные этими идентификаторами как initcall
, будут вызываться в том же порядке, либо сначала будут вызываться ранние initcall
, потом core initcalls
и т.д. С этого момента мы немного знаем о initcall
механизме, поэтому мы можем начать погружаться в исходный код ядра Linux, чтобы увидеть, как реализован этот механизм.
Ядро Linux предоставляет набор макросов из заголовочного файла include/linux/init.h для маркировки заданного функционировать как initcall
. Все эти макросы довольно просты:
#define early_initcall(fn) __define_initcall(fn, early)
#define core_initcall(fn) __define_initcall(fn, 1)
#define postcore_initcall(fn) __define_initcall(fn, 2)
#define arch_initcall(fn) __define_initcall(fn, 3)
#define subsys_initcall(fn) __define_initcall(fn, 4)
#define fs_initcall(fn) __define_initcall(fn, 5)
#define device_initcall(fn) __define_initcall(fn, 6)
#define late_initcall(fn) __define_initcall(fn, 7)
и, как мы видим, эти макросы просто расширяются до вызова макроса __define_initcall
из того же заголовочного файла. Более того, макрос __define_initcall
принимает два аргумента:
fn
- функция обратного вызова, которая будет вызываться при вызовеinitcalls
определенного уровня;id
— идентификатор для идентификацииinitcall
, чтобы предотвратить ошибку, когда два одинаковыхinitcall
указывают на один и тот же обработчик.
Реализация макроса __define_initcall
выглядит так:
#define __define_initcall(fn, id) \
static initcall_t __initcall_##fn##id __used \
__attribute__((__section__(".initcall" #id ".init"))) = fn; \
LTO_REFERENCE_INITCALL(__initcall_##fn##id)
Чтобы понять макрос __define_initcall
, прежде всего давайте посмотрим на тип initcall_t
. Этот тип определен в том же файле header и представляет собой указатель на функцию, которая возвращает указатель на integer, который будет результатом initcall
:
typedef int (*initcall_t)(void);
Теперь вернемся к макросу _-define_initcall
. ## обеспечивает возможность объединения двух символов. В нашем случае первая строка макроса __define_initcall
создает определение данной функции, которая находится в .initcall id .init
[раздел ELF](http://www.skyfree.org/linux/references/ ELF_Format.pdf) и отмечен следующими атрибутами gcc: __initcall_function_name_id
и __used
. Если мы посмотрим в заголовочный файл include/asm-generic/vmlinux.lds.h который представляет данные для скрипта ядра linker, мы увидим, что все разделы initcalls
будут помещены в раздел .data
:
#define INIT_CALLS \
VMLINUX_SYMBOL(__initcall_start) = .; \
*(.initcallearly.init) \
INIT_CALLS_LEVEL(0) \
INIT_CALLS_LEVEL(1) \
INIT_CALLS_LEVEL(2) \
INIT_CALLS_LEVEL(3) \
INIT_CALLS_LEVEL(4) \
INIT_CALLS_LEVEL(5) \
INIT_CALLS_LEVEL(rootfs) \
INIT_CALLS_LEVEL(6) \
INIT_CALLS_LEVEL(7) \
VMLINUX_SYMBOL(__initcall_end) = .;
#define INIT_DATA_SECTION(initsetup_align) \
.init.data : AT(ADDR(.init.data) - LOAD_OFFSET) { \
... \
INIT_CALLS \
... \
}
Второй атрибут — __used
определен в заголовке include/linux/compiler-gcc.h. файл и расширяется до определения следующего атрибута gcc
:
#define __used __attribute__((__used__))
что предотвращает предупреждение переменная определена, но не используется
. Последняя строка макроса __define_initcall
:
LTO_REFERENCE_INITCALL(__initcall_##fn##id)
зависит от параметра конфигурации ядра CONFIG_LTO
и просто предоставляет заглушку для компилятора оптимизация времени компоновки:
#ifdef CONFIG_LTO
#define LTO_REFERENCE_INITCALL(x) \
static __used __exit void *reference_##x(void) \
{ \
return &x; \
}
#else
#define LTO_REFERENCE_INITCALL(x)
#endif
Чтобы предотвратить проблемы, связанные с отсутствием ссылки на переменную в модуле, она будет перенесена в конец программы. Это все, что касается макроса __define_initcall
. Таким образом, все макросы *_initcall
будут расширены во время компиляции ядра Linux, и все initcalls
будут помещены в свои разделы, и все они будут доступны из раздела .data
, и ядро Linux будет знать, где найти определенный initcall
, чтобы вызвать его во время процесса инициализации.
Поскольку initcalls
может вызываться ядром Linux, давайте посмотрим, как ядро Linux это делает. Этот процесс запускается функцией do_basic_setup
из файла исходного кода init/main.c:
static void __init do_basic_setup(void)
{
...
...
...
do_initcalls();
...
...
...
}
который вызывается во время инициализации ядра Linux, сразу после основных этапов инициализации, таких как инициализация, связанная с диспетчером памяти, подсистема ЦП
и другие, уже завершенные. Функция do_initcalls
просто проходит через массив уровней initcall
и вызывает функцию do_initcall_level
для каждого уровня:
static void __init do_initcalls(void)
{
int level;
for (level = 0; level < ARRAY_SIZE(initcall_levels) - 1; level++)
do_initcall_level(level);
}
Массив initcall_levels
определен в том же исходном коде файл и содержит указатели на разделы, которые были определены в макрос __define_initcall
:
static initcall_t *initcall_levels[] __initdata = {
__initcall0_start,
__initcall1_start,
__initcall2_start,
__initcall3_start,
__initcall4_start,
__initcall5_start,
__initcall6_start,
__initcall7_start,
__initcall_end,
};
Если вам интересно, вы можете найти эти разделы в скрипте компоновщика arch/x86/kernel/vmlinux.lds
, который генерируется после компиляции ядра Linux:
.init.data : AT(ADDR(.init.data) - 0xffffffff80000000) {
...
...
...
...
__initcall_start = .;
*(.initcallearly.init)
__initcall0_start = .;
*(.initcall0.init)
*(.initcall0s.init)
__initcall1_start = .;
...
...
}
Если вы не знакомы с этим, вы можете узнать больше о линкерах в специальной части этой книги.
Как мы только что увидели, функция do_initcall_level
принимает один параметр - уровень initcall и выполняет две следующие задачи: Во-первых, эта функция анализирует initcall_command_line
, который является копией обычной командной строки ядра, которая может содержать параметры для модулей с помощью функции parse_args
из исходного кода файла kernel/params.c и вызывает функцию do_on_initcall
для каждого уровня.
for (fn = initcall_levels[level]; fn < initcall_levels[level+1]; fn++)
do_one_initcall(*fn);
do_on_initcall
выполняет за нас основную работу. Как мы видим, эта функция принимает один параметр, который представляет функцию обратного вызова initcall
, и выполняет вызов данного обратного вызова:
int __init_or_module do_one_initcall(initcall_t fn)
{
int count = preempt_count();
int ret;
char msgbuf[64];
if (initcall_blacklisted(fn))
return -EPERM;
if (initcall_debug)
ret = do_one_initcall_debug(fn);
else
ret = fn();
msgbuf[0] = 0;
if (preempt_count() != count) {
sprintf(msgbuf, "preemption imbalance ");
preempt_count_set(count);
}
if (irqs_disabled()) {
strlcat(msgbuf, "disabled interrupts ", sizeof(msgbuf));
local_irq_enable();
}
WARN(msgbuf[0], "initcall %pF returned with %s\n", fn, msgbuf);
return ret;
}
Давайте попробуем понять, что делает функция do_on_initcall
. Прежде всего мы увеличиваем счетчик preemption, чтобы позже проверить его и убедиться, что он не несбалансирован. После этого шага мы можем увидеть вызов функции initcall_backlist
, которая проходит по списку blacklisted_initcalls
, в котором хранятся занесенные в черный список initcalls
, и освобождает данный initcall
, если он находится в этом списке:
list_for_each_entry(entry, &blacklisted_initcalls, next) {
if (!strcmp(fn_name, entry->buf)) {
pr_debug("initcall %s blacklisted\n", fn_name);
kfree(fn_name);
return true;
}
}
Занесенные в черный список initcalls
хранятся в списке blacklisted_initcalls
, и этот список заполняется во время ранней инициализации ядра Linux из командной строки ядра Linux.
После того как initcalls
, занесенные в черный список, будут обработаны, следующая часть кода выполняет непосредственный вызов initcall
:
if (initcall_debug)
ret = do_one_initcall_debug(fn);
else
ret = fn();
В зависимости от значения переменной initcall_debug
функция do_one_initcall_debug
будет вызывать initcall
или эта функция сделает это напрямую через fn()
. Переменная initcall_debug
определена в том же файле исходного кода:
bool initcall_debug;
и предоставляет возможность распечатать некоторую информацию в ядре журнала. Значение переменной можно установить из команд ядра через параметр initcall_debug
. Как мы можем прочитать из документации командной строки ядра Linux:
initcall_debug [KNL] Trace initcalls as they are executed. Useful
for working out where the kernel is dying during
startup.
И это правда. Если мы посмотрим на реализацию функции do_one_initcall_debug
, мы увидим, что она делает то же самое, что и функция do_one_initcall
или, то есть, do_one_initcall
функция do_one_initcall_debug
вызывает заданный initcall
и печатает некоторую информацию (например, pid текущей выполняющейся задачи, продолжительность выполнения initcall
и т. д.), связанное с выполнением данного initcall
:
static int __init_or_module do_one_initcall_debug(initcall_t fn)
{
ktime_t calltime, delta, rettime;
unsigned long long duration;
int ret;
printk(KERN_DEBUG "calling %pF @ %i\n", fn, task_pid_nr(current));
calltime = ktime_get();
ret = fn();
rettime = ktime_get();
delta = ktime_sub(rettime, calltime);
duration = (unsigned long long) ktime_to_ns(delta) >> 10;
printk(KERN_DEBUG "initcall %pF returned %d after %lld usecs\n",
fn, ret, duration);
return ret;
}
Поскольку initcall
был вызван одной из функций do_one_initcall
или do_one_initcall_debug
, мы можем увидеть две проверки в конце функции do_one_initcall
. Первый проверяет количество возможностей.
ble __preempt_count_add
и __preempt_count_sub
вызывает внутри выполненного initcall, и если это значение не равно предыдущему значению вытесняемого счетчика, мы добавляем строку preempt_count_add
в буфер сообщений и устанавливаем правильное значение вытесняемого счетчика:
if (preempt_count() != count) {
sprintf(msgbuf, "preemption imbalance ");
preempt_count_set(count);
}
Позже эта строка ошибки будет напечатана. Последние проверяют состояние локальных IRQ, и если они отключены, мы добавляем строки отключенных прерываний
в наш буфер сообщений и включаем IRQ
для текущего процессора, чтобы предотвратить состояние, когда IRQ
были отключены initcall
и больше не включались:
if (irqs_disabled()) {
strlcat(msgbuf, "disabled interrupts ", sizeof(msgbuf));
local_irq_enable();
}
Вот и все. Таким образом, ядро Linux выполняет инициализацию многих подсистем в правильном порядке. С этого момента мы знаем, что такое механизм initcall
в ядре Linux. В этой части мы рассмотрели основную часть механизма initcall
, но оставили некоторые важные понятия. Давайте кратко рассмотрим эти понятия.
Прежде всего, мы пропустили один уровень initcalls
, это rootfs initcalls
. Вы можете найти определение rootfs_initcall
в заголовочном файле include/linux/init.h вместе со всеми аналогичные макросы, которые мы видели в этой части:
#define rootfs_initcall(fn) __define_initcall(fn, rootfs)
Как мы можем понять из названия макроса, его основная цель — хранить обратные вызовы, связанные с rootfs. Помимо этой цели, может быть полезно инициализировать другие элементы после инициализации, связанные с уровнем файловых систем, только если элементы, связанные с устройствами, не инициализированы. Например, распаковка initramfs, которая произошла в функции populate_rootfs
из файла [init/initramfs.c](https://github.com /torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/init/initramfs.c) файл исходного кода:
rootfs_initcall(populate_rootfs);
Отсюда мы можем увидеть знакомый вывод:
[ 0.199960] Unpacking initramfs...
Помимо уровня rootfs_initcall
, существуют дополнительные уровни console_initcall
, security_initcall
и другие вторичные уровни initcall
. Последнее, что мы упустили, это набор уровней *_initcall_sync
. Почти каждый макрос *_initcall
, который мы видели в этой части, имеет макрос-компаньон с префиксом _sync
:
#define core_initcall_sync(fn) __define_initcall(fn, 1s)
#define postcore_initcall_sync(fn) __define_initcall(fn, 2s)
#define arch_initcall_sync(fn) __define_initcall(fn, 3s)
#define subsys_initcall_sync(fn) __define_initcall(fn, 4s)
#define fs_initcall_sync(fn) __define_initcall(fn, 5s)
#define device_initcall_sync(fn) __define_initcall(fn, 6s)
#define late_initcall_sync(fn) __define_initcall(fn, 7s)
Основная цель этих дополнительных уровней — дождаться завершения всех процедур инициализации, связанных с модулем, для определенного уровня.
Вот и все.
В этой части мы увидели важный механизм ядра Linux, который позволяет вызывать функцию, которая зависит от текущего состояния ядра Linux во время его инициализации.