Rolling your own minimal embedded Linux for the Raspberry Pi -- part two: early initialization

Pi logo In the first part of this series, I explained how to build a minimal Linux from the stock Raspbian firmware, that just booted to a root prompt. I explained that the only software needed in the root partition was a shell, and whatever libraries, etc., the shell needed. I suggested a couple of ways to get a shell -- using the busybox package from the Raspbian repository, or using the bash shell and the various libraries it needs. At this point, your root filesystem probably contains the /bin directory, and perhaps /lib. You may, or may not, have other utilities like, ls, copy, etc.

I also explained how to script a process that downloaded packages from the Raspbian repository, and unpack them, along with their dependencies, into a specified staging directory. The coreutils package would be a good thing to add to your root filesystem, unless you've got these basic utilities already from busybox, for example. In an embedded system you may not need these conventional command-line utilities, but they are helpful for troubleshooting.

In this article I will explain the next stage of the build -- how to do the early initialization that is important for preparing the system. At the end of this process, we will still only have a root shell, but we should have something that looks like a feasible Linux that has booted into single-user mode. This is, we'll be able to manipulate files, inspect processes, mount external drives, that kind of thing. Depending on what packages you install, you might be able to do simple networking operations using a wired connection.

I always use a read-only root filesystem for embedded Linux equipment, because I want to be able to switch off without worrying about shutting anything down. However, running with a read-only root filesystem does create some complications that have to be dealt with.

I should point out that all I can do in this article is to give general guidance -- every embedded Linux project is different, and will require a different set of binaries and a different configuration.

Get essential utilities

It's very difficult to do proper initialization of a Linux system in application code -- as a minimum, we need a basic set of utilties. If you installed busybox that you'll already have the basic stuff but, unless space is really, really limited, it's better to install the full versions before going much further.

I would suggest getting at least the following packages -- and their dependencies -- from the Raspbian repository, and installing them in your root filesystem staging area.

Note:
In case it isn't obvious -- the kernel module package must be a perfect match for the running kernel, down to the last digit in the version numbers

Plan volatile filesytems and create /etc/fstab

This is a crucial step with a read-only root filesystem. Some directories simply have to be writeable, like /tmp. If you're going to run a log daemon, then /var/log will also have to be writeable. With a read-only filesystem, the only way to accomplish this is to put these directories in RAM. The Pi doesn't have a huge amount of RAM to start with, so we have to be a bit careful here. In the end, it's a design decision how much RAM to allocate to in-memory filesystems.

My preferred approach here -- and it's by no means the only one -- is to define a conventional /etc/fstab with the relevant definitions, and just mount the individual directories in the shell script that starts the system. This allows me to mount the pseudo-filesystems like /sys and /proc as well.

Here's a typical fstab, allocating 256Mb to /tmp and 50Mb to /var/log.

tmpfs /tmp tmpfs defaults,size=256M,noatime,nodev,nosuid,mode=1777 0 0
tmpfs /var/log tmpfs defaults,size=50m,noatime,nodev,nosuid,mode=1777 0 0
/dev/mmcblk0p2 / auto defaults,noatime,ro 0 1
/dev/mmcblk0p1 /boot auto defaults,noatime,ro 0 1
proc /proc proc defaults 0 0
sys /sys sysfs defaults 0 0
devpts /dev/pts devpts defaults 0 0

Ensure the staging root filesystem contains mount points for the volatile filesystems

Having the entries in fstab isn't sufficient -- there need to be places to mount them. If your root filesystem only contains /bin at this point, you'll need to add (empty) directories /proc, /tmp, /dev, and /sys.

Some utilities will expect /var/run and /run to exist, and to be writeable. I generally make these symlinks to directories under /tmp -- that is, in RAM. Although the symlinks themselves exist in the root filesystem I build, the directories they link to -- under /tmp -- do not exist at boot time, because /tmp is in memory. So my start-up script needs to create these subdirectories of /tmp -- I guess this only adds a few milliseconds to the boot time.

Depending on what your Linux system has to do, you may find other utilities and applications that need certain directories to be writable, and these will also need provision made from them. It's impossible to give more specific advice that this, because the details will be entirely application-specific.

Create basic networking configuration files

This isn't urgent at this early stage, but it's more-or-less the minimum needed to get get wired ethernet working. We're not in a position to use wifi yet, but a wired connection should be workable. I should point out that if your application does not require any network access, this step is not essential.

In early stages of testing I always assign a static IP number, even if I plan to use DHCP later. The IP number will appear in various configuration files, and I usually write a script to write these network configuration files, given some basic configuration. In the examples that follow, my IP number is 192.168.1.81, and my hostname 'pi'.

The file /etc/hostname looks like this:

pi

which is simple enough. /etc/hosts looks like this:

127.0.0.1 localhost
192.168.1.81 pi

I will deal with more sophisticated network configuration in a later article.

Create the new startup script

With the configuration described above, it's easy enough to implement a script that will do early initialization, and leave us in a state where we can run basic utilties, and maybe test wired ethernet connection to local systems. I save this script as /bin/startup.sh. The script must begin with a #! to identify the shell, because it will be executed by the kernel.

#!/bin/bash
mount /proc
mount /sys
mount /tmp
mkdir /dev/pts
mount /dev/pts
mkdir /tmp/var
mkdir /tmp/run
hostname --file /etc/hostname
exec /bin/bash

Configure the kernel to boot the script

Somewhat surprisingly, perhaps, the Linux kernel will boot a shell script rather than a binary. That's provided the shell actually exists, and can be executed -- but we've already tested that. So now we just need to modify the init= statement in cmdline.txt in the boot partition:

... init=/bin/startup.sh

Burn the SD card and test

Copy the boot and root filesystem files to the first and second partitions of the SD card, and boot the Pi. It almost certainly won't work, but with luck you will at least have error messages sufficiently detailed to figure out what files you forgot to install ;)

Next: service management and remote access; or go to the series index.