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
SIGKILLfrom 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
/etc/rc<N>.d/ Symlink Convention
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
Kprefix — Kill scripts: run with argumentstopwhen leaving this runlevelSprefix — Start scripts: run with argumentstartwhen entering this runlevel- Two-digit number — determines execution order (00–99, lower = earlier)
When transitioning from runlevel A to runlevel B:
- All K scripts in
/etc/rcB.d/run (stopping services not wanted in B) - 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
Inspecting the rc.d Symlinks Directly
# 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 callwaitpid(). On systems usingnohupordisownin 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
startwhen entering the runlevel. K (Kill) scripts are executed with argumentstopwhen 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 badfstab), password reset (bypasses PAM authentication), and diagnosing startup failures by preventing normal services from starting. On modern systems it maps torescue.targetin systemd.
Q4 — What do wall and shutdown commands do, and how are they related to init?
shutdownsends a message to all logged-in users viawall(write all), then transitions the system to runlevel 0 (halt) or 6 (reboot) by callingtelinit. The-tflag controls how long between sending SIGTERM and SIGKILL to processes.wallalone just broadcasts a message without shutting down. Both require root privileges.
Q5 — Explain the respawn action in /etc/inittab and a practical use case.
respawncauses 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. Withoutrespawn, 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
initdefaultentry sets the runlevel that init transitions to after thesysinitandbootphases complete. Theprocessfield is ignored. Ifinitdefaultis 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 |