module_init() по сравнению с core_initcall() по сравнению с Early_initcall()

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

module_init()
core_initcall()
early_initcall()
  1. При каких обстоятельствах я должен их использовать?
  2. Кроме того, есть ли другие способы инициализации?

person mk..    schedule 04.09.2013    source источник


Ответы (3)


Они определяют порядок инициализации встроенных модулей. Драйверы будут использовать 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 просто вызовет функцию, на которую указывает текущая запись.

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

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

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

Вкратце: механизм инициализации ядра Linux действительно прекрасен, хотя и зависит от GCC.

person eepp    schedule 04.09.2013
comment
Что module_init будет расширено, зависит от того, obj-y это или obj-m ? Как это происходит? - person demonguy; 29.05.2015
comment
где __initcall6_start назначается адрес первой функции? - person Tony; 03.04.2018
comment
я нашел vmlinux.lds.h, похоже, что он имеет какое-то отношение к скрипту компоновщика, будет ли vmlinux.lds.h участвовать на этапе связывания или этапе компиляции и экспортировать символ __initcall6_start для ссылки main.o? - person Tony; 03.04.2018
comment
я думаю, что я найду ответ сам. sourceware.org/binutils/docs/ld/ - person Tony; 03.04.2018

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

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

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


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

do_initcalls() в исходном коде ядра Linux< /a> содержит вызов списка различных вызовов инициализации и относительный порядок, в котором они вызываются во время загрузки ядра Linux.

  1. early_initcall()
  2. core_initcall()
  3. postcore_initcall()
  4. arch_initcall()
  5. subsys_initcall()
  6. fs_initcall()
  7. device_initcall()
  8. late_initcall()
    конец встроенных модулей
  9. modprobe или insmod из *.ko модулей.

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

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

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

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

person TheCodeArtist    schedule 04.09.2013

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

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

Поэтому здесь мы хотели бы сосредоточиться на том, как определяется каждый элемент переменной глобального массива с именем 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, который также предоставляет вспомогательную функцию для вызовов инициализации.

#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 (reference ## операция конкатенации в 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, как указано в атрибуте раздела.

#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);
person ruach    schedule 17.08.2019