Post

Linux - Systemd Architecture: Unit Lifecycle, Overrides, Timers, and Resource Isolation

Linux - Systemd Architecture: Unit Lifecycle, Overrides, Timers, and Resource Isolation

The Paradigm Shift: Beyond the Shell Script Init

For decades, the standard for Unix system initialisation was the SysV init system. It was simple, treating process startup as a sequential pipeline of shell scripts located in /etc/init.d/. While predictable, SysV init acted as a rigid blocker to system parallelisation. If a network mount took thirty seconds to resolve, the entire boot sequence stalled. Process monitoring was virtually non-existent; if a daemon crashed after backgrounding itself, the init system remained blissfully unaware, leaving administrators to hack together fragile PID-checking loop scripts in cron.

Systemd represents the mandatory transition from hardware-centric sequential execution to a declarative, state-aware concurrency engine. By focusing on resource abstraction rather than script sequencing, systemd views the operating system as a comprehensive graph of inter-dependent resources.

For an enterprise infrastructure administrator, mastering systemd is not merely about learning how to run systemctl restart. It requires a deep understanding of unit mechanics, event-driven triggers, transient workspaces, and cgroup-level resource isolation. This guide explores the complete architecture of systemd, detailing how to design, override, schedule, and harden services for high-concurrency production environments.


1. Unit File Anatomy and Dependency Graph Execution

At the core of systemd is the concept of a Unit. Everything from a background process (.service) and a hardware device (.device) to a filesystem mount point (.mount) or a network socket (.socket) is defined declaratively as a unit file.

Systemd Unit File Hierarchies

Unit files reside in three primary directories on the filesystem, evaluated in strict order of precedence:

  1. /lib/systemd/system/ (or /usr/lib/systemd/system/): Vendor-packaged defaults. Never edit files in this directory, as package managers will overwrite them during updates.
  2. /etc/systemd/system/: Local administrative customisations and custom unit files. This directory overrides the vendor directory.
  3. /run/systemd/system/: Runtime transient units created dynamically at runtime. These evaporate upon reboot.

Designing a Professional Service Unit

A pristine, highly functional unit file avoids shell manipulation and declares explicit dependency logic. Let us dissect a production-grade service unit for a custom internal Go API (telemetry-api.service):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
[Unit]
Description=Telemetry Processing Ingestion API
Documentation=https://docs.internal.network/telemetry
After=network-online.target mariadb.service
Wants=network-online.target
Requires=mariadb.service

[Service]
Type=simple
WorkingDirectory=/opt/telemetry
ExecStart=/opt/telemetry/bin/api-server --config=/etc/telemetry/config.yaml
Restart=on-failure
RestartSec=5s

# Resource Constraints
MemoryMax=4Gi
CPUWeight=100

[Install]
WantedBy=multi-user.target

Deconstructing the Dependency Directives

Understanding how systemd orders and requires units prevents subtle “race conditions” during boot cycles:

  • Wants=: A loose dependency declaration. If telemetry-api.service is started, systemd will attempt to start network-online.target in parallel. If the network target fails to start, the API service will still proceed.
  • Requires=: A strict dependency constraint. If mariadb.service fails to initialize or is stopped, telemetry-api.service is immediately deactivated or blocked from starting.
  • **Before= and After=**: Ordering directives. These are entirely orthogonal to dependency declarations. Declaring After=mariadb.service ensures that systemd waits for the MariaDB service to report itself healthy before executing the binary for our API, preventing instantaneous connection failures on boot.

2. Drop-in Overrides: Clean Administrative Customisation

A common administrative failure is modifying package-managed unit files directly inside /lib/systemd/system/. When a security patch updates that software package, your custom changes are quietly wiped out, resulting in unannounced production outages.

The Mechanism of the Drop-In File

The clean way to alter a service is by using Drop-in Overrides. These are targeted configuration blocks located inside a dedicated directory matching the pattern /etc/systemd/system/<unit>.service.d/.

Systemd natively provides a command-line wrapper to manage this directory hierarchy cleanly without manual file generation:

1
2
sudo systemctl edit telemetry-api.service

This opens your default environment text editor and builds a blank override.conf file. When you save and exit, systemd automatically detects the modification and reloads its internal state daemon.

The ‘Empty Value’ Reset Trap

When overriding parameters that accept a list of entries—such as ExecStart=, ExecStartPre=, or EnvironmentFile=—simply adding a new line will append the value rather than replace it. For fields like ExecStart=, which only allow a single command entry in standard service types, this causes a critical configuration validation error.

To replace a parameter cleanly, you must first declare the directive with an empty value to wipe the internal systemd array, then declare your new configuration on the subsequent line:

1
2
3
4
5
6
[Service]
# Clear the original ExecStart command
ExecStart=
# Inject the modified execution string with verbose logging enabled
ExecStart=/opt/telemetry/bin/api-server --config=/etc/telemetry/config.yaml --verbose

Verifying the Consolidated Configuration

To inspect the absolute state of a unit after all overrides have been structurally compiled, use the systemctl show command or inspect the file layout using systemctl status:

1
2
systemctl show telemetry-api.service -p ExecStart

Alternatively, the systemd-delta utility allows you to audit every single override, modification, and divergence from vendor defaults across your entire machine:

1
2
systemd-delta --type=extendeded


3. High-Precision Automation: Replacing Cron with Systemd Timers

Traditional cron is a blunt tool. It operates with a minimum granularity of one minute, logs output obscurely to general syslog buffers, lacks built-in locking constraints to prevent overlapping execution runs, and handles missed executions (due to system downtime) poorly.

Systemd Timers provide a native scheduling engine paired directly with systemd service units. Every timer acts upon a target service file of the exact same name (unless explicitly overridden via the Unit= directive).

Types of Scheduled Triggers

  1. Realtime Timers (Wall-Clock): Activated by calendar events, functioning similarly to cron. Defined via the OnCalendar= directive.
  2. Monotonic Timers (Relative): Activated relative to varying system state transitions, such as system boot up (OnBootSec=) or since the service unit itself was last deactivated (OnUnitInactiveSec=).

Implementation: Building an Atomic Database Backup Schedule

Let us build a bulletproof database pruning job that triggers every night at 02:00 UTC, ensuring that if the server is offline during that window, the job executes immediately upon system wake up.

First, define the execution unit file that handles the script payload:

1
2
3
4
5
6
7
8
9
10
[Unit]
Description=Nightly Database Maintenance and Pruning Task
After=mariadb.service

[Service]
Type=oneshot
ExecStart=/opt/telemetry/scripts/db-prune.sh
User=postgres
Group=postgres

Next, create the corresponding schedule control unit (.timer):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[Unit]
Description=Run Nightly Database Maintenance Task
ConditionPathExists=/opt/telemetry/scripts/db-prune.sh

[Timer]
# Trigger daily at exactly 02:00:00 UTC
OnCalendar=*-*-* 02:00:00
# If the machine was powered off during the execution window, run immediately on boot
Persistent=true
# Define a randomized window delay to prevent multiple timers from resource-starving the host
RandomizedDelaySec=15m
# Enforce microsecond precision boundary handling
AccuracySec=1us

[Install]
WantedBy=timers.target

To activate and register this scheduled task into the graph fabric:

1
2
3
sudo systemctl daemon-reload
sudo systemctl enable --now db-maintenance.timer

Reviewing the Global Schedule

To monitor and track all active schedules, execution histories, and remaining times until subsequent iterations across the node:

1
2
systemctl list-timers --all


4. Event-Driven Execution: Path and Socket Activation

Systemd can lazily instantiate processes or respond dynamically to internal system actions, moving away from resource-heavy daemons that run constantly waiting for an event.

Path Activation (.path)

Path units allow systemd to monitor explicit paths on the local filesystem using Linux kernel inotify mechanisms. When a file undergoes alteration, truncation, creation, or deletion, systemd instantly kicks off a corresponding service file.

Imagine an automated processing pipeline where log uploads to a specific directory require triggering an indexing script.

1
2
3
4
5
6
7
8
9
10
11
12
[Unit]
Description=Monitor Ingestion Directory for Telemetry Blobs

[Path]
# Monitor the path for new files or modifications closing their file descriptors
PathChanged=/var/payload/ingest/
# Optional: Ensure the folder is fully available on disk
DirectoryNotEmpty=/var/payload/ingest/

[Install]
WantedBy=multi-user.target

Socket Activation (.socket)

Socket activation allows systemd to bind directly to a network port or a Unix domain socket during the boot sequence. The actual application daemon does not start up, meaning it consumes zero system memory.

When a client hits that network port, the systemd supervisor holds the file descriptor in memory, starts up the target process on-demand, and seamlessly passes the connection handle down to the freshly spawned child process.

1
2
3
4
5
6
7
8
9
10
11
12
[Unit]
Description=Network Ingestion Portal Socket Definition

[Socket]
ListenStream=0.0.0.0:8443
# Enforce high-concurrency socket configurations
Backlog=4096
NoDelay=true

[Install]
WantedBy=sockets.target

The underlying application must be designed to inherit this file descriptor directly from the system supervisor (typically passed down as standard descriptor 3), removing network binding logic from your application code completely.


5. Sandboxing and Service Hardening Directives

Running daemons with broad privileges exposes the entire operating system to compromise if an application execution vulnerability is uncovered. Systemd bridges the gap between traditional access controls and full containerisation by providing advanced sandboxing kernel abstractions directly in the unit configuration block.

By adding these constraints inside your [Service] directive, you cleanly enforce the principle of least privilege:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
[Service]
# User Isolation
User=telemetry-worker
Group=telemetry-worker

# Filesystem Sandboxing
ProtectSystem=strict
ProtectHome=true
ReadOnlyPaths=/usr/bin /usr/lib
ReadWritePaths=/var/log/telemetry/ /opt/telemetry/tmp/
PrivateTmp=true

# Kernel and Device Lockdown
NoNewPrivileges=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX

# System Call Filtering
SystemCallFilter=@system-service ~@privileged

Deep Dive into Isolation Flags

  • ProtectSystem=strict: Mounts the entire OS file tree (/usr, /boot, /etc) as entirely read-only to the process context. The process can only interact with explicitly declared write locations using ReadWritePaths=.
  • PrivateTmp=true: Generates an completely isolated file namespace for the service instance under /tmp/systemd-private-*-*, rendering it mathematically impossible for an attacked service to read or write to temp files shared by other local server operations.
  • NoNewPrivileges=true: Sets the PR_SET_NO_NEW_PRIVS kernel flag. This prevents your service or any child processes from escalating their privileges via setuid binaries or file capabilities.
  • SystemCallFilter=: Intercepts the system call layer using seccomp. In the example above, the service is granted access to common standard utilities (@system-service) while explicitly blocking dangerous instructions like kernel parameter modifications or direct hardware changes (~@privileged).

6. Resource Control and Control Group (Cgroups v2) Integration

Systemd maps every execution stream directly into a unified hierarchical tree structure via the Linux Cgroups v2 architecture. This mechanism ensures that child workers spawned by an application cannot escape the boundaries assigned to their parent processes.

Investigating the Live Group Topology

To visualize exactly how your memory and processor load are distributed across services in real time, use the tracking tool built directly into the supervisor:

1
2
systemd-cgls

For an interactive, top-like view of resource consumption mapped directly to specific unit boundaries:

1
2
systemd-cgtop

Implementing Resource Ceilings and Multi-Tenant Isolation

When configuring clusters hosting multi-tenant microservices or resource-intensive batch engines, you must enforce strict resource caps to avoid system-wide starvation:

1
2
3
4
5
6
7
8
9
10
11
12
13
[Service]
# Restrict maximum memory envelope
MemoryMax=3Gi
# Swap limits
MemorySwapMax=512Mi
# Force immediate process killing when memory pressure breaches limits
MemoryHigh=2.5Gi

# CPU Allocation via weight scaling vectors
CPUAccounting=true
CPUWeight=200
TasksMax=1024

By using weights (CPUWeight=) instead of rigid core pinning, systemd allocates CPU time proportionally depending on total host saturation. If the CPU hits 100% utilisation, a service assigned a weight of 200 receives double the scheduling cycles compared to an adjacent microservice set to a default weight of 100.


7. Advanced Observability: Streamlining Journald for High-Throughput

The days of parsing scattered, uncompressed flat text files under /var/log/ are gone. Systemd aggregates all system metrics, daemon errors, and audit trails into a unified, cryptographically signable binary database called Journald.

Querying the Log Database Like a Pro

Because journald logs are highly structured binary blocks containing explicit metadata tags, you can query massive volumes of raw logs instantly without heavy shell pipeline filters:

1
2
3
4
5
6
7
8
9
# Query logs for a specific service since a precise window
journalctl -u telemetry-api.service --since "2026-07-11 00:00:00"

# Follow logs with high-resolution kernel metadata fields in real time
journalctl -u telemetry-api.service -f -o json-pretty

# Audit kernel panics or boot sequences up until the previous hardware cycle
journalctl -b -1 -p err..emerg

Taming Journald Storage Allocation

Out of the box, journald will dynamically expand its storage allocation, which can sometimes consume massive disk space on systems experiencing heavy log volume. To enforce predictable behavior and ensure fast query execution times, you should tune the central journal configuration file:

1
2
3
4
5
6
7
8
[Journal]
Storage=persistent
Compress=yes
SystemMaxUse=10Gi
SystemMaxFileSize=1Gi
MaxRetentionSec=1month
ForwardToSyslog=no

Setting ForwardToSyslog=no stops systemd from duplicating log blocks to legacy syslog daemons, instantly reducing disk write pressure on high-performance infrastructure servers.


Summary: Designing Resilient, State-Aware Systems

Systemd shifts system administration from a world of loose shell scripting to a robust, declarative operational model. By treating daemons, schedules, paths, and resource limits as unified components of a holistic graph fabric, you gain deterministic control over your environment’s state.

Subsystem ComponentAdministrative FunctionPrimary Directives
.serviceExecution process wrapperExecStart, Type, Restart
.timerTime-based schedulingOnCalendar, Persistent, RandomizedDelaySec
.pathFilesystem monitoringPathChanged, DirectoryNotEmpty
.socketOn-demand network instantiationListenStream, Backlog, NoDelay
OverridesVersion-safe customisation/etc/systemd/system/<unit>.service.d/override.conf

Whether you are configuring high-concurrency database workloads, securing microservices, or building automated maintenance pipelines, building proper unit graphs, enforcing drop-in separation rules, and applying cgroup isolation flags ensures your infrastructure remains reliable, predictable, and highly performant.

This post is licensed under CC BY 4.0 by the author.