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 fsck on 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:

  1. The kernel starts /sbin/init (PID 1)
  2. init reads /etc/inittab
  3. inittab says: "default runlevel is 3"
  4. init runs /etc/rc.d/init.d/rc 3
  5. The rc script looks in /etc/rc.d/rc3.d/
  6. It finds symlinks like S10network, S20syslog, etc.
  7. It runs each S* script with the start argument, 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:

  • S prefix = Start this service
  • K prefix = 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 the S (sysinit) scripts
  • l0 through l6 — for each runlevel, run the corresponding rc script and wait for it to finish
  • ca:12345:ctrlaltdel: — Ctrl+Alt+Del triggers a reboot (nice to have)
  • su and s1 — single-user mode gets a root login prompt via sulogin
  • Lines 1 through 6 — spawn agetty on 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/bin and /usr/sbin. That's it. No /usr/local/bin mystery 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
  • Debuggablebash -x /etc/rc.d/init.d/network start shows 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 journalctl output 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