Part 10 of 12 in the Linux From Scratch series.
Previous: The Final Packages · Next: Compiling the Kernel
We have a complete userspace. Every binary, every library, every tool — compiled, installed, ready. But right now our system is inert. It's a collection of files on a disk. Nothing knows how to start.
That changes now. Chapter 9 of LFS is about the bootscripts and configuration files that transform a pile of software into a system that actually boots, configures itself, and presents you with a login prompt.
This is the nervous system. The wiring between init (PID 1) and everything else.
LFS-Bootscripts: The Glue
The LFS-Bootscripts package is a collection of shell scripts. That's it. No daemons, no frameworks, no abstractions. Just shell scripts that get called at the right time to do the right thing.
After installation, /etc/rc.d/init.d/ contains:
- checkfs — runs
fsckon filesystems before mounting them - cleanfs — removes stale files (
/run,/tmp) on boot - console — sets up the Linux console (keymap, font)
- halt — shuts the system down
- localnet — brings up the loopback interface (
lo) - modules — loads kernel modules listed in
/etc/sysconfig/modules - mountfs — mounts all filesystems from
/etc/fstab - mountvirtfs — mounts virtual filesystems (
/proc,/sys,/dev) - network — brings up network interfaces
- rc — the master script that orchestrates runlevel transitions
- reboot — reboots the system
- sendsignals — sends TERM and KILL signals to all processes during shutdown
- setclock — sets the system clock from the hardware clock
- swap — enables swap partitions
- sysctl — applies kernel parameters from
/etc/sysctl.conf - sysklogd — starts the system logger
- udev / udev_retry — starts udev and retries failed device events
Every one of these is a plain shell script you can read in five minutes. No magic. No binary blobs. If something goes wrong at boot, you open the script in vim and read it.
How SysV Init Actually Works
Here's the boot sequence from init's perspective:
- The kernel starts
/sbin/init(PID 1) - init reads
/etc/inittab - inittab says: "default runlevel is 3"
- init runs
/etc/rc.d/init.d/rc 3 - The
rcscript looks in/etc/rc.d/rc3.d/ - It finds symlinks like
S10network,S20syslog, etc. - It runs each
S*script with thestartargument, in numerical order
That's it. The entire init system is: read a config, find some symlinks, run them in order.
Runlevels
Runlevels are just numbered states:
| Runlevel | Meaning |
|---|---|
| 0 | Halt |
| 1 | Single-user (rescue) |
| 2 | Multi-user, no network |
| 3 | Multi-user with network (our default) |
| 4 | User-definable |
| 5 | Multi-user with GUI |
| 6 | Reboot |
Each runlevel has a directory: rc0.d, rc1.d, ..., rc6.d. Each directory contains symlinks to the scripts in init.d/.
The naming convention is everything:
Sprefix = Start this serviceKprefix = Kill (stop) this service- The two-digit number = order (lower numbers run first)
So rc3.d/S10network means "when entering runlevel 3, start the network script, and do it 10th." And rc0.d/K10network means "when halting, stop the network."
When transitioning between runlevels, init runs K* scripts (with stop) for the old runlevel, then S* scripts (with start) for the new one. Elegant. Predictable. Debuggable.
/etc/inittab: The Master Config
# Begin /etc/inittab
id:3:initdefault:
si::sysinit:/etc/rc.d/init.d/rc S
l0:0:wait:/etc/rc.d/init.d/rc 0
l1:1:wait:/etc/rc.d/init.d/rc 1
l2:2:wait:/etc/rc.d/init.d/rc 2
l3:3:wait:/etc/rc.d/init.d/rc 3
l4:4:wait:/etc/rc.d/init.d/rc 4
l5:5:wait:/etc/rc.d/init.d/rc 5
l6:6:wait:/etc/rc.d/init.d/rc 6
ca:12345:ctrlaltdel:/sbin/shutdown -t1 -a -r now
su:S06:once:/sbin/sulogin
s1:1:respawn:/sbin/sulogin
1:2345:respawn:/sbin/agetty --noclear tty1 9600
2:2345:respawn:/sbin/agetty tty2 9600
3:2345:respawn:/sbin/agetty tty3 9600
4:2345:respawn:/sbin/agetty tty4 9600
5:2345:respawn:/sbin/agetty tty5 9600
6:2345:respawn:/sbin/agetty tty6 9600
# End /etc/inittab
Line by line:
id:3:initdefault:— boot to runlevel 3 (multi-user with network)si::sysinit:— first thing: run theS(sysinit) scriptsl0throughl6— for each runlevel, run the corresponding rc script and wait for it to finishca:12345:ctrlaltdel:— Ctrl+Alt+Del triggers a reboot (nice to have)suands1— single-user mode gets a root login prompt viasulogin- Lines
1through6— spawnagettyon six virtual terminals (tty1-tty6), respawn if they die
The respawn keyword is important. It means if the process exits, init restarts it. That's why you always get a login prompt back after logging out — init re-spawns agetty on that tty.
Six terminals. Alt+F1 through Alt+F6 to switch between them. No GUI needed.
Network Configuration
LFS uses static network configuration. No NetworkManager, no DHCP client (unless you add one). Direct, explicit config files.
/etc/sysconfig/ifconfig.eth0
ONBOOT=yes
IFACE=eth0
SERVICE=ipv4-static
IP=192.168.1.100
GATEWAY=192.168.1.1
PREFIX=24
BROADCAST=192.168.1.255
The network bootscript reads this file. ONBOOT=yes means bring it up at boot. SERVICE=ipv4-static tells it which helper script to use. The rest is standard networking.
For our VM, the actual values depend on your virtual network setup. The point is: you see exactly what's configured. No "magic" DHCP that silently assigns addresses. You chose every number.
/etc/resolv.conf
nameserver 8.8.8.8
nameserver 8.8.4.4
DNS resolution. When your system needs to resolve google.com, the resolver library reads this file and asks these nameservers. Google's public DNS. Simple.
/etc/hostname
lfs
One word. That's your machine's name. The localnet bootscript reads this and sets the hostname via hostname lfs.
/etc/hosts
127.0.0.1 localhost
127.0.1.1 lfs
::1 localhost ip6-localhost ip6-loopback
Local name resolution. Before DNS is queried, the system checks this file. localhost always resolves to 127.0.0.1. Your hostname resolves locally too.
/etc/profile: Your Shell Environment
# Begin /etc/profile
export LANG=en_US.UTF-8
pathremove() {
local IFS=':'
local NEWPATH
local DIR
local PATHVARIABLE=${2:-PATH}
for DIR in ${!PATHVARIABLE}; do
if [ "$DIR" != "$1" ]; then
NEWPATH=${NEWPATH:+$NEWPATH:}$DIR
fi
done
export $PATHVARIABLE="$NEWPATH"
}
pathprepend() {
pathremove $1 $2
local PATHVARIABLE=${2:-PATH}
export $PATHVARIABLE="$1${!PATHVARIABLE:+:${!PATHVARIABLE}}"
}
pathappend() {
pathremove $1 $2
local PATHVARIABLE=${2:-PATH}
export $PATHVARIABLE="${!PATHVARIABLE:+${!PATHVARIABLE}:}$1"
}
export -f pathremove pathprepend pathappend
export PATH=/usr/bin:/usr/sbin
# Set the umask
umask 022
# End /etc/profile
Every login shell sources this. It sets:
- LANG — UTF-8 locale. Your terminal can display international characters.
- PATH — where the shell looks for commands.
/usr/binand/usr/sbin. That's it. No/usr/local/binmystery paths. - umask 022 — new files get 644 permissions (owner read/write, everyone else read-only). New directories get 755. A sane default.
The pathremove, pathprepend, pathappend functions are utilities for cleanly manipulating PATH without duplicates. Thoughtful.
/etc/inputrc: Readline Configuration
# Begin /etc/inputrc
set horizontal-scroll-mode Off
set meta-flag On
set input-meta On
set convert-meta Off
set output-meta On
set bell-style none
"\eOd": backward-word
"\eOc": forward-word
"\e[Home": beginning-of-line
"\e[End": end-of-line
"\e[5~": beginning-of-history
"\e[6~": end-of-history
"\e[3~": delete-char
"\e[2~": quoted-insert
# End /etc/inputrc
This configures GNU Readline — the library that handles line editing in Bash (and many other programs). Arrow keys, Home/End, Delete, word-jumping with Ctrl+arrows. Without this file, your terminal input handling would be bare-bones.
bell-style none is a mercy. No beeping on tab completion failures.
/etc/shells: The Gatekeeper
/bin/sh
/bin/bash
This file lists valid login shells. It's a security mechanism. Programs like chsh (change shell) and FTP daemons check this list. If a shell isn't listed here, it can't be used as a login shell.
Right now we have Bash and its sh symlink. If you later add zsh or fish, you'd add them here.
The SysV vs systemd Question
You might wonder why LFS uses SysV init instead of systemd. It's a deliberate choice.
SysV init is:
- Transparent — every script is readable shell code
- Simple — symlinks determine boot order
- Debuggable —
bash -x /etc/rc.d/init.d/network startshows you exactly what happens - Educational — you understand the entire boot process
systemd is:
- Faster — parallel startup, socket activation
- Feature-rich — cgroups, journal, timers, container support
- Complex — binary logs, D-Bus dependencies, unit files with their own syntax
- Opaque — when something fails, you're reading
journalctloutput and hoping
For a production server, systemd's features matter. For learning how Linux works, SysV init is unbeatable. You can hold the entire boot process in your head. Every step is a shell script you wrote or installed.
That's the LFS philosophy. Not "SysV is better" — but "SysV teaches you more."
What We Built
Take a step back. We now have:
- Boot scripts that bring the system up and shut it down
- An init configuration that defines runlevels and spawns terminals
- Network configuration with static IP, DNS, and hostname
- A shell environment with proper PATH and locale
- Terminal input handling via readline
- Security via
/etc/shells
Our system isn't just a collection of binaries anymore. It's configured. It knows how to start, how to set up networking, how to present a login prompt.
One thing remains: the kernel and bootloader. That's next.
Previous: The Final Packages · Next: Compiling the Kernel
All posts in this series: Linux From Scratch