Revision History | ||
---|---|---|
Revision 0.2 | May 2012 | WW |
document mainloop and a bit about networking |
Table of Contents
This document came out of a bunch of notes I took while trying to hack on
dracut
. Once you know how it’s put together, it’s actually really easy to get
things done… but woo boy, that learning curve.
So. Hopefully these notes will help anyone else who’s looking at dracut and trying to understand it, or add new stuff, or fix/improve/destroy existing code.
Dracut is like 98% shell scripts, so you’re gonna need to know some
sh
/bash
to do anything with it. Luckily bash
is really easy. Almost too
easy. But I’ll save that rant for another day.
This work is licensed under the Creative Commons Attribution-ShareAlike 3.0 Unported License. To view a copy of this license, visit http://creativecommons.org/licenses/by-sa/3.0/ or send a letter to Creative Commons, 444 Castro Street, Suite 900, Mountain View, California, 94041, USA.
Whenever I talk about modules in this document I’m talking about dracut modules. Kernel modules are a totally different thing, and to avoid confusion I may refer to them as drivers instead.
dracut
builds the initramfs out of modules, which are found in
/usr/lib/dracut/modules.d
.
[1]
Each module is prefixed with a number, like 40network or 99base. The
number (which I’ll call the priority) determines what order the modules
are run in while building the initramfs.
dracut refuses to overwrite files when installing things into the
initramfs, so things installed by the the lower numbered modules have
priority over the higher ones. This lets your module override
things that are set up by the dracut
builtin modules (which are numbered
90-99).
the module’s priority has nothing to do with what order the scripts
will run inside the initramfs. That’s all controlled by the priorities set
by the inst_hook
lines in module-setup.sh
. Speaking of which..
module-setup.sh will usually contain three functions: check()
,
depends()
, and install()
.
This function checks to see if this module should be included in the initramfs that’s being built. It can return one of three values:
check()
is missing.
If there isn’t an explicit return
statement, the return value of a
shell function is the return value of the last command executed.
So it’s not uncommon to see a check()
function like this:
check() { [ -x /usr/bin/cpio ] }
This will return 0 if /usr/bin/cpio
exists and 1 otherwise. Easy!
This function prints a list of other modules required by this module. The return value is ignored.
This is where the files from the module directory (called $moddir
inside the
script) get installed into the initramfs, along with other stuff the module
requires (executables, udev rules, etc.).
Dracut provides a bunch of functions that are specialized for installing various types of files:
inst
, but you can install a whole bunch of files at once. Useful
for installing required binaries. This will abort initramfs creation if
any of the files are missing, unless you use the -o
flag (for
"optional" binaries.)
There are other functions, but these are the really useful ones. See dracut-functions.sh for details.
If a module requires certain kernel modules, it might define
installkernel()
. To install kernel modules you do the following:
=block
or =drivers/usb/storage
.
When the system boots, the kernel unpacks the initramfs and runs /init, which is installed from 99base/init.sh. It runs through a bunch of different phases of operation to bring up the system. In each phase there are hooks, which are places where modules can insert scripts to be run.
Everything that runs in a hook is sourced by init
.
The variables and functions created in each script will be visible in later
scripts. This lets you set a variable in one script and then refer to it later
without needing to save it to a file, but be careful not to overwrite the
variable names used by other scripts.
Here’s a condensed version of how init
runs, for reference. I’ll explain
each step in more detail in the following sections.
cmdline
hook.
DIE if $root
is empty or $rootok
is not 1.
Export root info.
pre-udev
hook. Start udevd.
Run pre-trigger
hook. Run udevadm trigger.
Kernel modules load, udev rules start running.
initqueue/finished
hook succeed,
run initqueue
hooks and subhooks every 0.5 seconds.
DIE after 30 attempts.
pre-mount
hook.
Run mount
hook until $NEWROOT
is usable. DIE after 20 attempts.
Run pre-pivot
hook.
Check for real init
. Drop into emergency shell
if missing.
$NEWROOT
/run, if the latter exists.
Stop logging.
Run cleanup
hook. Drop into shell if rd.break
is set.
switch_root
into $NEWROOT
and start real init
.
This is basically the first thing that happens, and it pulls in all the useful
functions from dracut-lib.sh. You should be using those functions rather
than writing your own strstr()
or splitsep()
methods!
The full list of filesystems that get mounted: /proc, /sys, /dev, /dev/pts, /dev/shm, and /run.
It also creates $UDEVRULESD
and /run/initramfs.
If rd.debug
is set, dracut will be run with bash -x
and all output
will be logged to /run/initramfs/init.log.
All the scripts in the cmdline hook are run at this point. They run in order
of the priority set when they were installed by inst_hook
.
As noted above, the scripts are all sourced by dracut, so they all share the same environment.
Even though /sys is mounted at this point, there’s nothing useful in /sys/class/block or /sys/class/net until udev gets started.
If you need to get some info out of /sys you’ll probably need to schedule a job to run in the mainloop. (See the section called “mainloop”)
If your module is responsible for finding a root device
[2], you must
ensure that root
is non-empty and rootok=1
before the cmdline
hook
ends, or dracut will simply halt.
At this point, dracut exports the following variables:
root rflags fstype netroot NEWROOT
In version 017 and earlier it wrote these variables to /tmp/root.info but that file is no longer written and should be considered deprecated. [3]
This is where all the scripts that generate udev rules (usually named
*-genrules.sh
- for example, 40network/net-genrules.sh
.
Usually, generating the udev rules will look something like this:
printf 'SUBSYSTEM=="block", SYMLINK=="%s", RUN+="/sbin/initqueue --settled --onetime --unique /sbin/some-command $env{DEVNAME}"\n' by-label/$DISKLABEL
In short, this says that when a block device with the label $DISKLABEL appears (and udev is settled), we should run "/sbin/some-command $DEVNAME" - and run it only once.
nearly all the udev rules in dracut use /sbin/initqueue
to quickly
schedule the command to be run in the dracut mainloop and then return. (See
the section called “mainloop”). You should not run commands directly from udev,
especially if they take a non-trivial amount of time.
TODO: more about udev rules TODO: more about using initqueue
This hook runs after udev is started, but before any of the rules run.
This is a good place to set udev properties / environment variables using
udevproperty
.
What actually gets run is:
udevadm control --reload udevadm trigger --type=subsystems --action=add udevadm trigger --type=devices --action=add
which will cause the system to load drivers for basically every device it can find and fire off any applicable udev rules. This is where all the drivers get loaded.
Once the network devices appear, dracut can try bring up the network.
dracut will not bring up the network unless it needs to! In general,
it only brings up the network if your root=
argument is on a network device.
You can force it to bring up the network by adding rd.neednet=1
to the boot
arguments.
Dracut supports a whole bunch of network configuration arguments - see dracut.kernel(7) for details. A couple of common ones:
Try DHCP on every interface (this is the default):
ip=dhcp
Try DHCP on a specific interface:
ip=eth0:dhcp
Static configuration of a specific interface:
ip=192.168.122.100::192.168.122.1:255.255.255.0::eth0:none nameserver=192.168.122.1
as mentioned above, dracut will ignore these arguments unless it needs
to bring up the network to find the root device (or you set rd.neednet=1
).
This is the heart of dracut. Basically, dracut will sit in a loop, waiting for
events and running scripts until all the scripts in the initqueue/finished
hook return success or a timeout value is reached.
The first thing dracut does is run the scripts in the initqueue/finished
hook and check their return values. If all the initqueue/finished
scripts
return successfully, dracut exits the mainloop immediately.
If this happens on the first pass through the mainloop, none of the
other initqueue scripts will run. Make sure you install an initqueue/finished
script to make dracut wait for anything that must run!
If one or more of the initqueue/finished
scripts returns non-zero, we’ll
continue processing. Dracut will then run the scripts in the initqueue
hook.
The scripts in the initqueue
hooks are generally put there by udev rules
running /sbin/initqueue
- see the section called “Run pre-udev
hook (genrules scripts)” above.
After running the hook, dracut checks initqueue/finished
again, and exits
the loop if they all succeed.
Now dracut checks to see if udev is settled - that is, if there are any new
udev events that need processing. If there are, we bounce back to the start of
the mainloop. If not, then we’re settled, and the initqueue/settled
hook
gets run.
As before, after the hook it checks initqueue/finished
and exits if we’re
all done.
Having finished one pass through the loop, it’s time to wait for more events. The mainloop sleeps for half a second, and then we check and increment our loop counter.
If we’ve gotten to the rd.retry
value (default: 20 loops, 10 seconds) then
the initqueue/timeout
hook is run.
If we’ve gotten to twice the rd.retry
value (default: 40 loops, 20 seconds)
then the emergency_shell
function runs with the message "Unable to process
initqueue". Alas!
Now that we’ve got some idea how dracut works, let’s talk a bit about writing good shell code. [4]
TODO