web-dev-qa-db-fra.com

module_init () contre core_initcall () contre early_initcall ()

Dans les pilotes, je vois souvent ces trois types de fonctions init utilisées.

module_init()
core_initcall()
early_initcall()
  1. Dans quelles circonstances dois-je les utiliser?
  2. Existe-t-il également d'autres moyens d'init?
29
mk..

Ils déterminent l'ordre d'initialisation des modules intégrés. Les pilotes utiliseront device_initcall (Ou module_init; Voir ci-dessous) la plupart du temps. L'initialisation précoce (early_initcall) Est normalement utilisée par le code spécifique à l'architecture pour initialiser les sous-systèmes matériels (gestion de l'alimentation, DMA, etc.) avant qu'un véritable pilote ne soit initialisé.

Trucs techniques pour comprendre ci-dessous

Regardez init/main.c . Après quelques initialisations spécifiques à l'architecture effectuées par du code dans Arch/<Arch>/boot Et Arch/<Arch>/kernel, La fonction portable start_kernel Sera appelée. Finalement, dans le même fichier, do_basic_setup Est appelé:

/*
 * 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();
}

qui se termine par un appel à 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);
}

Vous pouvez voir les noms ci-dessus avec leur index associé: early est 0, core est 1, etc. Chacune de ces entrées __initcall*_start Pointe vers un tableau de pointeurs de fonction qui obtiennent appelés l'un après l'autre. Ces pointeurs de fonction sont les modules réels et les fonctions d'initialisation intégrées, ceux que vous spécifiez avec module_init, early_initcall, Etc.

Qu'est-ce qui détermine quel pointeur de fonction entre dans quel tableau __initcall*_start? L'éditeur de liens effectue cela, en utilisant des conseils des macros module_init Et *_initcall. Ces macros, pour les modules intégrés, affectent les pointeurs de fonction à une section ELF spécifique.

Exemple avec module_init

En considérant un module intégré (configuré avec y dans .config), module_init Se développe simplement comme ceci ( include/linux/init.h ):

#define module_init(x)  __initcall(x);

puis nous suivons ceci:

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

Donc, maintenant, module_init(my_func) signifie __define_initcall(my_func, 6). C'est _define_initcall:

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

ce qui signifie que jusqu'à présent, nous avons:

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

Wow, beaucoup de choses GCC, mais cela signifie seulement qu'un nouveau symbole est créé, __initcall_my_func6, Qui est mis dans la section ELF nommée .initcall6.init, Et comme vous pouvez le voir, pointe vers la fonction spécifiée (my_func). L'ajout de toutes les fonctions à cette section crée finalement le tableau complet des pointeurs de fonction, tous stockés dans la section ELF .initcall6.init.

Exemple d'initialisation

Regardez à nouveau ce morceau:

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

Prenons le niveau 6, qui représente tous les modules intégrés initialisés avec module_init. Il commence à partir de __initcall6_start, Sa valeur étant l'adresse du premier pointeur de fonction enregistré dans la section .initcall6.init, Et se termine à __initcall7_start (Exclu), incrémentant à chaque fois avec la taille de *fn (qui est un initcall_t, qui est un void*, qui est de 32 bits ou 64 bits selon l'architecture).

do_one_initcall Appellera simplement la fonction pointée par l'entrée actuelle.

Dans une section d'initialisation spécifique, ce qui détermine pourquoi une fonction d'initialisation est appelée avant une autre est simplement l'ordre des fichiers dans les Makefiles puisque l'éditeur de liens concatène les symboles __initcall_* Les uns après les autres dans leur init ELF respective. sections.

Ce fait est en fait utilisé dans le noyau, par ex. avec les pilotes de périphérique ( drivers/Makefile ):

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

tl; dr: le mécanisme d'initialisation du noyau Linux est vraiment beau, bien que surligné en fonction de GCC.

47
eepp

module_init est utilisé pour marquer une fonction à utiliser comme point d'entrée d'un périphérique Linux- chauffeur.
On l'appelle

  • pendant do_initcalls() (pour un pilote intégré)
    ou
  • au moment de l'insertion du module (pour un module *.ko)

Il peut y avoir SEULEMENT 1 module_init() par module de pilote.


Les fonctions *_initcall() sont généralement utilisées pour définir les pointeurs de fonction pour initialiser divers sous-systèmes.

do_initcalls() dans le code source du noyau Linux contient l'appel de la liste des divers appels init et l'ordre relatif dans lequel ils sont appelés lors du démarrage du noyau 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()
    fin des modules intégrés
  9. modprobe ou insmod des modules *.ko.

L'utilisation de module_init() dans un pilote de périphérique équivaut à équivalent à l'enregistrement d'un device_initcall().

Gardez à l'esprit que lors de la compilation, l'ordre de liaison des différents fichiers objet du pilote (*.o) dans le noyau Linux est important; il détermine l'ordre dans lequel ils sont appelés lors de l'exécution.

*_initcall fonctions du même niveau
sera appelé lors du démarrage dans l'ordre où ils sont liés .

Par exemple, changer l'ordre de liaison des pilotes SCSI dans drivers/scsi/Makefile Changera l'ordre dans lequel les contrôleurs SCSI sont détectés, et donc la numérotation des disques.

18
TheCodeArtist

Il semble que personne ne se soit concentré sur la façon dont le script de l'éditeur de liens est configuré pour fournir des pointeurs de fonction utilisés pour l'initialisation du code du noyau, alors essayons de voir à quel point le noyau Linux crée magnifiquement le script de l'éditeur de liens pour les appels init.

Parce que les réponses ci-dessus ont montré que le code Linux C peut créer et gérer tous les appels init de manière à définir une fonction comme initcall, une variable globale pour accéder aux fonctions définies et des fonctions qui invoquent réellement l'initcall défini lors de l'initialisation phase, je ne veux pas les revoir.

Par conséquent, ici, nous aimerions nous concentrer sur la façon dont chaque élément de la variable de tableau global appelé initcall_levels [] est défini, que signifie-t-il, que est contenue dans la mémoire pointée par chaque élément du tableau initcall_levels, etc.

Tout d'abord, essayons de comprendre où les variables sont définies dans le référentiel du noyau Linux. Lorsque vous regardez le fichier init/main.c, vous pouvez constater que tous les éléments du tableau initcall_levels n'ont pas été définis dans le fichier main.c et importés de quelque part.

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[];

Cependant, vous pouvez constater que ces variables ne sont déclarées dans aucun code source C du référentiel Linux, alors d'où viennent les variables? À partir du script de l'éditeur de liens!

Linux fournit de nombreuses fonctions d'assistance pour aider les programmeurs à générer un fichier de script de l'éditeur de liens spécifique à l'architecture, et elles sont définies dans le fichier linux/include/asm-generic/vmlinux.lds.h qui fournit également une assistance pour les appels init.

#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) = .;

Nous pouvons facilement constater que plusieurs macros sont définies pour les appels init. La macro la plus importante est INIT_CALLS qui émet une syntaxe de script de l'éditeur de liens qui définit un symbole de script de l'éditeur de liens accessible dans le code C simple et la section d'entrée.

En détail, chaque invocation de la macro INIT_CALLS_LEVEL (x) définit un nouveau symbole appelé __initcall ## level _ ## start ( référez-vous à l'opération de concaténation ## dans CPP ); ce symbole est généré par VMLINUX_SYMBOL (__ initcall ## level ## _ start) =.; . Par exemple INIT_CALLS_LEVEL (1) la macro définit le symbole de script de l'éditeur de liens nommé __ initcall1_start .

Par conséquent, les symboles __initcall0_start à __initcall7_start sont définis dans le script de l'éditeur de liens et peuvent être référencés dans le code C en le déclarant avec le mot clé extern.

De plus, la macro INIT_CALLS_LEVEL définit de nouvelles sections appelées . InitcallN.init , ici N est un 0 à 7. La section générée contient toutes les fonctions définies avec un fourni macro telle que __define_initcall comme spécifié par l'attribut section.

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

Les symboles et sections créés doivent être correctement configurés par le script de l'éditeur de liens pour être situés dans une section, la section .init.data. Pour activer cela, la macro INIT_DATA_SECTION est utilisée; et nous pouvons constater qu'il invoque la macro INIT_CALLS que nous avons examinée.

#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                     \
    }

Par conséquent, en appelant la macro INIT_CALLS, l'éditeur de liens Linux localise __ initcall0_start à __ initcall7_start symboles et les sections . initcall0.init à . initcall7.init dans la section .init.data, qui se trouvent dos à dos. Notez ici que chaque symbole ne contient aucune donnée, mais sert à localiser le début et la fin de la section générée.

Essayons ensuite de voir si le noyau Linux compilé contient correctement les symboles générés, les sections et la fonction. Après avoir compilé le noyau Linux, en utilisant l'outil nm, nous pouvons récupérer tous les symboles définis dans l'image Linux compilée appelée 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

Comme indiqué ci-dessus, entre le symbole __initcall0_start et __initcall2_start, toutes les fonctions définies avec la macro pure_initcall sont localisées. Par exemple, regardons la fonction ipc_ns_init définie dans le fichier 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); 

Comme indiqué ci-dessus, la macro pure_initcall est utilisée pour placer la fonction ipc_ns_init dans la section .initcall0.init qui est localisée par le symbole __initcall0_start. Par conséquent, comme indiqué dans le code ci-dessous, toutes les fonctions des sections .initcallN.init sont appelées une par une séquentiellement.

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