Reader small image

You're reading from  Mastering Embedded Linux Programming - Third Edition

Product typeBook
Published inMay 2021
PublisherPackt
ISBN-139781789530384
Edition3rd Edition
Right arrow
Authors (2):
Frank Vasquez
Frank Vasquez
author image
Frank Vasquez

Frank Vasquez is an independent software consultant specializing in consumer electronics. He has over a decade of experience designing and building embedded Linux systems. During that time, he has shipped numerous devices including a rackmount DSP audio server, a diver-held sonar camcorder, and a consumer IoT hotspot. Before his career as an embedded Linux engineer, Frank was a database kernel developer at IBM where he worked on DB2. He lives in Silicon Valley.
Read more about Frank Vasquez

Chris Simmonds
Chris Simmonds
author image
Chris Simmonds

Chris Simmonds is a software consultant and trainer living in southern England. He has almost two decades of experience in designing and building open-source embedded systems. He is the founder and chief consultant at 2net Ltd, which provides professional training and mentoring services in embedded Linux, Linux device drivers, and Android platform development. He has trained engineers at many of the biggest companies in the embedded world, including ARM, Qualcomm, Intel, Ericsson, and General Dynamics. He is a frequent presenter at open source and embedded conferences, including the Embedded Linux Conference and Embedded World.
Read more about Chris Simmonds

View More author details
Right arrow

Chapter 5: Building a
Root Filesystem

The root filesystem is the fourth and final element of embedded Linux. Once you have read this chapter, you will be able to build, boot, and run a simple embedded Linux system.

The techniques I will describe here are broadly known as roll your own or RYO. Back in the early days of embedded Linux, this was the only way to create a root filesystem. There are still some use cases where an RYO root filesystem is applicable, for example, when the amount of RAM or storage is very limited, for quick demonstrations, or for any case in which your requirements are not (easily) covered by the standard build system tools. Nevertheless, these cases are quite rare. Let me emphasize that the purpose of this chapter is educational; it is not meant to be a recipe for building everyday embedded systems: use the tools described in the next chapter for this.

The first objective is to create a minimal root filesystem that will give us a shell prompt. Then, using...

Technical requirements

To follow along with the examples, make sure you have the following:

  • A Linux-based host system
  • A microSD card reader and card
  • The microSD card prepared for the BeagleBone Black from Chapter 4, Configuring and Building the Kernel
  • zImage and DTB for QEMU from Chapter 4, Configuring and Building the Kernel
  • A USB to TTL 3.3V serial cable
  • BeagleBone Black
  • A 5V 1A DC power supply
  • An Ethernet cable and port for NFS and TFTP

All of the code for this chapter can be found in the Chapter05 folder from the book's GitHub repository: https://github.com/PacktPublishing/Mastering-Embedded-Linux-Programming-Third-Edition

What should be in the root filesystem?

The kernel will get a root filesystem, either as an initramfs passed as a pointer from the bootloader or by mounting the block device given on the kernel command line by the root= parameter. Once it has a root filesystem, the kernel will execute the first program, by default named init, as described in the Early user space section in Chapter 4, Configuring and Building the Kernel. Then, as far as the kernel is concerned, its job is complete. It is up to the init program to begin starting other programs and bring the system to life.

To make a minimal root filesystem, you need these components:

  • init: This is the program that starts everything off, usually by running a series of scripts. I will describe how init works in much more detail in Chapter 13, Starting Up – The init Program.
  • Shell: You need a shell to give you a command prompt but, more importantly, also to run the shell scripts called by init and other programs.
  • ...

Transferring the root filesystem to the target

After having created a skeleton root filesystem in your staging directory, the next task is to transfer it to the target. In the sections that follow, I will describe three possibilities:

  • initramfs: Also known as a ramdisk, this is a filesystem image that is loaded into RAM by the bootloader. Ramdisks are easy to create and have no dependencies on mass storage drivers. They can be used in fallback maintenance mode when the main root filesystem needs updating. They can even be used as the main root filesystem in small embedded devices, and they are commonly used as the early user space in mainstream Linux distributions. Remember that the contents of the root filesystem are volatile, and any changes you make in the root filesystem at runtime will be lost when the system next boots. You would need another storage type to store permanent data such as configuration parameters.
  • Disk image: This is a copy of the root filesystem formatted...

Creating a boot initramfs

An initial RAM filesystem, or initramfs, is a compressed cpio archive. cpio is an old Unix archive format, similar to TAR and ZIP but easier to decode and so requiring less code in the kernel. You need to configure your kernel with CONFIG_BLK_DEV_INITRD to support initramfs.

As it happens, there are three different ways to create a boot ramdisk: as a standalone cpio archive, as a cpio archive embedded in the kernel image, and as a device table that the kernel build system processes as part of the build. The first option gives the most flexibility because we can mix and match kernels and ramdisks to our heart's content. However, it means that you have two files to deal with instead of one, and not all bootloaders have the facility to load a separate ramdisk. I will show you how to build one into the kernel later.

Standalone initramfs

The following sequence of instructions creates the archive, compresses it, and adds a U-Boot header ready for loading...

The init program

Running a shell, or even a shell script, at boot time is fine for simple cases, but really you need something more flexible. Normally, Unix systems run a program called init that starts up and monitors other programs. Over the years, there have been many init programs, some of which I will describe in Chapter 13, Starting Up – The init Program. For now, I will briefly introduce the init program from BusyBox.

The init program begins by reading the configuration file, /etc/inittab. Here is a simple example that is adequate for our needs:

::sysinit:/etc/init.d/rcS
::askfirst:-/bin/ash

The first line runs a shell script, rcS, when init is started. The second line prints the message Please press Enter to activate this console to the console and starts a shell when you press Enter. The leading - before /bin/ash means that it will become a login shell, which sources /etc/profile and $HOME/.profile before giving the shell prompt. One of the advantages of launching...

Configuring user accounts

As I have hinted already, it is not good practice to run all programs as root, since if
one program is compromised by an outside attack, then the whole system is at risk.
It is preferable to create unprivileged user accounts and use them where full root is
not necessary.

Usernames are configured in /etc/passwd. There is one line per user, with seven fields of information separated by colons, which are, in order, the following:

  • The login name
  • A hash code used to verify the password or, more usually, an x to indicate that the password is stored in /etc/shadow
  • The user ID
  • The group ID
  • A comment field, often left blank
  • The user's home directory
  • The shell this user will use (optional)

Here is a simple example in which we have user root with UID 0 and user daemon
with UID 1:

root:x:0:0:root:/root:/bin/sh
daemon:x:1:1:daemon:/usr/sbin:/bin/false

Setting the shell for user daemon to /bin/false ensures that any attempt...

A better way of managing device nodes

Creating device nodes statically with mknod is quite hard work and inflexible. There are other ways to create device nodes automatically on demand:

  • devtmpfs: This is a pseudo filesystem that you mount over /dev at boot time.
    The kernel populates it with device nodes for all the devices that the kernel currently knows about, and it creates nodes for new devices as they are detected at runtime. The nodes are owned by root and have default permissions of 0600. Some well-known device nodes, such as /dev/null and /dev/random, override the default to 0666. To see exactly how this is done, take a look at the Linux source file drivers/char/mem.c and see how struct memdev is initialized.
  • mdev: This is a BusyBox applet that is used to populate a directory with device nodes and to create new nodes as needed. There is a configuration file, /etc/mdev.conf, which contains rules for ownership and the mode of the nodes.
  • udev: This is the mainstream...

Configuring the network

Next, let's look at some basic network configurations so that we can communicate with the outside world. I am assuming that there is an Ethernet interface, eth0, and that we only need a simple IPv4 configuration.

These examples use the network utilities that are part of BusyBox, and they are sufficient for a simple use case, using the old-but-reliable ifup and ifdown programs. You can read the manual pages for both to get the details. The main network configuration is stored in /etc/network/interfaces. You will need to create these directories in the staging directory:

etc/network
etc/network/if-pre-up.d
etc/network/if-up.d
var/run

For a static IP address, /etc/network/interfaces would look like this:

auto lo
iface lo inet loopback
auto eth0
iface eth0 inet static
    address 192.168.1.101
    netmask 255.255.255.0
    network 192.168.1.0

For a dynamic IP address allocated using...

Creating filesystem images with device tables

We saw earlier, in the Creating a boot initramfs section, that the kernel has an option to create initramfs using a device table. Device tables are really useful because they allow a non-root user to create device nodes and to allocate arbitrary UID and GID values to any file or directory. The same concept has been applied to tools that create other filesystem image formats, as shown in this mapping from filesystem format to tool:

  • jffs2: mkfs.jffs2
  • ubifs: mkfs:ubifs
  • ext2: genext2fs

We will look at jffs2 and ubifs in Chapter 9, Creating a Storage Strategy, when we look at filesystems for flash memory. The third, ext2, is a format commonly used for managed flash memory including SD cards. The example that follows uses ext2 to create a disk image that can be copied to an SD card.

To begin with, you need to install the genext2fs tool on your host. On Ubuntu, the package to install is named genext2fs:

$ sudo apt...

Mounting the root filesystem using NFS

If your device has a network interface, it is often useful to mount the root filesystem over the network during development. It gives you access to the almost unlimited storage on your host machine, so you can add in debug tools and executables with large symbol tables. As an added bonus, updates made to the root filesystem on the development machine are made available on the target immediately. You can also access all the target's log files from the host.

To begin with, you need to install and configure an NFS server on your host. On Ubuntu, the package to install is named nfs-kernel-server:

$ sudo apt install nfs-kernel-server

The NFS server needs to be told which directories are being exported to the network; this is controlled by /etc/exports. There is one line for each export. The format is described in the manual page exports(5). As an example, to export the root filesystem on my host, I have this:

/home/chris/rootfs *(rw...

Using TFTP to load the kernel

Now that we know how to mount the root filesystem over a network using NFS, you may be wondering if there is a way to load the kernel, device tree, and initramfs over the network as well. If we can do this, the only component that needs to be written to storage on the target is the bootloader. Everything else could be loaded from the host machine. It would save time since you would not need to keep reflashing the target, and you could even get work done while the flash storage drivers are still being developed (it happens).

The Trivial File Transfer Protocol (TFTP) is the answer to the problem. TFTP is a very simple file transfer protocol, designed to be easy to implement in bootloaders such as U-Boot.

To begin with, you need to install a TFTP daemon on your host. On Ubuntu, the package to install is named tftpd-hpa:

$ sudo apt install tftpd-hpa

By default, tftpd-hpa grants read-only access to files in the /var/lib/tftpboot directory. With...

Summary

One of the strengths of Linux is that it can support a wide range of root filesystems, and so it can be tailored to suit a wide range of needs. We have seen that it is possible to construct a simple root filesystem manually with a small number of components and that BusyBox is especially useful in this regard. By going through the process one step at a time, it has given us insight into some of the basic workings of Linux systems, including network configuration and user accounts. However, the task rapidly becomes unmanageable as devices get more complex. And, there is the ever-present worry that there may be a security hole in the implementation that we have not noticed.

In the next chapter, I will show you how using an embedded build system can make
the process of creating an embedded Linux system much easier and more reliable. I will start by looking at Buildroot, and then go onto look at the more complex, but powerful, Yocto Project.

Further reading

  • Filesystem Hierarchy Standard, Version 3.0https://refspecs.linuxfoundation.org/fhs.shtml
  • ramfs, rootfs and initramfs, by Rob Landley, which is part of the Linux source in Documentation/filesystems/ramfs-rootfs-initramfs.txt
lock icon
The rest of the chapter is locked
You have been reading a chapter from
Mastering Embedded Linux Programming - Third Edition
Published in: May 2021Publisher: PacktISBN-13: 9781789530384
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
undefined
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at €14.99/month. Cancel anytime

Authors (2)

author image
Frank Vasquez

Frank Vasquez is an independent software consultant specializing in consumer electronics. He has over a decade of experience designing and building embedded Linux systems. During that time, he has shipped numerous devices including a rackmount DSP audio server, a diver-held sonar camcorder, and a consumer IoT hotspot. Before his career as an embedded Linux engineer, Frank was a database kernel developer at IBM where he worked on DB2. He lives in Silicon Valley.
Read more about Frank Vasquez

author image
Chris Simmonds

Chris Simmonds is a software consultant and trainer living in southern England. He has almost two decades of experience in designing and building open-source embedded systems. He is the founder and chief consultant at 2net Ltd, which provides professional training and mentoring services in embedded Linux, Linux device drivers, and Android platform development. He has trained engineers at many of the biggest companies in the embedded world, including ARM, Qualcomm, Intel, Ericsson, and General Dynamics. He is a frequent presenter at open source and embedded conferences, including the Embedded Linux Conference and Embedded World.
Read more about Chris Simmonds