Implementing Per-User CPU/Memory Limits in systemd with cgroups v2


21 views

In contemporary Linux systems where systemd manages the cgroups hierarchy, traditional approaches to user resource limitation face significant obstacles. The previously suggested method of templating user-UID.slice units proves ineffective due to unsupported functionality in current systemd versions.

With cgroups v2 becoming the default in most modern distributions (since systemd v230+), administrators need to adapt their resource limitation strategies. The key constraint lies in systemd's architecture where user slices are dynamically created at login time.

Here's a Python daemon solution that listens for login events and applies resource limits:

#!/usr/bin/env python3
import dbus
from dbus.mainloop.glib import DBusGMainLoop
from gi.repository import GLib
import subprocess

def handle_user_new(uid, path):
    slice_name = f"user-{uid}.slice"
    subprocess.run([
        "systemctl",
        "set-property",
        slice_name,
        "CPUAccounting=yes",
        "MemoryAccounting=yes",
        "CPUQuota=50%",
        "MemoryMax=2G"
    ], check=True)

DBusGMainLoop(set_as_default=True)
bus = dbus.SystemBus()
bus.add_signal_receiver(
    handle_user_new,
    signal_name="UserNew",
    dbus_interface="org.freedesktop.login1.Manager",
    path="/org/freedesktop/login1"
)

loop = GLib.MainLoop()
loop.run()

For more immediate effect without relying on a daemon, integrate with PAM using pam_exec:

# /etc/pam.d/login
session optional pam_exec.so /usr/local/bin/set_user_limits.sh

Sample script:

#!/bin/bash
if [ "$PAM_TYPE" = "open_session" ]; then
    systemctl set-property "user-${PAM_UID}.slice" \
        CPUAccounting=yes \
        MemoryAccounting=yes \
        CPUQuota=75% \
        MemoryMax=4G
fi

To verify limits are applied:

systemd-cgls
systemd-cgtop
cat /sys/fs/cgroup/user.slice/user-1000.slice/cpu.max

Important factors when implementing this solution:

  • Daemon reliability (consider systemd unit for the monitor)
  • Performance impact of frequent cgroup updates
  • Interaction with existing cgroup configurations
  • Logging for audit purposes

In contemporary Linux distributions using systemd (v232+), traditional cgroup approaches for per-user resource limitations face compatibility issues. The systemd's unified cgroup hierarchy (cgroup v2) renders older methods ineffective, particularly for user-based resource control.

Attempting to use templated slices like user-UID.slice proves problematic due to systemd's architectural decisions. As documented in systemd issue #2556, this approach isn't natively supported in the current implementation.

The most reliable method involves monitoring systemd-logind's DBus signals. Here's a Python implementation that automatically applies limits upon user login:

#!/usr/bin/env python3
import dbus
from dbus.mainloop.glib import DBusGMainLoop
from gi.repository import GLib
import subprocess

def handle_user_new(uid, object_path):
    slice_name = f"user-{uid}.slice"
    subprocess.run([
        "systemctl", "set-property",
        slice_name,
        "CPUAccounting=true",
        "MemoryAccounting=true",
        "CPUQuota=150%",
        "MemoryMax=4G"
    ], check=True)

DBusGMainLoop(set_as_default=True)
bus = dbus.SystemBus()
login_manager = bus.get_object(
    "org.freedesktop.login1",
    "/org/freedesktop/login1"
)
login_manager.connect_to_signal(
    "UserNew",
    handle_user_new,
    dbus_interface="org.freedesktop.login1.Manager"
)

loop = GLib.MainLoop()
loop.run()

For enterprise environments, consider implementing these limits via PAM:

# /etc/pam.d/system-auth
session optional pam_exec.so /usr/local/bin/set_user_limits.sh

# set_user_limits.sh
#!/bin/bash
if [ "$PAM_TYPE" = "open_session" ]; then
    uid=$(id -u "$PAM_USER")
    systemctl set-property "user-${uid}.slice" \
        CPUAccounting=true \
        MemoryAccounting=true \
        CPUQuota=200% \
        MemoryHigh=6G \
        MemoryMax=8G
fi

After implementation, verify your settings with:

systemd-cgtop
systemctl show user-1000.slice | grep -E "(CPU|Memory)"
cat /sys/fs/cgroup/user.slice/user-1000.slice/cpu.max

For fine-grained control, create custom slice units:

# /etc/systemd/system/user-resource-control.slice.d/90-limits.conf
[Slice]
CPUQuota=180%
MemoryHigh=4G
MemoryMax=6G
IOReadBandwidthMax=/dev/sda 10M