Rolling your own minimal embedded Linux for the Raspberry Pi -- part two: early initialization
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.
coreutils
. This contains the really fundamental utilities --cp
,ls
, etcmount
-- surprisingly, this is not in thecore-utils
bundlehostname
-- it's good to be able to set the hostname, even if we don't have workable networking at this stagefile
bash
, or some other fully-featured shell if you've been using Busybox until nownet-tools
-- with these utilities it should be possible to get wired ethernet working, even if wifi will need to be deferred until laterkmod
-- utilities to load and list kernel modulesThe kernel modules your system will need. In the early stages of development, it's probably best to install the full set of modules (from the
raspberrypi-kernel
package). Eventually you will probably want to prune this set down to the modules you actually need, which will speed up module little a little.
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.