When Linux tries to boot into User Space, it does so by starting the init (PID=1) process from several well-known locations (e..g: /usr/sbin/init
, etc.) For this to happen, the kernel first needs to load a root filesystem. The location of this rootfs is specified as part of the kernel's command line arguments:
$ cat /proc/cmdline BOOT_IMAGE=/boot/vmlinuz-linux root=/dev/mapper/volgrp0-lv_root rw loglevel=3 quiet
Notice in this example how the root
variable is not /dev/sda
or /dev/nvme0n1
, but instead a Logical Volume (i.e.: an intermediate representational layer that abstracts the backing storage devices – see figure below.) Although the kernel has LVM support, the drivers are likely to be compiled as modules, to be loaded on demand when the user requires them, rather than increasing the size and complexity of the kernel with features that the majority of users do not need. This puts us in an awkward position where we need the LVM drivers to access the root partition but the modules are located on the very same root partition.
The solution is to add an initial ramdisk: a disk image that is loaded in RAM at the same time as the kernel. The kernel will view this region in RAM as a storage device, without needing extensive driver support (e.g.: NET_FS). This minimal disk image should contain all kernel modules and tools required to gain access and mount the real rootfs. Once the initramfs is mounted at /
and the real rootfs is mounted at another location (e.g.: /mnt/
), the init process will use the pivot_root()
system call to switch around the two mount points. As a result, the real rootfs will then be mounted at /
, making the initramfs safe to unmount from /mnt/
and clear from RAM.
During ARM development, such ramdisks are often used for prototyping purposes. Once we ensure that the Linux kernel successfully boots to User Space, we may consider writing a partition table to the eMMC memory.
A Device Tree (DT) is a data structure that (normally) contains a hierarchical representation of the available hardware, but also runtime configuration information. The DT is always written in the Device Tree Source (DTS) format and looks something like this:
We notice that the DT is structured as multiple nested nodes. Each node can be referenced by a path comprised of the names of all of its ancestors. In fact, nodes that represent hardware devices are usually assigned an alias based on this path:
Each node is comprised of properties and other node. A property can be a:
device_type = “cpu”;
regulator-boot-on;
reg = <0x30860000 0x10000>;
Additionally, there are special properties that start with a #
symbol. These properties are used by the FDT parser as hints on how to interpret the packed array of integers. Normally, each element in the array is a uint32_t. However, certain properties (reg
especially) usually contain one or more (address, size)
tuples. In the example above, the reg
property located in the serial@30890000
node informs the kernel that the serial (UART) device is mapped in memory starting at 0x30890000
has a size of 0x10000
. Note that in this case we have one tuple where each element consists of precisely one uint32_t element. This may not always be the case! Hence, the following properties:
#address-cells
: Number of uint32_t cells that comprise the address element in each tuple.#size-cells
: Number of uint32_t cells that comprise the size element in each tuple.
Knowing these values is the only way to determine the number of tuples in a packed array property. Note, however, that the #*-cells
properties do not apply to the node where they are encountered, but only to its children. For example, if bus@30800000
overrides these properties (whose default value was inherited from soc@0
), only the reg
of serial@30890000
would adhere to these changes. The ranges
property of bus@30800000
would still use the values inherited from soc@0
.