carloscn / blog

My blog
Apache License 2.0
119 stars 34 forks source link

Linux System : Managing Linux Services - initramfs #173

Open carloscn opened 1 year ago

carloscn commented 1 year ago

Linux System : Managing Linux Services - initramfs

The NXP-IMX6ULL is the NXP cortex-A7 (Armv7 arch) and the Xilinx ZYNQ is the cortex-A53 (Armv8 arch). We will use the cortex-A7 and cortex-A53 as initramfs examples to decribe how to setup the initramfs before booting the real rootfs. The article will introduce as follows:

1. What is the initramfs

1.1 initramfs/initrd introducation

The initramfs is the solution introduced for the 2.6 Linux kernel series. The idea is that there's a lot of initialization magic done in the kernel that could be just as easily done in userspace.[^1]

Initial RAM disk (INITRD) provides the capability to load a RAM disk by the boot loader during the Linux startup process. The Linux kernel mounts it as RootFS and starts the initialization process. This section describes the procedure to configure the INITRD boot[^3].

Initial RAM file system (INITRAMFS) is the successor of INITRD. It is a cpio archive of the initial file system that gets loaded into memory during the Linux startup process. The Linux kernel mounts it as RootFS and starts the initialization process[^2].

At a first glance, it is only slightly different than a traditional initrd. Initramfs' are loaded quite a bit sooner than initrd's are. The key parts of initramfs are[^1]:

There is an using the initramfs example which initramfs decrypts the encrypted rootfs before mounting real rootfs, you can refer to the link: [Embedded] enabling the cryptsetup on ramdisk. In this article, we will introduce INITRAMFS rather than INITRD.

1.2 initramfs principle

The Linux kernel interacts with a file system while it’s running.  The kernel supports a wide variety of memory storage devices where the filesystem can be stored.  Most storage devices are persistent, which means they remember their state when powered off.  The Linux kernel can also run in volatile memory, such as RAM.  Some reasons for doing this include speed, since volatile memory is usually much faster, and the ability to modify a persistent file system while it’s not in use.  The Linux kernel can be configured to run from RAM by using the initramfs feature[^5].

When using initramfs, a filesystem image is embedded into the kernel image.  When the kernel boots, it extracts the filesystem image into RAM and runs an init script from there.  The init script is usually a file called ‘init’ in the root of the ramfs filesystem, but this can be overridden by setting the ‘rdinit’ variable in the Linux command line from the boot loader.  Configuring the Linux kernel to use initramfs is done through the menuconfig system.  The option is “Initial RAM filesystem and RAM disk” in the General Setup section.  The configuration requires the filename of the filesystem image to embed in the kernel.  Creating the ramfs filesystem image must be done separately from building the kernel[^5].

After the initramfs finished tasks, initramfs will switch to the real root file system by switch_root[^6].

2. Build initramfs

There are countless ways to make an initramfs. You can choose not to create an initramfs at all but let tools, such as Genkernel or Dracut, do the work for you. If you are lucky, one of them does what you want out of the box, and you don't need to bother with how initramfs works and what it does anymore. If you're unlucky, they don't do what you want and you have to extend their functionality, or even build an initramfs all by yourself.

An initramfs contains at least one file called /init. This file is executed by the kernel as the main init process (PID 1). It has to do all the work. In addition, there can be any number of additional files and directories that are required by /init. They are usually files you'll also find on any other root filesystem, such as /dev for device nodes, /proc for kernel information, /bin for binaries, and so on. The structure of an initramfs can be simple, or it can be complicated, depending on what you are planning to do.

When the kernel mounts the initramfs, your target root partition is not yet mounted, so you can't access any of your files. That means there is nothing but the initramfs. So everything you need, everything you want, you have to include it in your initramfs. If you want a shell, you have to include it in your initramfs. If you want to mount something, you need a mount utility. If you need to load a module, your initramfs has to provide both the module, as well as a utility to load it. If the utility depends on libraries in order to work, you have to include the libraries as well. This seems complicated, and it is, because the initramfs has to function independently.

2.1 build source code

2.1.1 using the buildroot as ramdisk

This post explains how to load a ramdisk on iMX boards. The initrd and initramfs are two ways for achieving this. This tutorial approaches only the initrd (initial ram disk) method. This guide uses iMX6Q Sabre SD Board, and uses a file system generated by Buildroot.

Follow the next steps to enable the Kernel to support initrd:

$ make menuconfig

Then, mark the following packages in General setup:

Compile the Kernel:

$ make

For building the initrd on Buildroot file system, follow the next steps:

$ git clone git clone git://git.buildroot.net/buildroot
$ cd buildroot/
$ make imx6-sabresd_defconfig
$ make menuconfig

Then, mark the following option:

Compile the file system:

$ make

The result of this compilation is a rootfs.ext2.gz file.

For converting the file system to the right format, it is required to use mkimage tool. To install the tool use the following command:

$ sudo apt-get install u-boot-tools

Run the following command:

$ cd output/images/
$ mkimage -A arm -O linux -T ramdisk -C gzip -n "Build Root File System" -d rootfs.ext2.gz rootfs.ext2.gz.uboot

2.1.1 using the busybox as ramdisk[^9]

Instead of collecting countless utilities and libraries (and never seeing the end of it), just use busybox. It's a set of utilities for rescue and embedded systems, it contains a shell, utilities like ls, mkdir, cp, mount, insmod, and many more - all in a single binary called /bin/busybox.

BusyBox

BusyBox can be built either as a single static binary requiring no external libraries, or built requiring shared libraries such as GLIBC (default). This setting can be found under BusyBox Settings -> Build Options -> Build BusyBox as a static binary (no shared libs).

I generally choose to build BusyBox to require GLIBC as it is highly likely you will want to run additional applications that will require GLIBC sometime in the future.

wget https://github.com/mirror/busybox/archive/refs/tags/1_32_1.zip --no-check-certificate
unzip 1_32_1.zip
cd busybox-1.32.1/
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabi- defconfig
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabi- menuconfig

At the menu, you can configure BusyBox options. Once configured, you can build BusyBox:

make ARCH=arm CROSS_COMPILE=arm-linux-gnueabi- 
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabi- install CONFIG_PREFIX=/home/export/rootfs

GLIBC

wget http://ftp.gnu.org/gnu/libc/glibc-2.33.tar.xz
tar -xJf glibc-2.33.tar.xz
mkdir glibc-build
cd glibc-build/
../glibc-2.33/configure arm-linux-gnueabi --build=i686-pc-linux-gnu CXX=arm-linux-gnueabi-g++ --prefix= --enable-add-ons
make
make install install_root=/home/export/rootfs 

Some programs may require libgcc_s.so, otherwise you will receive an error:

error while loading shared libraries: libgcc_s.so.1: cannot open shared object file: No such file or directory

libgcc_s.so.1 can be copied over from your arm-linux-gnueabi installation:

cp /usr/lib/gcc-cross/arm-linux-gnueabi/4.7.3/libgcc_s.so.1 /home/export/rootfs/lib

Preparing RootFS

Once BusyBox and GLIBC has been cross-compiled, you will want to create the remainder of the root file system. Start by creating the necessary directory structure:

mkdir proc sys dev etc/init.d usr/lib

Now we must mount the /proc & /sys filesystem and populate the /dev nodes. This can be done at runtime by creating a file called etc/init.d/rcS and adding:

#!bin/sh
mount -t proc none /proc
mount -t sysfs none /sys
echo /sbin/mdev > /proc/sys/kernel/hotplug
/sbin/mdev -s

and make executable:

chmod +x etc/init.d/rcS

You should now have a basic, yet quite functional, BusyBox root file system.

DropBear (Optional)

DropBear requires RSA and DSS (Digital Signature Standard) encryption keys to be generated. I normally do this on the target, but you could generate the keys on the host if you have the dropbearkey executable installed.

To generate your keys:

mkdir /etc/dropbear
dropbearkey -t dss -f /etc/dropbear/dropbear_dss_host_key  
dropbearkey -t rsa -f /etc/dropbear/dropbear_rsa_host_key 

You will also require users and passwords to validate login credentials:

touch /etc/passwd
touch /etc/group
adduser root -u 0

Unless otherwise specified, root will be given a default home directory of /home/root. However as this doesn't exist, DropBear will close your connection immediately after successfully logging in. To address this, simply create a home directory for root:

mkdir /home /home/root

DropBear can now be started by running:

dropbear

and you should be able to remotely login to your system using the root user.

If you get an error after logging in, "Server refused to allocate pty" check you have Device Drivers > Character devices > Legacy (BSD) PTY support enabled in your kernel. (Especially applicable to Beaglebone kernels)

Alternatively, make sure you have mounted the /dev/pts filesystem (after /dev has been mounted)

mkdir /dev/pts
mount -t devpts none /dev/pts

ldconfig

ldconfig is used to configure dynamic linker run-time bindings. It creates symbolic links and a cache to the most recent shared libraries. As you build upon your root filesystem and add additional libraries, you may need to run ldconfig.

ldconfig will search for libraries in the trusted directory /lib. Additional search paths can be added to the ld.so.conf configuration file. ldconfig looks for a configuration file in /etc/ld.so.conf and generates a warning if this cannot be found. Suppress the warning and extend the search range to include /usr/lib by:

echo /usr/lib > etc/ld.so.conf

ldconfig will also generate a cache at /etc/ld.so.cache. If this file doesn't exist, it will be automatically generated.

Finally, to update the dynamic linker run-time bindings with verbose output, execute:

ldconfig -v

Read Only Filesystems

If you are using a root file system residing on flash memory, to improve longevity it may be desirable to either mount your rootfs as read only, or to move as many of the frequently written to files (for example /var) to a temporary volatile file system stored in RAM.

The following is an expanded etc/init.d/rcS file demonstrating this.

We mount /var and /dev as tmpfs. The device nodes are generated using mdev at boot and from hotplug, hence we move these to a temp file system otherwise they could not be generated on a read only root filesystem.

#!bin/sh
mount -t proc none /proc
mount -t sysfs none /sys
mount -t tmpfs none /var
mount -t tmpfs none /dev
mkdir /dev/pts
mount -t devpts none /dev/pts
echo /sbin/mdev > /proc/sys/kernel/hotplug
/sbin/mdev -s
mkdir /var/log
syslogd
dropbear

syslogd writes system messages to /var/log/messages and is something you probably don't want constantly writing to flash. After mounting /var as a temp file system, we create the /var/log directory for systemd.

2.2 get built ramdisk directly

For the ARMv8 Arch, the file arm_ramdisk.image.gz of https://xilinx-wiki.atlassian.net/wiki/spaces/A/pages/18842473/Build+and+Modify+a+Rootfs.

The ARMv8 also can obtain by : https://drive.google.com/file/d/1N-0J3lmGCIeCO1_9zTNYmPMeVTZk3Ngh/view?usp=share_link

For making the uramdisk.gz follows the next steps:

$ gunzip ramdisk.image.gz
$ sudo mount -o loop ramdisk.image tmp_mnt
$ cd tmp_mnt

Change the /etc/inittab file[^8]:

$ cd ..
$ sudo umount tmp_mnt
$ gzip ramdisk.image
$ mkimage -A arm -T ramdisk -C gzip -d ramdisk.image.gz uramdisk.image.gz

3. Custom Script

3.1 INIT

The file structure of your initramfs is almost complete. The only thing that is missing is /init itself, the executable in the root of the initramfs that is executed by the kernel. Because sys-apps/busybox includes a fully functional shell, this means you can write your /init binary as a simple shell script (instead of making it a complicated application written in Assembler or C that needs to compile).

The following example shows a minimalistic shell script, based on the busybox shell:

#!/bin/busybox sh

#/usr/src/initramfs/initminimalistic /init example
# Mount the /proc and /sys filesystems.
mount -t proc none /proc
mount -t sysfs none /sys

# Do your stuff here.
echo "This script just mounts and boots the rootfs, nothing else!"

# Mount the root filesystem.
mount -o ro /dev/sda1 /mnt/root

# Clean up.
umount /proc
umount /sys

# Boot the real thing.
exec switch_root /mnt/root /sbin/init

This example needs some device nodes to work, mainly the root block device. Change the script and copy the corresponding /dev/ node to fit the system needs.

Don't forget to make the /init file executable:

root # chmod +x /usr/src/initramfs/init

3.2 Packaging

The initramfs now has to be made available to the kernel at boot time. This is done by packaging it as a compressed cpio archive. This archive is then either embedded directly into the kernel image, or stored as a separate file which can be loaded by the bootloader during the boot process. Both methods perform equally well/

3.2.1 Kernel configuration

With either method, there is a need to enable Initial RAM filesystem and RAM disk (initramfs/initrd) support.

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

3.2.2 Embedding into the Kernel

The initramfs to be embedded into the kernel image, set Initramfs source file(s) to the root of your initramfs, (e.g. /usr/src/initramfs) but this isn't necessary:

General setup  --->
    (/usr/src/initramfs) Initramfs source file(s)

Now compile the kernel it will automatically put the files into a cpio archive and embed it into the kernel image. There will need to rebuild the kernel any time a changes is made to the initramfs.

3.2.3 Creating a separate file

To use a standalone archive file, adjust the kernel settings accordingly:

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

For this example gzip is sufficient.

Create a standalone archive file by running the following commands:

root #cd /usr/src/initramfs
root #find . -print0 | cpio --null --create --verbose --format=newc | gzip --best > /boot/custom-initramfs.cpio.gz

This will create a file called custom-initramfs.cpio.gz in /boot. Now instruct your bootloader to load this file along with the kernel.

3.3 uboot script setup

Set the u-boot script that is booting from ramdisk.

setenv bootargs console=ttymxc0,115200 root=/dev/ram0 

mmc dev 0; fatload mmc 0:2 80800000 zImage; fatload mmc 0:2 83000000 device.dtb; fatload mmc 0:2 90000000 uramdisk.image.gz;

bootz 80800000 90000000 83000000

Ref

[^1]:Ubuntu Wiki - Initramfs [^2]:Configuring INITRAMFS Boot [^3]:Configuring INITRD Boot [^4]:Xilinx Wiki - Linux Xilinx Open Source - Linux - Build and Modify a Rootfs [^5]:How do I use Linux initramfs? [^6]:switch_root busybox init problems? [^7]:How to boot imx using ramdisk [^8]:arm移植ubuntu系统出现A start job is running for dev-ttymxc0.device [^9]:Building a minimal RootFS with Busybox, GLIBC and DropBear [^10]:Custom Initramfs