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:

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:

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.targetbasic.targetmulti-user.targetgraphical.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 groupscgroups — 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

  1. 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:

TargetSlice
AncestorSysV init runlevels (early 1980s)Linux cgroups (Google, 2006 → kernel 2.6.24, 2008)
Arrived in systemdday one (2010)v205 (2013)
What it isa node in the dependency grapha node in the cgroup tree
Question it answerswhat should run, and in what order?whose resources, and what limits?
Has processes?no — pure synchronization pointnot directly; its leaf children (services/scopes) do
Kernel object?no, it's bookkeepingyes, a real cgroup directory
A unit relates to it viaWantedBy=, Wants=, Requires=, After=Slice=
Cardinalitya unit can be in many targetsa unit is in exactly one slice
The knobs it carriesnone (it does nothing itself)CPUWeight=, MemoryMax=, IOWeight=, TasksMax=

The deep point is orthogonality. Every service is simultaneously at two coordinates:

  1. Somewhere in the dependency graph — pulled in by some targets, ordered before/after other units. This is the time / causality axis. Targets live here.
  2. 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.target
systemctl --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.slice
systemctl --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


Comments