Подтвердить что ты не робот

Module_init() vs. core_initcall() vs. early_initcall()

В драйверах я часто вижу эти три типа функций init.

module_init()
core_initcall()
early_initcall()
  • При каких обстоятельствах я должен их использовать?
  • Также есть ли другие способы init?
4b9b3361

Ответ 1

Они определяют порядок инициализации встроенных модулей. Водители будут использовать device_initcall (или module_init, см. Ниже) большую часть времени. Ранняя инициализация (early_initcall) обычно используется кодом, специфичным для архитектуры, для инициализации аппаратных подсистем (управление питанием, DMA и т.д.) До инициализации любого реального драйвера.

Технический материал для понимания ниже

Посмотрите init/main.c. После нескольких инициализаций, специфичных для архитектуры, выполняемых с помощью кода в arch/<arch>/boot и arch/<arch>/kernel, будет вызываться портативная функция start_kernel. В конце концов, в том же файле вызывается do_basic_setup:

/*
 * Ok, the machine is now initialized. None of the devices
 * have been touched yet, but the CPU subsystem is up and
 * running, and memory and process management works.
 *
 * Now we can finally start doing some real work..
 */
static void __init do_basic_setup(void)
{
    cpuset_init_smp();
    usermodehelper_init();
    shmem_init();
    driver_init();
    init_irq_proc();
    do_ctors();
    usermodehelper_enable();
    do_initcalls();
}

который заканчивается вызовом do_initcalls:

static initcall_t *initcall_levels[] __initdata = {
    __initcall0_start,
    __initcall1_start,
    __initcall2_start,
    __initcall3_start,
    __initcall4_start,
    __initcall5_start,
    __initcall6_start,
    __initcall7_start,
    __initcall_end,
};

/* Keep these in sync with initcalls in include/linux/init.h */
static char *initcall_level_names[] __initdata = {
    "early",
    "core",
    "postcore",
    "arch",
    "subsys",
    "fs",
    "device",
    "late",
};

static void __init do_initcall_level(int level)
{
    extern const struct kernel_param __start___param[], __stop___param[];
    initcall_t *fn;

    strcpy(static_command_line, saved_command_line);
    parse_args(initcall_level_names[level],
           static_command_line, __start___param,
           __stop___param - __start___param,
           level, level,
           &repair_env_string);

    for (fn = initcall_levels[level]; fn < initcall_levels[level+1]; fn++)
        do_one_initcall(*fn);
}

static void __init do_initcalls(void)
{
    int level;

    for (level = 0; level < ARRAY_SIZE(initcall_levels) - 1; level++)
        do_initcall_level(level);
}

Вы можете увидеть приведенные выше имена со своим индексом: early равно 0, core равно 1 и т.д. Каждая из этих записей __initcall*_start указывает на массив указателей функций, которые вызываются один за другим. Эти указатели функций представляют собой фактические модули и встроенные функции инициализации, те, которые вы указываете с помощью module_init, early_initcall и т.д.

Что определяет, какой указатель функции попадает в массив __initcall*_start? Компонент делает это, используя подсказки из макросов module_init и *_initcall. Эти макросы для встроенных модулей назначают указатели на определенные секции ELF.

Пример с module_init

Учитывая встроенный модуль (сконфигурированный с y в .config), module_init просто расширяется следующим образом (include/linux/init.h):

#define module_init(x)  __initcall(x);

а затем мы следуем этому:

#define __initcall(fn) device_initcall(fn)
#define device_initcall(fn)             __define_initcall(fn, 6)

Итак, теперь module_init(my_func) означает __define_initcall(my_func, 6). Это _define_initcall:

#define __define_initcall(fn, id) \
    static initcall_t __initcall_##fn##id __used \
    __attribute__((__section__(".initcall" #id ".init"))) = fn

что означает, что до сих пор мы имеем:

static initcall_t __initcall_my_func6 __used
__attribute__((__section__(".initcall6.init"))) = my_func;

Ничего себе, много материалов GCC, но это означает только создание нового символа __initcall_my_func6, который помещается в раздел ELF с именем .initcall6.init, и, как вы можете видеть, указывает на указанную функцию (my_func). Добавление всех функций в этот раздел в конечном итоге создает полный массив указателей на функции, которые хранятся в разделе .initcall6.init ELF.

Пример инициализации

Посмотрите еще раз на этот фрагмент:

for (fn = initcall_levels[level]; fn < initcall_levels[level+1]; fn++)
    do_one_initcall(*fn);

Возьмем уровень 6, который представляет все встроенные модули, инициализированные с помощью module_init. Он начинается с __initcall6_start, его значение является адресом первого указателя функции, зарегистрированного в разделе .initcall6.init, и заканчивается на __initcall7_start (исключено), каждый раз увеличивая размер *fn (который является initcall_t, который является void*, который является 32-разрядным или 64-битным в зависимости от архитектуры).

do_one_initcall будет просто вызывать функцию, на которую указывает текущая запись.

В определенном разделе инициализации то, что определяет, почему функция инициализации вызывается перед другой, - это просто порядок файлов в Make файлах, поскольку компоновщик будет конкатенировать символы __initcall_* один за другим в их соответствующем элементе ELF. разделы.

Этот факт фактически используется в ядре, например. с драйверами устройств (drivers/Makefile):

# GPIO must come after pinctrl as gpios may need to mux pins etc
obj-y                           += pinctrl/
obj-y                           += gpio/

tl; dr: механизм инициализации ядра Linux действительно красив, хотя и выделяет GCC-зависимый.

Ответ 2

module_init используется для обозначения функции, которая будет использоваться в качестве точки входа устройства Linux -driver.
Он называется

  • во время do_initcalls() (для встроенного драйвера)
    или
  • во время установки модуля (для модуля *.ko)

Для каждого модуля драйвера может быть ТОЛЬКО 1 module_init().


Функции *_initcall() обычно используются для установки указателей функций для инициализации различных подсистем.

do_initcalls() в исходном коде ядра Linux содержит вызов списка различных initcalls и относительного порядка, в котором они вызываются во время Загрузка ядра Linux.

  • early_initcall()
  • core_initcall()
  • postcore_initcall()
  • arch_initcall()
  • subsys_initcall()
  • fs_initcall()
  • device_initcall()
  • late_initcall()
    конец встроенных модулей
  • modprobe или insmod модулей *.ko.

Использование module_init() в драйвере устройства эквивалентно регистрации device_initcall().

Имейте в виду, что во время компиляции порядок связывания различных файлов объектов драйвера (*.o) в ядре Linux значителен; он определяет порядок, в котором они вызывается во время выполнения.

*_initcall функции того же уровня
будет вызываться во время загрузки в том порядке, в котором они связаны.

Например, изменение порядка ссылок драйверов SCSI в drivers/scsi/Makefile изменит порядок обнаружения SCSI-контроллеров и, следовательно, нумерацию дисков.

Ответ 3

Похоже, что никто не сосредоточился на том, как скрипт компоновщика сконфигурирован для предоставления указателей функций, используемых для инициализации кода ядра, поэтому давайте попробуем посмотреть, как красиво ядро Linux создает скрипт компоновщика для вызовов инициализации.

Потому что выше отличные ответы показали, что код Linux C может создавать и управлять всеми вызовами initcall таким образом, как определить функцию как initcall, глобальную переменную для доступа к определенным функциям и функции, которые фактически вызывают определенный initcall при инициализации фаза, я не хочу возвращаться к ним снова.

Поэтому здесь мы хотели бы сосредоточиться на том, как определяется каждый элемент переменной глобального массива с именем initcall_levels [], что это означает, что содержится в памяти, на которую указывает каждый элемент массив initcall_levels и т.д.

Во-первых, давайте попробуем понять, где переменные определены в репозитории ядра Linux. Когда вы посмотрите на файл init/main.c, вы обнаружите, что все элементы массива initcall_levels не определены в файле main.c и не импортированы откуда-либо.

extern initcall_t __initcall_start[];
extern initcall_t __initcall0_start[];
extern initcall_t __initcall1_start[];
extern initcall_t __initcall2_start[];
extern initcall_t __initcall3_start[];
extern initcall_t __initcall4_start[];
extern initcall_t __initcall5_start[];
extern initcall_t __initcall6_start[];
extern initcall_t __initcall7_start[];
extern initcall_t __initcall_end[];

Однако вы можете обнаружить, что эти переменные не объявлены ни в одном исходном коде C репозитория Linux, тогда откуда эти переменные? Из скрипта компоновщика!

Linux предоставляет множество вспомогательных функций, помогающих программистам создавать файл сценария компоновщика для конкретной архитектуры, и они определены в файле linux/include/asm-generic/vmlinux.lds.h, который также предоставляет помощник для initcalls.

#define __VMLINUX_SYMBOL(x) _##x
#define __VMLINUX_SYMBOL_STR(x) "_" #x
#else
#define __VMLINUX_SYMBOL(x) x
#define __VMLINUX_SYMBOL_STR(x) #x
#endif

/* Indirect, so macros are expanded before pasting. */
#define VMLINUX_SYMBOL(x) __VMLINUX_SYMBOL(x)

#define INIT_CALLS_LEVEL(level)                     \
        VMLINUX_SYMBOL(__initcall##level##_start) = .;      \
        KEEP(*(.initcall##level##.init))            \
        KEEP(*(.initcall##level##s.init))           \

#define INIT_CALLS                          \
        VMLINUX_SYMBOL(__initcall_start) = .;           \
        KEEP(*(.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) = .;

Мы можем легко обнаружить, что для initcall определены несколько макросов. Наиболее важным макросом является INIT_CALLS, который испускает синтаксис сценария компоновщика, который определяет символ сценария компоновщика, к которому можно получить доступ в простом C-коде и разделе ввода.

Подробно, каждый вызов макроса INIT_CALLS_LEVEL (x) определяет новый символ с именем __initcall ## level _ ## start (ссылается на операцию конкатенации ## в CPP); этот символ генерируется VMLINUX_SYMBOL (__ initcall ## level ## _ start) =.;. Например, макрос INIT_CALLS_LEVEL (1) определяет символ сценария компоновщика с именем __initcall1_start.

В результате символы от __initcall0_start до __initcall7_start определены в сценарии компоновщика и могут быть указаны в коде C, объявив его с помощью ключевого слова extern.

Кроме того, макрос INIT_CALLS_LEVEL определяет новые разделы с именем .initcallN.init, здесь N - от 0 до 7. Сгенерированный раздел содержит все функции, определенные с помощью предоставленного макроса, такого как __define_initcall, как определено атрибутом section.

#define __define_initcall(fn, id) \
    static initcall_t __initcall_##fn##id __used \
    __attribute__((__section__(".initcall" #id ".init"))) = fn

Созданные символы и разделы должны быть правильно настроены сценарием компоновщика, который должен находиться в одном разделе, разделе .init.data. Чтобы включить это, используется макрос INIT_DATA_SECTION; и мы можем обнаружить, что он вызывает макрос INIT_CALLS, который мы посмотрели.

#define INIT_DATA_SECTION(initsetup_align)              \
    .init.data : AT(ADDR(.init.data) - LOAD_OFFSET) {       \
        INIT_DATA                       \
        INIT_SETUP(initsetup_align)             \
        INIT_CALLS                      \
        CON_INITCALL                        \
        SECURITY_INITCALL                   \
        INIT_RAM_FS                     \
    }

Поэтому, вызывая макрос INIT_CALLS, компоновщик Linux размещает символы __initcall0_start в __initcall7_start и символы .initcall0.init в .initcall7.init. разделы в разделе .init.data, которые расположены вплотную. Здесь обратите внимание, что каждый символ не содержит каких-либо данных, но используется для определения местоположения, где начинается и заканчивается созданный раздел.

Затем давайте попробуем проверить, правильно ли скомпилированное ядро Linux содержит сгенерированные символы, разделы и функцию. После компиляции ядра Linux, используя инструмент nm, мы можем получить все символы, определенные в скомпилированном образе Linux под названием vmlinux.

//ordering nm result numerical order 
$nm -n vmlinux > symbol 
$vi symbol


ffffffff828ab1c8 T __initcall0_start
ffffffff828ab1c8 t __initcall_ipc_ns_init0
ffffffff828ab1d0 t __initcall_init_mmap_min_addr0
ffffffff828ab1d8 t __initcall_evm_display_config0
ffffffff828ab1e0 t __initcall_init_cpufreq_transition_notifier_list0
ffffffff828ab1e8 t __initcall_jit_init0
ffffffff828ab1f0 t __initcall_net_ns_init0
ffffffff828ab1f8 T __initcall1_start
ffffffff828ab1f8 t __initcall_xen_pvh_gnttab_setup1
ffffffff828ab200 t __initcall_e820__register_nvs_regions1
ffffffff828ab208 t __initcall_cpufreq_register_tsc_scaling1
......
ffffffff828ab3a8 t __initcall___gnttab_init1s
ffffffff828ab3b0 T __initcall2_start
ffffffff828ab3b0 t __initcall_irq_sysfs_init2
ffffffff828ab3b8 t __initcall_audit_init2
ffffffff828ab3c0 t __initcall_bdi_class_init2

Как показано выше, между символами __initcall0_start и __initcall2_start находятся все функции, определенные с помощью макроса pure_initcall. Например, давайте посмотрим на функцию ipc_ns_init, определенную в файле ipc/shim.c

static int __init ipc_ns_init(void)
{
    const int err = shm_init_ns(&init_ipc_ns);
    WARN(err, "ipc: sysv shm_init_ns failed: %d\n", err);
    return err;
}

pure_initcall(ipc_ns_init); 

Как показано выше, макрос pure_initcall используется для помещения функции ipc_ns_init в раздел .initcall0.init, который расположен символом __initcall0_start. Поэтому, как показано в приведенном ниже коде, все функции в разделах .initcallN.init вызываются последовательно одна за другой.

for (fn = initcall_levels[level]; fn < initcall_levels[level+1]; fn++)
    do_one_initcall(*fn);