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


3 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