all
Stage 09

SysVinit & Runlevels

Understand SysVinit — the traditional Linux init system used in legacy and minimal embedded systems. Learn /etc/inittab, runlevels, rc.d init scripts, update-rc.d, and OpenRC as a modern alternative.

12 min read
53278 chars

SysVinit Overview

{:.gc-basic}

Basic

SysVinit (System V init) is the classical Unix init system, descended directly from AT&T System V Unix. It was the default PID 1 on virtually every Linux distribution until systemd overtook it around 2011–2015. It remains relevant in:

  • Legacy embedded systems and industrial controllers
  • Debian-based systems using the compatibility layer
  • Minimal containers where systemd overhead is unacceptable
  • Systems using OpenRC (a modern compatible replacement)

PID 1 — The Init Process

The Linux kernel always starts the first userspace process with PID 1. This process is special:

  • It is the ancestor of all other processes
  • It can never be killed by SIGKILL from another process
  • If PID 1 exits, the kernel panics
  • Orphaned processes (whose parent died) are reparented to PID 1 — init reaps them with wait() to prevent zombie accumulation

The kernel searches for the init binary in this order (overridable via init= kernel parameter):

/sbin/init
/etc/init
/bin/init
/bin/sh    ← last resort

Boot Sequence: kernel → SysVinit

Kernel boots
  └── Mounts rootfs (read-only initially)
        └── Executes /sbin/init (PID 1)
              └── Reads /etc/inittab
                    ├── sysinit: runs /etc/init.d/rcS (or rc.sysinit)
                    ├── Transitions to default runlevel
                    │     └── Runs /etc/rc<N>.d/ scripts in order
                    └── Spawns getty processes on terminals

/etc/inittab Format

id:runlevels:action:process
Field Description
id Unique 1–4 character identifier
runlevels Runlevel(s) this entry applies to (blank = all)
action What to do (see table below)
process Command to execute
# Example /etc/inittab for a traditional SysVinit system
id:3:initdefault:

# System initialization
si::sysinit:/etc/init.d/rcS

# Transition to runlevel 3
l3:3:wait:/etc/rc3.d/rc 3

# Virtual consoles
1:2345:respawn:/sbin/getty 38400 tty1
2:23:respawn:/sbin/getty 38400 tty2
3:23:respawn:/sbin/getty 38400 tty3

# Serial console
S0:12345:respawn:/sbin/getty -L ttyS0 115200 vt100

# Ctrl-Alt-Del
ca:12345:ctrlaltdel:/sbin/shutdown -t1 -a -r now

# UPS power failure
pf::powerfail:/sbin/shutdown -h +5 "Power Failure; System Shutting Down"
# UPS power restored within 5 minutes
pr:12345:powerokwait:/sbin/shutdown -c "Power Restored; Shutdown Cancelled"
Action Description
initdefault Sets the default runlevel at boot (no process field)
sysinit Run before entering any runlevel; init waits for completion
bootwait Run during boot; init waits
boot Run during boot; init does not wait
wait Run when entering runlevel; init waits
once Run once when entering runlevel; no wait
respawn Restart whenever the process exits
ondemand Like respawn but only for runlevels a, b, c
ctrlaltdel Triggered by Ctrl-Alt-Del
powerfail Triggered by SIGPWR (UPS event)
powerokwait Triggered when power is restored
off Do nothing

Runlevels

{:.gc-basic}

Basic

A runlevel is a named machine state that defines which services are running. SysVinit defines 7 runlevels (0–6), though their exact meaning varies by distribution.

Standard Runlevel Meanings

Runlevel Standard Debian/Ubuntu Red Hat/RHEL/CentOS
0 Halt Power off Power off
1 Single-user Single-user (maintenance) Single-user (maintenance)
2 Multi-user (no network) Multi-user + networking Multi-user, no NFS
3 Multi-user + network Same as 2 (no distinction) Multi-user + network, text mode
4 Undefined Same as 2 Undefined / user-defined
5 Multi-user + GUI Same as 2 Multi-user + network + GUI
6 Reboot Reboot Reboot

Key distinction: Debian/Ubuntu use runlevels 2–5 identically (all are full multi-user). RHEL separates runlevel 3 (text) and 5 (graphical).

Querying and Changing Runlevels

# Check current and previous runlevel
runlevel
# Output: N 3  (N = previous, 3 = current; N means "none" at boot)

# Switch runlevel immediately (like telinit)
telinit 3
telinit 1        # drop to single-user / maintenance mode
telinit 0        # halt
telinit 6        # reboot

# init accepts a signal: SIGHUP causes it to re-read /etc/inittab
kill -HUP 1

For each runlevel N, init runs /etc/rc<N>.d/ scripts in alphabetical order:

/etc/rc3.d/
├── K01bluetooth -> ../init.d/bluetooth
├── K05nfs-common -> ../init.d/nfs-common
├── S10network -> ../init.d/networking
├── S20syslog -> ../init.d/rsyslog
├── S50ssh -> ../init.d/ssh
└── S80apache2 -> ../init.d/apache2
  • K prefix — Kill scripts: run with argument stop when leaving this runlevel
  • S prefix — Start scripts: run with argument start when entering this runlevel
  • Two-digit number — determines execution order (00–99, lower = earlier)

When transitioning from runlevel A to runlevel B:

  1. All K scripts in /etc/rcB.d/ run (stopping services not wanted in B)
  2. All S scripts in /etc/rcB.d/ run (starting services wanted in B)

Writing Init Scripts

{:.gc-mid}

Intermediate

SysVinit scripts follow the LSB (Linux Standard Base) format. They live in /etc/init.d/ and accept start, stop, restart, reload, and status arguments.

LSB Header

Every init script must begin with an LSB header that update-rc.d and dependency solvers read:

#!/bin/sh
### BEGIN INIT INFO
# Provides:          myapp
# Required-Start:    $remote_fs $syslog $network
# Required-Stop:     $remote_fs $syslog $network
# Default-Start:     2 3 4 5
# Default-Stop:      0 1 6
# Short-Description: My embedded application daemon
# Description:       Long description of what myapp does,
#                    can span multiple lines with a leading #  space.
### END INIT INFO

Virtual facilities (starting with $):

Facility Meaning
$local_fs Local filesystems are mounted
$remote_fs Remote filesystems (NFS) are mounted
$network Network interfaces are configured
$syslog Syslog daemon is running
$time System time has been set
$named DNS resolver is available

Complete Init Script Example

#!/bin/sh
### BEGIN INIT INFO
# Provides:          myapp
# Required-Start:    $remote_fs $syslog $network
# Required-Stop:     $remote_fs $syslog $network
# Default-Start:     2 3 4 5
# Default-Stop:      0 1 6
# Short-Description: My embedded application
### END INIT INFO

# Source LSB init functions (Debian/Ubuntu)
. /lib/lsb/init-functions

NAME=myapp
DAEMON=/opt/myapp/bin/myapp
DAEMON_ARGS="--config /etc/myapp/config.toml --daemonize"
PIDFILE=/var/run/$NAME.pid
SCRIPTNAME=/etc/init.d/$NAME

# Check the binary exists
[ -x "$DAEMON" ] || exit 0

do_start() {
    # Return:
    #   0 — daemon has been started
    #   1 — daemon was already running
    #   2 — daemon could not be started
    start-stop-daemon --start --quiet --pidfile $PIDFILE \
        --exec $DAEMON --test > /dev/null || return 1
    start-stop-daemon --start --quiet --pidfile $PIDFILE \
        --exec $DAEMON -- $DAEMON_ARGS || return 2
}

do_stop() {
    # Return:
    #   0 — daemon has been stopped
    #   1 — daemon was already stopped
    #   2 — daemon could not be stopped
    start-stop-daemon --stop --quiet --retry=TERM/30/KILL/5 \
        --pidfile $PIDFILE --name $NAME
    RETVAL="$?"
    [ "$RETVAL" = 2 ] && return 2
    rm -f $PIDFILE
    return "$RETVAL"
}

do_reload() {
    # Send SIGHUP to reload config without full restart
    start-stop-daemon --stop --signal HUP --quiet \
        --pidfile $PIDFILE --name $NAME
    return 0
}

case "$1" in
    start)
        log_daemon_msg "Starting $NAME" "$NAME"
        do_start
        case "$?" in
            0|1) log_end_msg 0 ;;
            *)   log_end_msg 1 ;;
        esac
        ;;
    stop)
        log_daemon_msg "Stopping $NAME" "$NAME"
        do_stop
        case "$?" in
            0|1) log_end_msg 0 ;;
            *)   log_end_msg 1 ;;
        esac
        ;;
    reload)
        log_daemon_msg "Reloading $NAME" "$NAME"
        do_reload
        log_end_msg $?
        ;;
    restart)
        log_daemon_msg "Restarting $NAME" "$NAME"
        do_stop
        case "$?" in
            0|1)
                do_start
                case "$?" in
                    0) log_end_msg 0 ;;
                    *) log_end_msg 1 ;;
                esac
                ;;
            *)  log_end_msg 1 ;;
        esac
        ;;
    status)
        status_of_proc "$DAEMON" "$NAME" && exit 0 || exit $?
        ;;
    *)
        echo "Usage: $SCRIPTNAME {start|stop|restart|reload|status}" >&2
        exit 3
        ;;
esac

exit 0

LSB Return Codes

Code Meaning for start Meaning for stop
0 Success Stopped successfully
1 Generic failure Stopping failed
2 Invalid argument
5 Not installed

update-rc.d and chkconfig

{:.gc-mid}

Intermediate

Debian/Ubuntu — update-rc.d

# Install with LSB header defaults (reads Default-Start/Stop from header)
update-rc.d myapp defaults

# Explicit start/stop priority and runlevels
update-rc.d myapp start 80 2 3 4 5 . stop 20 0 1 6 .

# Remove all symlinks (does NOT delete /etc/init.d/myapp)
update-rc.d myapp remove

# Disable without removing (keeps symlinks but renames S→K)
update-rc.d myapp disable

# Re-enable
update-rc.d myapp enable

Red Hat / SUSE — chkconfig

# Add service (reads chkconfig header comment)
chkconfig --add myapp

# Enable for runlevels 3 and 5
chkconfig --level 35 myapp on

# Disable
chkconfig --level 35 myapp off

# List all services and their runlevel state
chkconfig --list

# List specific service
chkconfig --list myapp
# myapp     0:off  1:off  2:off  3:on   4:off  5:on   6:off
# Show what runs in runlevel 3
ls -la /etc/rc3.d/

# Find which runlevels a service is enabled in
find /etc/rc*.d -name '*myapp' -ls

OpenRC

{:.gc-adv}

Advanced

OpenRC is a dependency-based init system that is compatible with SysVinit scripts but adds proper dependency resolution, parallel service startup, and cleaner script helpers. It is used by:

  • Alpine Linux (the primary embedded/container distro using OpenRC)
  • Gentoo Linux
  • Various embedded and router firmware distributions

OpenRC is not a replacement for PID 1 itself — it still uses /sbin/init from sysvinit (or its own openrc-init) as PID 1.

OpenRC Service Script Format

#!/sbin/openrc-run
# /etc/init.d/myapp

description="My embedded application"
command="/opt/myapp/bin/myapp"
command_args="--config /etc/myapp/config.toml"
pidfile="/run/${RC_SVCNAME}.pid"
command_background=true   # run in background, creates pidfile automatically

# Dependencies
depend() {
    need net
    need localmount
    after logger
    use dns
}

start_pre() {
    # Runs before start(); return non-zero to abort
    checkpath --directory --owner myapp:myapp --mode 0750 /var/lib/myapp
    checkpath --directory --owner myapp:myapp --mode 0750 /var/log/myapp
}

start() {
    ebegin "Starting ${RC_SVCNAME}"
    start-stop-daemon --start \
        --pidfile "${pidfile}" \
        --user myapp \
        --exec "${command}" \
        -- ${command_args}
    eend $?
}

stop() {
    ebegin "Stopping ${RC_SVCNAME}"
    start-stop-daemon --stop \
        --pidfile "${pidfile}" \
        --retry TERM/10/KILL/5
    eend $?
}

Managing Services with OpenRC

# Start / stop / restart / status
rc-service myapp start
rc-service myapp stop
rc-service myapp restart
rc-service myapp status

# Add to a runlevel (equivalent to update-rc.d defaults)
rc-update add myapp default
rc-update add myapp boot      # for very early services

# Remove from runlevel
rc-update delete myapp default

# Show all services and their runlevel assignments
rc-update show

# Change runlevel interactively
openrc default    # transition to default runlevel
openrc boot       # transition to boot runlevel

OpenRC Runlevels

OpenRC Runlevel Description
sysinit Kernel filesystems, udev
boot Logging, hostname, clock, root filesystem check
default Normal multi-user operation
nonetwork Like default but without networking
shutdown Shutdown sequence
reboot Reboot sequence

Comparison: SysVinit vs systemd vs BusyBox init

{:.gc-adv}

Advanced

Feature SysVinit OpenRC BusyBox init systemd
Parallel service start No Yes (with rc_parallel=YES) No Yes
Socket activation No No No Yes
cgroup integration No Optional No Yes (mandatory)
Service supervision (auto-restart) Manual (respawn in inittab) No respawn in inittab Yes (Restart=)
On-demand/lazy start No No No Yes (socket/path units)
Structured logging No No No Yes (journald)
Watchdog support No No No Yes (sd_notify)
Dependency graph No Yes No Yes
Rootfs size impact ~150 KB ~300 KB ~500 KB (all utils) 3–10 MB
RAM footprint ~1 MB ~2 MB ~2 MB ~20–50 MB
Container friendly Partly Yes (Alpine) Yes Requires cgroups v2
Suitable for production embedded Simple devices Yes Tiny devices Full-featured SBCs

When to Choose Each

BusyBox init: Targets with under 64 MB RAM, no complex service dependencies, or when booting in under 500 ms is a hard requirement.

SysVinit / OpenRC: Legacy compatibility, Alpine Linux containers, Gentoo-based embedded systems, or when you want dependency ordering without systemd’s footprint.

systemd: Yocto/OpenEmbedded targets, ARM SBCs (Raspberry Pi, BeagleBone), any system with SSH, D-Bus, NetworkManager, or complex service interdependencies.


Interview Q&A

{:.gc-iq}

Interview Q&A

Q1 — What happens to orphaned processes in Linux, and what is init’s role?

When a process exits before its child, the child becomes an orphan. The kernel automatically reparents orphaned processes to PID 1 (init). Init periodically calls wait() to collect exit status from zombie children, preventing zombie accumulation. This is why PID 1 must never block indefinitely — it must always be able to call waitpid(). On systems using nohup or disown in shell sessions, processes are also reparented to init.

Q2 — What is the difference between K and S symlinks in /etc/rc<N>.d/?

S (Start) scripts are executed with argument start when entering the runlevel. K (Kill) scripts are executed with argument stop when entering the runlevel — they stop services that should not be running at that level. The two-digit number controls execution order: lower numbers run first. K scripts run before S scripts during a runlevel transition.

Q3 — When is single-user mode (runlevel 1) used?

Single-user mode boots with only essential services, no network, and a root shell. It is used for: system recovery (filesystem check with fsck, recovering from a bad fstab), password reset (bypasses PAM authentication), and diagnosing startup failures by preventing normal services from starting. On modern systems it maps to rescue.target in systemd.

Q4 — What do wall and shutdown commands do, and how are they related to init?

shutdown sends a message to all logged-in users via wall (write all), then transitions the system to runlevel 0 (halt) or 6 (reboot) by calling telinit. The -t flag controls how long between sending SIGTERM and SIGKILL to processes. wall alone just broadcasts a message without shutting down. Both require root privileges.

Q5 — Explain the respawn action in /etc/inittab and a practical use case.

respawn causes init to restart the listed process whenever it exits, regardless of exit code. The primary use case is getty — the program that presents a login prompt on a terminal. When a user logs in, getty exits and the shell takes over; when the shell exits (user logs out), init immediately spawns a new getty so the next user can log in. Without respawn, the terminal would go dark after the first user logs out.

Q6 — How would you add a new service to start at boot on a Debian SysVinit system?

# 1. Create the init script
vim /etc/init.d/myservice    # with proper LSB header

# 2. Make it executable
chmod +x /etc/init.d/myservice

# 3. Register it — reads Default-Start/Stop from LSB header
update-rc.d myservice defaults

# 4. Verify
ls /etc/rc2.d/ | grep myservice
# S20myservice -> ../init.d/myservice

# 5. Test
/etc/init.d/myservice start
/etc/init.d/myservice status

Q7 — What does /etc/inittab’s initdefault action do, and what happens if it is missing?

The initdefault entry sets the runlevel that init transitions to after the sysinit and boot phases complete. The process field is ignored. If initdefault is missing from /etc/inittab, init interactively asks the operator to type a runlevel at the console — on a headless embedded system this causes the boot to hang indefinitely until a console is connected and a runlevel entered.


References

{:.gc-ref}

References

Resource Link
SysVinit source and documentation savannah.nongnu.org/projects/sysvinit
LSB Init Script Standard refspecs.linuxbase.org/LSB_5.0.0/LSB-Core-generic/LSB-Core-generic/iniscrptfunc.html
OpenRC Documentation github.com/OpenRC/openrc/blob/master/README.md
Alpine Linux OpenRC Wiki wiki.alpinelinux.org/wiki/OpenRC
man 8 init SysVinit init manual page
man 5 inittab inittab format reference
man 8 update-rc.d Debian service registration tool
man 8 chkconfig Red Hat service management
Debian Policy — System Run Levels debian.org/doc/debian-policy/ch-opersys.html