Two Family Trees: systemd Targets and Slices
I went looking for a way to group a handful of my own systemd units — my
sync timers, a mail fetcher, a couple of personal daemons — so I could see them
and operate on them as a set instead of squinting at the full systemctl
output. Two answers kept coming up: targets and slices. They both
"group units." They both show up in the same listings. And for an embarrassing
minute I couldn't have told you the difference between them.
So I went and read the history. It turns out the reason they feel like
overlapping tools is an illusion of proximity: they live next to each other in
systemctl today, but they were born in different decades, at different
companies, to solve completely unrelated problems. Targets are the descendants
of init runlevels. Slices are the descendants of control groups. They
only look like siblings because systemd dresses every unit type in the same
uniform.
Here's the one sentence to hold onto before we dig in:
A target is a node in a dependency graph. A slice is a node in a cgroup tree.
One answers "what should be running, and in what order?" The other answers "whose resources are these, and what are the limits?" Those are orthogonal questions, and once you see that, the whole model snaps into focus.
Let me trace each family tree.
Family tree #1: runlevels → targets
The 1980s: a runlevel is a state
The story starts with SysV init, the init system that came out of AT&T's
UNIX System III in the early 1980s and carried into System V (hence "SysV"). Its
central concept was the runlevel: a named configuration of the machine — a
state you transition the system into, in which only a selected group of
processes is allowed to run. The mapping of "what runs at which level" lived in
/etc/inittab, and init's entire job was to drag the machine into the requested
state and keep the right processes alive.
There were eight of them, 0–6 plus S, and the ones at the edges were
universal:
0 halt / poweroff
1 single-user (maintenance)
S single-user (≈ 1)
6 reboot
The interesting part is the middle, 2–5, because that's where the distros quietly disagreed — a fact that bites people to this day:
- Red Hat / Fedora: runlevel 3 was full multi-user with networking but a
text console; runlevel 5 added the graphical X11 login. The famous
"boot to a GUI vs boot to a console" toggle was literally
3vs5. - Debian / Ubuntu: runlevels 2 through 5 were all identical — full multi-user mode, no text-vs-graphical distinction at all.
The mental model that matters: a runlevel is a state machine position. You are at a runlevel; you switch to another. It's about the global condition of the box, and which bundle of services that condition implies.
The init wars (2005–2010)
SysV init had an Achilles' heel: it ran service scripts sequentially, in a numbered order, each one blocking the next. On a machine with dozens of services, boot was a slow conga line. Two projects attacked this before systemd existed:
- launchd (Apple, 2005, shipped in Mac OS X 10.4 "Tiger"). One daemon to replace init, cron, inetd, and friends — and crucially, it started services on demand via socket activation rather than eagerly in a fixed order. Apple relicensed it under Apache 2.0 in 2006 to encourage adoption elsewhere.
- Upstart (Canonical, 2006, by Scott James Remnant, first in Ubuntu 6.10). An event-based init: services reacted to emitted events ("a disk appeared", "a filesystem mounted") instead of marching through runlevel scripts.
Both were saying the same thing in different dialects: the rigid, sequential runlevel model is the wrong abstraction.
April 30, 2010: "Rethinking PID 1"
Then Lennart Poettering (at Red Hat), with design work from Kay Sievers (then at Novell), published an essay titled Rethinking PID 1 and announced systemd. The pitch: aggressive parallelization of boot, socket activation pushed to its limit (create all the listening sockets first, hand them to services, let everything start at once without hand-written ordering), D-Bus activation, cgroups to track processes (more on that in the second family tree), and — the relevant bit here — targets to replace runlevels.
A target is systemd's answer to "what is a runlevel, really?" Stripped of
the state-machine framing, a runlevel was just a name for a set of services
that should be up together. So systemd made that explicit. From the
systemd.target(5) man page:
Target units are used to group units and to set synchronization points for ordering dependencies with other unit files. […] Target units do not offer any additional functionality on top of the generic functionality provided by units. They merely group units. […] Target units provide a more flexible replacement for SysV runlevels in the classic SysV init system.
Read that twice, because it's the whole thing: a target has no processes of
its own, does nothing on its own, and exists purely as a named node in
the dependency graph that other units can point at. The old runlevels are still
there as compatibility aliases — and the mapping is hard-coded right in
systemd's build files (units/meson.build):
runlevel0.target → poweroff.target
runlevel1.target → rescue.target
runlevel2/3/4.target → multi-user.target
runlevel5.target → graphical.target
runlevel6.target → reboot.target
systemd's own FAQ doesn't mince words: "The concept of runlevels is obsolete. A set of target units are exposed that carry similar semantics, e.g. runlevel 5 -> graphical.target."
Targets also do the job runlevels never could: they're synchronization
points in a real dependency graph. Boot is a chain of them —
sysinit.target → basic.target → multi-user.target → graphical.target —
and units hook themselves in with WantedBy=, Wants=, Requires=, After=,
Before=. That last point matters for later: a single unit can be WantedBy
many targets at once. Membership in the dependency graph is many-to-many.
You can see the graph directly:
systemctl list-units --type=target # what's active
systemctl list-dependencies graphical.target # the tree under a target
So: runlevels → targets. The lineage is init, boot, state, ordering. Nothing about resources.
Family tree #2: cgroups → slices
Now rewind to a completely different room.
2006–2008: Google needs to box in processes
Around 2006, engineers at Google — Paul Menage and Rohit Seth — were wrestling with enormous shared Linux clusters and built a kernel feature they called "process containers." The goal had nothing to do with booting: it was about hierarchically grouping processes so you could account for, limit, prioritize, and isolate their resource usage — CPU, memory, block I/O, and so on, each handled by a per-resource "controller."
To avoid collision with the loaded word "container," it was renamed control groups — cgroups — in late 2007, and merged into the Linux kernel in 2.6.24 (January 2008).
This is a kernel resource-management primitive. It knows nothing about services, runlevels, or boot order. It just lets you draw a box around a set of processes and say "this box gets at most 2 GB of RAM and 10% of one CPU."
Why systemd cared: processes that won't stay caught
Here's the bridge between the two family trees. Traditional Unix daemons double-fork to "daemonize" — detach from their parent and re-parent to PID
- That's deliberate, and it's a disaster for a service manager: once a daemon double-forks, the thing that launched it has lost track of it. PID-based supervision is racy and unreliable — PIDs get recycled, children scatter and re-parent to init, and you can never be sure you've found (or killed) all of a service's processes.
cgroups solved this cleanly, and it's exactly why "Rethinking PID 1" leaned on them. Put every service in its own cgroup, and the kernel maintains a reliable, race-free record of precisely which processes belong to that service, no matter how many times they fork. Poettering put it bluntly:
Unless it is privileged and has access to the cgroup file system it cannot escape its group.
That gives you accurate accounting and the ability to reliably kill an entire service — every last worker — by acting on the group.
July 3, 2013 (v205): systemd grows slices and scopes
For systemd's first few years it used cgroups internally for this tracking, but there was a problem: lots of programs (systemd, logind, libvirt, your shell) were all writing to the cgroup tree at once, stepping on each other. So in version 205 (released 2013-07-03), systemd introduced two new unit types and a new rule. From the NEWS file:
Two new unit types have been introduced: Scope units are very similar to service units, however, are created out of pre-existing processes […] By using scope units it is possible for system services and applications to group their own child processes […] Slice units may be used to partition system resources in an hierarchical fashion and then assign other units to them. By default there are now three slices:
system.slice(for all system services),user.slice(for all user sessions),machine.slice(for VMs and containers).
And the reason they appeared at all:
Slices and scopes have been introduced primarily in context of the work to move cgroup handling to a single-writer scheme, where only PID 1 creates/removes/manages cgroups.
That "single-writer" rule — only PID 1 touches the cgroup tree — is why modern systemd is the sole owner of cgroups on a Linux box. Everything else asks systemd.
So what is a slice? From systemd.slice(5):
A slice unit is a concept for hierarchically managing resources of a group of processes. This management is performed by creating a node in the Linux Control Group (cgroup) tree. Units that manage processes (primarily scope and service units) may be assigned to a specific slice. For each slice, certain resource limits may be set that apply to all processes of all units contained in that slice.
A slice is a cgroup directory. Its name encodes its path in the tree:
-.slice is the root, and foo-bar.slice sits inside foo.slice inside
-.slice. The default layout every systemd box boots with:
-.slice the root
├── system.slice all system services
├── user.slice all user sessions (each user gets user-UID.slice)
└── machine.slice VMs and containers (via systemd-machined)
The third unit type from v205, the scope, is the odd cousin: like a service, but for processes systemd didn't fork itself — login sessions, VMs, anything externally launched. Scopes and services are the leaves that hold actual processes; slices are the branches that hold them.
And that leaf/branch split isn't a metaphor — it's a kernel rule. systemd's
CONTROL_GROUP_INTERFACE.md spells it out:
Service, scope and slice units directly map to objects in the cgroup tree. […] Slices do not contain processes themselves, but the services and scopes contained in them do.
Under cgroup v2's "no processes in inner nodes" rule, an inner node can't hold
processes directly — so slices (inner nodes) never contain processes, only their
service/scope children (leaf nodes) do. Crucially, a unit is placed in exactly
one slice: the Slice= directive is a single value, defaulting to
system.slice. Membership in the cgroup tree is one-to-one.
The cgroup v2 detour
One more thread, because it explains why old guides feel inconsistent. The original cgroups (v1) gave each controller its own independent hierarchy — the cpu tree, the memory tree, the io tree were separate and could disagree about which group a process was in. It was a mess that couldn't be fixed without breaking the interface. Tejun Heo led the redesign — cgroup v2, the "unified hierarchy," where every controller shares one tree and a process belongs to exactly one cgroup. It landed as stable in Linux 4.5 (March 2016).
systemd's migration tracks that maturing:
v233 (2017) hybrid v1+v2 default, with knobs to opt into unified
v243 (2019) build-time default flips to "unified" (pure cgroup v2)
v258 (2025) cgroup v1 support removed entirely — v2 always mounted
So if you're on a current system, your slices are nodes in a single unified cgroup v2 tree. The "exactly one slice" cleanliness is partly because of Tejun's redesign.
So: cgroups → slices. The lineage is resources, accounting, limits, containment. Nothing about boot order.
The nuance: why they aren't siblings
Lay the two family trees side by side and the "overlap" evaporates:
| Target | Slice | |
|---|---|---|
| Ancestor | SysV init runlevels (early 1980s) | Linux cgroups (Google, 2006 → kernel 2.6.24, 2008) |
| Arrived in systemd | day one (2010) | v205 (2013) |
| What it is | a node in the dependency graph | a node in the cgroup tree |
| Question it answers | what should run, and in what order? | whose resources, and what limits? |
| Has processes? | no — pure synchronization point | not directly; its leaf children (services/scopes) do |
| Kernel object? | no, it's bookkeeping | yes, a real cgroup directory |
| A unit relates to it via | WantedBy=, Wants=, Requires=, After= | Slice= |
| Cardinality | a unit can be in many targets | a unit is in exactly one slice |
| The knobs it carries | none (it does nothing itself) | CPUWeight=, MemoryMax=, IOWeight=, TasksMax=… |
The deep point is orthogonality. Every service is simultaneously at two coordinates:
- Somewhere in the dependency graph — pulled in by some targets, ordered before/after other units. This is the time / causality axis. Targets live here.
- Somewhere in the cgroup tree — inside exactly one slice, subject to its resource limits. This is the resource / containment axis. Slices live here.
These axes don't compete; they're perpendicular. Asking "should I use a target or a slice?" is a bit like asking "should I describe this meeting by its time or its room?" Usually the honest answer is both, for different reasons.
So which one do I actually want?
Back to my original itch — "let me see and operate on my own set of units as a group." Now I can answer it properly, because it depends on which axis I care about.
If I want to bring them up/down together, order them, or depend on them as a set → that's the dependency-graph axis → a target. It's the runlevel-flavored answer: "here is a named bundle; activate it."
# ~/.config/systemd/user/personal.target
[Unit]
Description=My personal services# in each of my units:
[Install]
WantedBy=personal.targetsystemctl --user list-dependencies personal.target # see the whole set
systemctl --user start personal.target # bring the set up
If I want to cap or account their combined CPU/memory, or view them all as one
resource group → that's the cgroup-tree axis → a slice. It's the
cgroup-flavored answer, and status on a slice gives you a live tree of
everything inside it plus its resource usage:
# ~/.config/systemd/user/personal.slice
[Slice]
MemoryMax=2G
CPUWeight=50# in each of my units:
[Service]
Slice=personal.slicesystemctl --user status personal.slice # the cgroup tree + live usage
systemd-cgls --user # the whole user cgroup tree
And the punchline that took me the whole history to appreciate: you can use
both, and it isn't redundant. A personal.target that pulls the group up and
a personal.slice that boxes its resources are describing the same services
along two independent axes. The target says when they run; the slice says how
much they get.
(If all I really want is to list mine and never operate on them as a unit, the
cheapest trick of all is a shared name prefix — systemctl --user list-units 'personal-*', since systemctl accepts globs. No new unit types
required. But the moment I want grouped behavior — ordering or limits — I reach
for the matching family tree.)
The takeaway
systemd looks like one monolithic thing, but a lot of it is really two old
Unix ideas wearing the same systemctl uniform: the init state machine that
goes back to AT&T System V, and the kernel resource controller that came out of
Google's clusters. Targets are the grandchild of the runlevel. Slices are the
grandchild of the cgroup. They share a command-line front-end and absolutely
nothing else in their DNA.
Once you can glance at a unit type and ask "which family tree is this from — the dependency graph, or the cgroup tree?", the rest of systemd stops feeling arbitrary. It's just two lineages, finally living under one roof.
Sources & further reading
- Lennart Poettering, Rethinking PID 1 (2010-04-30) — the founding essay.
systemd.target(5),systemd.slice(5),systemd.scope(5),systemd.special(7)— the canonical definitions quoted above.- systemd
NEWS— v205 (slices/scopes + single-writer), v243 (unified default), v258 (v1 removed). - Control Group v2 — the kernel's own cgroup v2 documentation.
- LWN, 14 years of systemd — a retrospective.
Comments