Linux Embedded

Le blog des technologies libres et embarquées

Génération et configuration d’Initramfs sous Yocto

Bonjour. Dans cet article nous discuterons de l’intérêt d’utiliser un initramfs et des modalités de sa mise en place dans un environnement Yocto. Le but n’est pas ici de revenir aux principes fondateurs du projet Yocto, pour lesquels je vous recommanderais plutôt les précédents articles de ce blog :

Un layer Yocto d’exemple, résultat des manipulations décrites ici, se trouve dans les références de l’article.

Introduction

Historique

Le concept d’initrd (pour Initial RAM Disk) prédate celui d’initramfs (Initial RAM Filesystem, introduit avec le noyau 2.6), cependant aujourd’hui les deux notions sont allègrement confondues. Dans cet article nous nous restreindrons là où c’est applicable à l’Initramfs.

L’initramfs comme l’initrd empaquettent quelques fichiers, tout juste suffisants à assister le noyau lors des premiers stades du démarrage. Le démarrage du système se déroule alors selon les étapes suivantes :

  1. Une fois chargé en mémoire et démarré par le bootloader, le noyau charge le contenu de l’initramfs ou de l’initrd en mémoire vive et lance un programme d’initialisation (init, par défaut /init) sur celui-ci.
  2. Le programme d’initialisation se charge de préparer le noyau aux prochaines étapes du démarrage : chargement des modules adéquats, peuplement de /dev, réalisation des actions nécessaires à la préparation et au montage du disque racine.
  3. Le programme d’initialisation passe alors la main à l’init situé sur le disque racine. La fonction kernel_init du noyau met en évidence les différentes localisations possibles de l’init:
    1. /sbin/init
    2. /etc/init
    3. /bin/init
    4. /bin/sh

L’initrd est un vrai système de fichiers (souvent ext2), monté sur un ramdisk de taille fixe, ce qui gâche de la mémoire vive ainsi que de la bande passante mémoire. Celui-ci est spécifié dans le paramètre root= de la cmdline noyau, ce qui a pour conséquence la nécessité d’utiliser l’appel système pivot_root qui indique au noyau qu’il doit changer de système racine.

En revanche, l’initramfs est une archive CPIO compressée, qui est décompressée très tôt par le noyau en ramfs (ou tmpfs). Le ramfs et le tmpfs présentent l’avantage d’être de taille dynamique, le tmpfs supportant en sus le déchargement sur espace d’échange swap. Comme il ne s’agit pas d’un vrai périphérique, ce ne peut pas être la partition racine : on utilise donc switch_root au lieu de pivot_root, et le périphérique pointé par root= dans la cmdline est le périphérique qui sera réellement utilisé comme racine lors de l’exécution.

L’initramfs peut être livré intégré au noyau, ou bien séparément.

Illustration du processus de démarrage avec Initramfs

Une documentation plus complète peut être retrouvée dans Documentation/filesystems/ramfs-rootfs-initramfs.txt et Documentation/admin-guide/initrd.rst

Intérêt

Lors d’un démarrage simple, le bootloader (chargeur de démarrage) charge le noyau en mémoire depuis un disque qui lui est accessible ou un emplacement réseau. Celui-ci lit la cmdline qui lui a été donnée par le bootloader (ou sa cmdline statique) pour déterminer quel périphérique utiliser comme racine, le monte, et démarre le programme d’initialisation.

En revanche, les progrès matériels comme la multiplication des configurations font qu’un cas aussi simple est rare :

  • Démarrage sur volume RAID
  • Utilisation du mappeur de périphérique comme par exemple:
  • Gestion de volumes logiques (LVM)
  • Déchiffrement du rootfs avec dm-crypt par exemple
  • Vérification cryptographique du rootfs avant le montage
  • Détection précoce des périphériques ou lancement d’outils de vérification d’intégrité comme fsck
  • Démarrage sur des cibles très variées (LiveCD)
  • Montage de systèmes de fichiers dont le module n’est pas inclus dans le noyau

La mise en œuvre d’un démarrage aussi complexe aurait pu se faire dans le noyau mais est beaucoup plus simple et configurable si réalisée en espace utilisateur. Après l’exécution éventuelle de toutes ces tâches, l’initramfs rend la main au système de fichiers final. La plupart des distributions Linux mettent à disposition des utilitaires pour générer son initramfs : genkernel et dracut sous Gentoo, mkinitramfs sous Debian et dérivés par exemple.

Le support initramfs doit être activé dans les options du noyau avant compilation :

# CONFIG_BLK_DEV_INITRD=y
General setup  --->
      [*] Initial RAM filesystem and RAM disk (initramfs/initrd) support

Choisissons ensuite si notre initramfs sera intégré au noyau ou non. Si c’est le cas, le script de compilation du noyau peut se charger d’empaqueter un dossier comme s’il s’agissait du rootfs utilisé pour l’initramfs:

# CONFIG_INITRAMFS_SOURCE="/usr/src/initramfs"
General setup --->
      (/usr/src/initramfs) Initramfs source file(s)

En revanche, si l’on souhaite conserver un initramfs externe au noyau, il peut être intéressant d’activer le support pour un initramfs compressé :

General setup --->
      () Initramfs source file(s)
      [*] Support initial ramdisk/ramfs compressed using gzip
      [ ] Support initial ramdisk/ramfs compressed using bzip2
      [ ] Support initial ramdisk/ramfs compressed using LZMA
      [ ] Support initial ramdisk/ramfs compressed using XZ
      [ ] Support initial ramdisk/ramfs compressed using LZO
      [ ] Support initial ramdisk/ramfs compressed using LZ4

Yocto a sa propre manière de générer l’initramfs et de l’intégrer à ses artefacts de compilation. Nous allons maintenant les passer en revue.

Générer son initramfs avec Yocto

Dans le contexte du projet Yocto, l’initramfs est tout simplement une image séparée de l’image principale générée avec bitbake, et sélectionnée par la distribution pour inclusion (ou non) dans le noyau.

Elle peut ainsi différer amplement du rootfs ce qui peut être pratique pour y intégrer des outils absolument nécessaires au lancement du système mais dangereux ou inutiles en fonctionnement normal. Ainsi un acteur hostile gagnant l’accès au système pourra se retrouver privé d’outils cruciaux à l’escalade de privilège.

Nous partirons d’un nouveau layer vide, configuré ainsi :

# Dans le dossier de poky
poky@bs:poky/$ source oe-init-build-env
[...]

poky@bs:poky/build/$ bitbake-layers create-layer ../meta-initramfs
Add your new layer with 'bitbake-layers add-layer ../meta-initramfs'

poky@bs:poky/build/$ cd ../meta-initramfs

poky@bs:meta-initramfs/$ rm -r recipes-example/

poky@bs:meta-initramfs/$ mkdir -p recipes-core/image conf/distro

# Créer maintenant une distro minimale mydistro basée sur poky, et une image minimale myimage basée sur core-image-minimal
poky@bs:meta-initramfs/$ cat >recipes-core/images/myimage.bb << EOF
> require recipes-core/images/core-image-minimal.bb
> PR = "r1"
> DESCRIPTION = "My Image"
> EOF

poky@bs:meta-initramfs/$ cat >conf/distro/mydistro.conf << EOF
> require conf/distro/poky.conf
> PR = "r1"
> EOF

Ne pas oublier d’activer mydistro dans build/conf/local.conf :

DISTRO ?= "mydistro"

Configuration du noyau

Par défaut le noyau Yocto fourni par la recette linux-yocto devrait être configuré pour le support Initramfs. En cas d’utilisation d’une recette personnalisée pour le noyau, s’inspirer de la tâche do_bundle_initramfs de la bbclasse « kernel » (dans poky/meta/classes/kernel.bbclass).

Si nécessaire, nous pouvons créer un .bbappend et un fragment de configuration modifiant le .config utilisé pour la génération du noyau. En supposant que l’on ait choisi linux-yocto comme fournisseur pour virtual/kernel :

poky@bs:poky/meta-initramfs$ mkdir -p recipes-kernel/linux/files

poky@bs:poky/meta-initramfs$ cat >recipes-kernel/linux/linux-yocto_%.bbappend << EOF
> FILESEXTRAPATHS_prepend := "${THISDIR}/files:"
> SRC_URI += "file://enable-initramfs.cfg"
> EOF

poky@bs:poky/meta-initramfs$ cat >recipes-kernel/linux/files/enable-initramfs.cfg << EOF
> CONFIG_BLK_DEV_INITRD=y
> EOF

Configuration de la distribution et de l’image

Afin d’informer Yocto de notre souhait d’ajouter un initramfs à notre build, il nous faut manipuler deux variables dont la documentation est dans poky/meta-poky/conf/local.conf.sample.extended :

  • INITRAMFS_IMAGE indique la recette choisie comme génératrice de l’initramfs. Cette variable peut être définie dans une image ou un fichier .conf (distro ou fichier local.conf). Nous utiliserons l’image fournie par défaut avec Yocto, core-image-minimal-initramfs.
  • INITRAMFS_IMAGE_BUNDLE contrôle l’intégration ou non de l’initramfs dans le noyau. Si cette variable est à « 1 », les tâches do_bundle_initramfs et do_bundle de la recette linux-yocto se chargeront d’associer l’initramfs au noyau.
# Dans meta-initramfs/conf/distro/mydistro.conf ou build/conf/local.conf par exemple

# Enable bundling initramfs in kernel
# This means you have to replace the kernel after burning
# the image to your hard disk key manually (for now)
INITRAMFS_IMAGE="core-image-minimal-initramfs"
INITRAMFS_IMAGE_BUNDLE="1"

A ce point un bitbake myimage devrait générer dans le répertoire de déploiement le noyau contenant l’initramfs qui peut remplacer le noyau sur cible et l’image source qui pourra être placée à côté du noyau original pour tenter un démarrage avec initramfs externe au noyau :

poky@bs:deploydir$ ls -1 bzImage* core-image-minimal-initramfs*

# Original, non-initramfs kernel and symbolic links
bzImage
bzImage--4.14.67+XXX-r2-x86-64-XXX.bin
bzImage-x86-64.bin

# Initramfs-powered bzImages
bzImage-initramfs-4.14.67+XXX-x86-64-XXX.bin
bzImage-initramfs-x86-64.bin

# Images used for initramfs generation
core-image-minimal-initramfs-argodisplay-x86-64.rootfs.cpio.gz
core-image-minimal-initramfs-argodisplay-x86-64.cpio.gz
[...]

Utilisation des initramfs-frameworks

Un intérêt important de l’approche standard de Yocto est l’utilisation dans core-image-minimal-initramfs d’un petit système d’initialisation modulaire nommé initramfs-framework. On peut observer dans la définition de l’image l’inclusion de la recette et de certains de ses modules :

INITRAMFS_SCRIPTS ?= "\
      initramfs-framework-base \
      initramfs-module-setup-live \
      initramfs-module-udev \
      initramfs-module-install \
      initramfs-module-install-efi \
"

La recette, qui se trouve dans poky/meta/recipes-core/initrdscripts, installe un script shell « framework » comme script init sur la cible. Celui-ci définira au démarrage des fonctions utilitaires puis lancera un à un les scripts/modules situés dans son « répertoire de modules » (par défaut /init.d/).

Quelques avantages de ce système pour ses modules :

  • La fourniture d’utilitaires pour le chargement de modules noyau (load_kernel_module) et le déboggage à différents niveaux de verbosité (msg, info, debug, fatal).
  • Une ligne de commande déjà analysée, un paramètre foo=bar de la cmdline se trouvant dans la variable bootparam_foo=bar.
  • La possibilité d’installer des scripts hooks qui seront lancés avant et après chaque module, utiles pour le déboggage ou la remontée d’informations.
  • La configuration du comportement en cas d’échec : arrêt du système, passage sur une console shell…
  • Le switch_root effectué automatiquement par le module finish, installé avec le framework.

Les modules proposés sont fournis par les recettes suivantes :

RecetteUtilité
initramfs-module-execExécute tout fichier trouvé dans /exec.d
initramfs-module-mdevPeuple /dev avec mdev
initramfs-module-udevPeuple /dev avec udev
initramfs-module-e2fsCharge les modules et monte les systèmes de fichiers extX
initramfs-module-nfsrootfsMonte un rootfs par NFS
initramfs-module-rootfsMonte un rootfs classique
initramfs-module-debugLance un shell avant et après tout autre module
initramfs-module-lvmScanne les volumes LVM disponibles sur le système et les mappe avec udev
initramfs-module-install*Lance un installeur pour l’image
initramfs-module-setup-liveLance une session « live »

En ajoutant à notre image initramfs une recette créant des fichiers dans /init.d/ (ou en patchant grâce à un .bbappend initramfs-framework pour ce faire), il est possible d’ajouter des fonctionnalités au framework qui seront exécutées automatiquement.

Attention en revanche avant de lancer switch_root : tout ce qui se trouve hors du nouveau rootfs sera détruit avec sa disparition de la mémoire vive. De même, créer des montages inaccessibles dans le nouveau rootfs les fera disparaître sans les démonter proprement (il faut donc par exemple s’assurer qu’udev n’est pas configuré dans l’initramfs pour monter automatiquement les supports amovibles…).

Bonus : Intégration à Kickstart

Kickstart est l’outil qui combinera les différents artefacts (images Yocto, kernel, bootloaders…) et les assemblera en un fichier unique selon les règles édictées dans un fichier .wks. En sortie de Kickstart on retrouve généralement un fichier .wic (éventuellement compressé) qu’il est possible de flasher directement sur cible avec dd ou bmaptools.

Habituellement le noyau utilisé est assemblé dans la partition /boot de l’image finale grâce à un module (appelé source) dans le fichier de configuration Kickstart. :

# Boot partition
part /boot --source bootimg-pcbios --ondisk sda --label boot --active --align 1024 --use-uuid
# Rootfs
part / --source rootfs --ondisk sda --fstype=ext4 --label rootfs --align 1024 --use-uuid

# Bootloader configuration
bootloader  --ptable msdos --timeout=0  --append="rootwait rootfstype=ext4"

Le paramètre –append du bootloader correspond ici à ce qui sera ajouté à la cmdline de Linux. C’est ici qu’il est possible d’ajouter l’utilisation d’un initramfs externe avec le mot clef initrd une fois qu’il se trouve sur la partition de boot. Exemple :

bootloader --ptable msdos --timeout=0 --append="rootwait rootfstype=ext4 initrd=initramfs-linux.img"

Configurons maintenant notre image pour utiliser Kickstart :

poky@bs:meta-initramfs/$ cat recipes-core/images/myimage.bb
require recipes-core/images/core-image-minimal.bb
PR = "r1"
DESCRIPTION = "My Image"
# Generate a Kickstarter (eventually sparse) file as output. "wic.bz2" is convenient too.
IMAGE_FSTYPES += "wic"
# Feed myimage.wks to Kickstart
WKS_FILE = "myimage.wks"

Bien que Kickstart soit dans les grandes lignes peu configurable via le fichier .wks, nous allons maintenant voir comment ajouter cette fonctionnalité via notre propre module Kickstart, en Python.

Afin que Kickstart trouve facilement notre noyau combiné à l’initramfs, ajoutons tout d’abord un lien symbolique de nom bzImage.initramfs (si le noyau est au format bzImage) dans le dossier de déploiement en patchant la recette du noyau :

poky@bs:poky$ cat meta-initramfs/recipes-kernel/linux/linux-yocto_%.bbappend
FILESEXTRAPATHS_prepend := "${THISDIR}/files:"
SRC_URI += "file://enable-initramfs.cfg"

# Add "bzImage.initramfs" symlink to deploy folder for easy pickup
# by kickstart
kernel_do_deploy_append() {
    for type in ${KERNEL_IMAGETYPES}; do
        if [ -e "$deployDir/${initramfs_base_name}.bin" ]; then
            ln -sf ${initramfs_base_name}.bin $deployDir/${type}.initramfs
        fi
    done
}

Il nous faudra ensuite copier puis étendre le script utilisé (ici bootimg-pcbios.py) :

# DIR="script/lib/wic/plugins/source"
poky@bs:poky$ mkdir "meta-initramfs/$DIR"
poky@bs:poky$ cp "./$DIR/bootimg-pcbios.py" "meta-initramfs/$DIR/bootimg-custom.py"

Ajoutons donc dans la fonction do_prepare_partition la recherche et la copie du bon noyau :

def do_prepare_partition(cls, part, source_params, creator, cr_workdir,
                             oe_builddir, bootimg_dir, kernel_dir,
                             rootfs_dir, native_sysroot):
        [...]
        hdddir = "%s/hdd/boot" % cr_workdir

        # Recherche d'un fichier "bzImage.initramfs". En cas d'échec retourner à la valeur par défaut "bzImage"
        if os.path.isfile(staging_kernel_dir + "/bzImage.initramfs"):
            logger.debug("Found initramfs kernel in %s/bzImage.initramfs",
                         staging_kernel_dir)
            kernfile = "bzImage.initramfs"
        else:
            logger.debug("Initramfs kernel not found, falling back to"
                " regular bzImage")
            kernfile = "bzImage"

        cmds = ("install -m 0644 %s/%s %s/vmlinuz" %
                (staging_kernel_dir, kernfile, hdddir),
                "install -m 444 %s/syslinux/ldlinux.sys %s/ldlinux.sys" %
                (bootimg_dir, hdddir),
                "install -m 0644 %s/syslinux/vesamenu.c32 %s/vesamenu.c32" %
                (bootimg_dir, hdddir),
                "install -m 444 %s/syslinux/libcom32.c32 %s/libcom32.c32" %
                (bootimg_dir, hdddir),
                "install -m 444 %s/syslinux/libutil.c32 %s/libutil.c32" %
                (bootimg_dir, hdddir))

        [...]

Lors de l’utilisation de notre script comme source dans un fichier .wks, Kickstart cherchera maintenant un noyau avec initramfs si disponible :

part /boot --source bootimg-custom --ondisk sda --label boot --active --align 1024 --use-uuid

Références

Layer Yocto meta-article-generation-initramfs-yocto contenant les manipulations décrites dans cet article

Capture d’écran GRUB : Creative Commons Attribution-Share Alike 4.0 International

Logo Tux : lewing@isc.tamu.edu Larry Ewing and The GIMP, sous Creative Commons CC0 don universel au domaine public

Logo RAM : Pixabay License

Logo HDD : Pixabay License

Navigation de l'article