new file mode 100644
@@ -0,0 +1,95 @@
+# memory-control.bbclass - write a systemd drop-in memory limit into the image
+#
+# Usage:
+# inherit memory-control
+# MEMORY_LIMIT = "512M" # required; K/M/G or raw bytes
+# MEMORY_OOM_POLICY = "kill" # optional; "reboot" (default) or "kill"
+#
+# "reboot": system reboots on OOM (vm.panic_on_oom=2; typical platform default).
+# "kill": only the offending process is killed, system keeps running.
+# Writes /etc/sysctl.d/memory-control.conf with vm.panic_on_oom=1,
+# which sorts after vm.conf and overrides it at boot.
+#
+# MemoryHigh is set to 70% of MemoryMax so the kernel has room to reclaim
+# file-backed pages before hitting the hard ceiling.
+#
+# Kernel config prerequisite:
+# CONFIG_CGROUPS=y and CONFIG_MEMCG=y must be enabled. A reference kernel
+# config fragment is installed to ${datadir}/memory-control/cgroups-v2.cfg.
+# To apply it, copy the fragment to your kernel recipe's files directory and
+# add it to SRC_URI in your kernel bbappend:
+#
+# SRC_URI:append:<machine> = " file://cgroups-v2.cfg"
+
+MEMORY_LIMIT ?= ""
+MEMORY_LIMIT_SERVICE ?= "${PN}.service"
+MEMORY_OOM_POLICY ?= "reboot"
+
+RDEPENDS:${PN} += "memory-control"
+
+python memory_control_do_install() {
+ import os
+
+ limit_str = d.getVar('MEMORY_LIMIT') or ''
+ if not limit_str:
+ return
+
+ service = d.getVar('MEMORY_LIMIT_SERVICE')
+ destdir = d.getVar('D')
+ oom_policy = (d.getVar('MEMORY_OOM_POLICY') or 'reboot').strip().lower()
+
+ if oom_policy not in ('reboot', 'kill'):
+ bb.fatal("memory-control: MEMORY_OOM_POLICY='%s' is invalid; "
+ "use 'reboot' or 'kill'" % oom_policy)
+
+ limit_str = limit_str.strip()
+ units = {'K': 1024, 'KB': 1024,
+ 'M': 1024**2, 'MB': 1024**2,
+ 'G': 1024**3, 'GB': 1024**3}
+ number_part = ''.join(c for c in limit_str if c.isdigit())
+ unit_part = ''.join(c for c in limit_str if not c.isdigit()).upper()
+
+ if not number_part:
+ bb.fatal("memory-control: MEMORY_LIMIT='%s' is not a valid size" % limit_str)
+
+ bytes_val = int(number_part) * units.get(unit_part, 1)
+ bytes_high = int(bytes_val * 0.7)
+
+ dropin_dir = os.path.join(destdir, 'etc', 'systemd', 'system', service + '.d')
+ os.makedirs(dropin_dir, exist_ok=True)
+
+ dropin_path = os.path.join(dropin_dir, 'memory-control.conf')
+ with open(dropin_path, 'w') as f:
+ f.write('# generated by memory-control.bbclass\n')
+ f.write('# MEMORY_LIMIT = "%s" MEMORY_OOM_POLICY = "%s"\n' % (limit_str, oom_policy))
+ f.write('[Service]\n')
+ f.write('MemoryMax=%d\n' % bytes_val)
+ f.write('MemoryHigh=%d\n' % bytes_high)
+ f.write('MemorySwapMax=0\n')
+ if oom_policy == 'kill':
+ # keep the unit alive after the cgroup OOM kill
+ f.write('OOMPolicy=continue\n')
+ # make this unit the preferred victim for the system-wide OOM killer
+ # so it is killed as a process rather than causing a kernel panic
+ f.write('OOMScoreAdjust=500\n')
+
+ bb.note("memory-control: wrote %s for %s (%s, policy=%s)" % (
+ dropin_path, service, limit_str, oom_policy))
+
+ if oom_policy == 'kill':
+ # override vm.panic_on_oom=2 from vm.conf; value 1 kills only the
+ # process inside the cgroup that hit MemoryMax instead of panicking.
+ sysctl_dir = os.path.join(destdir, 'etc', 'sysctl.d')
+ os.makedirs(sysctl_dir, exist_ok=True)
+ sysctl_path = os.path.join(sysctl_dir, 'memory-control.conf')
+ if not os.path.exists(sysctl_path):
+ with open(sysctl_path, 'w') as f:
+ f.write('# generated by memory-control.bbclass\n')
+ f.write('vm.panic_on_oom=1\n')
+ bb.note("memory-control: wrote %s" % sysctl_path)
+}
+
+do_install[postfuncs] += "memory_control_do_install"
+
+FILES:${PN} += "/etc/systemd/system/${MEMORY_LIMIT_SERVICE}.d/memory-control.conf"
+FILES:${PN} += "${@ '/etc/sysctl.d/memory-control.conf' if (d.getVar('MEMORY_OOM_POLICY') or 'reboot').strip().lower() == 'kill' else '' }"
new file mode 100644
@@ -0,0 +1,19 @@
+# kernel config fragment required for cgroup v2 memory accounting
+
+CONFIG_CGROUPS=y
+CONFIG_MEMCG=y
+CONFIG_MEMCG_SWAP=y
+CONFIG_MEMCG_KMEM=y
+CONFIG_CGROUP_SCHED=y
+CONFIG_FAIR_GROUP_SCHED=y
+CONFIG_CFS_BANDWIDTH=y
+CONFIG_CGROUP_PIDS=y
+CONFIG_CGROUP_FREEZER=y
+CONFIG_CGROUP_DEVICE=y
+CONFIG_CGROUP_CPUACCT=y
+CONFIG_CGROUP_PERF=y
+CONFIG_CGROUP_BPF=y
+CONFIG_BLK_CGROUP=y
+CONFIG_PSI=y
+CONFIG_PSI_DEFAULT_DISABLED=n
+CONFIG_SWAP=y
new file mode 100755
@@ -0,0 +1,196 @@
+#!/bin/bash
+# memory-control - inspect cgroup v2 memory limits for systemd services
+#
+# requires: kernel CONFIG_MEMCG=y + cgroups v2 (mount | grep cgroup2),
+# systemd 243+, bash 4.0+
+#
+# usage:
+# memory-control get <service> show MemoryMax/MemoryHigh
+# memory-control status <service> show live usage and memory.stat
+# memory-control list list services with a drop-in
+# .service suffix is optional
+#
+# build-time configuration (recipe or .bbappend):
+#
+# inherit memory-control
+# MEMORY_LIMIT = "512M" # K/M/G or raw bytes
+# MEMORY_OOM_POLICY = "kill" # "reboot" (default) or "kill"
+#
+# machine overrides work as normal bitbake syntax:
+# MEMORY_LIMIT:mymachine = "512M"
+#
+# if the unit name differs from <PN>.service:
+# MEMORY_LIMIT_SERVICE = "my-custom-unit.service"
+#
+# accepted sizes: 256K 512M 1G 536870912 (raw bytes)
+#
+# what gets written at build time:
+#
+# /etc/systemd/system/<service>.service.d/memory-control.conf (always)
+# MemoryMax=<bytes> hard limit; cgroup OOM killer fires if exceeded
+# MemoryHigh=<70% of max> reclaim/throttle starts here; 30% gap to max
+# MemorySwapMax=0 swap disabled
+# OOMPolicy=continue kill policy only; unit stays alive after kill
+# OOMScoreAdjust=500 kill policy only; preferred system-wide OOM target
+#
+# /etc/sysctl.d/memory-control.conf (kill policy only)
+# vm.panic_on_oom=1 sorts after vm.conf, overrides =2 at boot
+#
+# oom policy:
+# reboot BMC reboots on OOM; matches vm.panic_on_oom=2 (platform default)
+# kill only the offending process dies; BMC keeps running
+# when MemoryMax is hit:
+# - cgroup OOM killer fires (scoped to this cgroup only),
+# vm.panic_on_oom is not consulted
+# - OOMPolicy=continue keeps the unit alive after the kill
+# - OOMScoreAdjust=500 makes this the preferred target if global
+# RAM is also exhausted, avoiding a kernel panic
+#
+# troubleshooting:
+# cgroups v2 not mounted:
+# mount | grep cgroup2
+# # add systemd.unified_cgroup_hierarchy=1 to kernel cmdline if missing
+#
+# memory controller missing:
+# cat /sys/fs/cgroup/cgroup.controllers # "memory" must appear
+# # rebuild kernel with CONFIG_MEMCG=y if not
+#
+# drop-in not loaded:
+# systemctl cat <service> # check for memory-control.conf
+# systemctl daemon-reload && systemctl restart <service>
+#
+# check kill policy is active:
+# systemctl show <service> -p OOMPolicy -p MemoryMax -p MemoryHigh
+# sysctl vm.panic_on_oom # should be 1
+#
+# MemoryMax shows infinity after daemon-reload:
+# ls /etc/systemd/system/<service>.service.d/
+# ls /usr/lib/systemd/system/<service>.service.d/
+# # look for a conflicting drop-in
+#
+# service keeps getting killed (limit too low):
+# watch memory-control status <service> # find peak usage
+# # set MEMORY_LIMIT to peak + ~30% in the .bbappend and rebuild
+set -e
+
+CGROUP_BASE=/sys/fs/cgroup
+
+usage() {
+ cat <<EOF
+usage: $(basename "$0") <command> [service]
+
+ get <service> show MemoryMax/MemoryHigh
+ status <service> show live usage and memory.stat
+ list list services with a memory-control drop-in
+ -h, --help show this text
+
+Limits are set at build time:
+ inherit memory-control
+ MEMORY_LIMIT = "512M"
+ MEMORY_OOM_POLICY = "kill"
+
+Drop-in location:
+ /etc/systemd/system/<service>.service.d/memory-control.conf
+EOF
+ exit 0
+}
+
+to_human() {
+ local val=$1
+ case "$val" in
+ max|infinity|"") echo "unlimited"; return ;;
+ esac
+ [[ "$val" =~ ^[0-9]+[KMG]$ ]] && { echo "$val"; return; }
+ [[ "$val" =~ ^[0-9]+$ ]] || { echo "unlimited"; return; }
+ if (( val >= 1024**3 )); then echo "$((val / 1024**3))G"
+ elif (( val >= 1024**2 )); then echo "$((val / 1024**2))M"
+ elif (( val >= 1024 )); then echo "$((val / 1024))K"
+ else echo "${val}B"
+ fi
+}
+
+resolve_service() {
+ local input=$1
+ [[ "$input" =~ \.service$ ]] || input="${input}.service"
+
+ local fragment
+ fragment=$(systemctl show "$input" -p FragmentPath --value 2>/dev/null)
+
+ if [[ -z "$fragment" || ! -f "$fragment" ]]; then
+ for dir in /etc/systemd/system /lib/systemd/system /usr/lib/systemd/system; do
+ [[ -f "$dir/$input" ]] && { fragment="$dir/$input"; break; }
+ done
+ fi
+
+ if [[ -z "$fragment" || ! -f "$fragment" ]]; then
+ echo "error: no unit file found for '$input'" >&2
+ return 1
+ fi
+
+ service=$(basename "$fragment")
+}
+
+cmd_get() {
+ local service=$1
+ [[ -n "$service" ]] || { echo "error: service name required" >&2; exit 1; }
+ resolve_service "$service" || exit 1
+
+ echo "service: $service"
+ echo "MemoryMax: $(to_human "$(systemctl show "$service" -p MemoryMax --value)")"
+ echo "MemoryHigh: $(to_human "$(systemctl show "$service" -p MemoryHigh --value)")"
+}
+
+cmd_status() {
+ local service=$1
+ [[ -n "$service" ]] || { echo "error: service name required" >&2; exit 1; }
+ resolve_service "$service" || exit 1
+
+ local state
+ state=$(systemctl is-active "$service" 2>/dev/null || true)
+ echo "service: $service"
+ echo "state: $state"
+
+ if [[ "$state" == "active" ]]; then
+ local mem_max mem_high mem_cur
+ mem_max=$(systemctl show "$service" -p MemoryMax --value)
+ mem_high=$(systemctl show "$service" -p MemoryHigh --value)
+ mem_cur=$(systemctl show "$service" -p MemoryCurrent --value)
+ echo "current: $(to_human "$mem_cur")"
+ echo "high: $(to_human "$mem_high")"
+ echo "max: $(to_human "$mem_max")"
+ [[ "$mem_max" =~ ^[0-9]+$ ]] && echo "usage: $(( mem_cur * 100 / mem_max ))%"
+ fi
+
+ local cgroup_path
+ cgroup_path=$(systemctl show "$service" -p ControlGroup --value)
+ [[ -z "$cgroup_path" ]] && return
+ printf "\ncgroup: %s\n" "$cgroup_path"
+
+ local mem_stat="${CGROUP_BASE}${cgroup_path}/memory.stat"
+ [[ -f "$mem_stat" ]] || return
+ printf "\nmemory.stat:\n"
+ grep -E "^(anon|file|shmem|kernel)" "$mem_stat" | while read -r key val; do
+ printf " %-20s %s\n" "$key" "$(to_human "$val")"
+ done
+}
+
+cmd_list() {
+ local found=0
+ for dir in /etc/systemd/system/*.service.d; do
+ [[ -d "$dir" && -f "$dir/memory-control.conf" ]] || continue
+ local unit
+ unit=$(basename "$dir" .d)
+ printf "%-40s %s\n" "${unit%.service}" \
+ "$(to_human "$(systemctl show "$unit" -p MemoryMax --value 2>/dev/null || echo unknown)")"
+ found=1
+ done
+ (( found )) || echo "no services with memory-control drop-ins found"
+}
+
+case "${1:-}" in
+ get) cmd_get "$2" ;;
+ status) cmd_status "$2" ;;
+ list) cmd_list ;;
+ -h|--help) usage ;;
+ *) usage ;;
+esac
new file mode 100644
@@ -0,0 +1,13 @@
+[Unit]
+Description=Memory Control for %i
+Documentation=man:systemd.resource-control(5)
+Before=%i.service
+PartOf=%i.service
+
+[Service]
+Type=oneshot
+RemainAfterExit=yes
+ExecStart=/bin/true
+
+[Install]
+WantedBy=multi-user.target
new file mode 100644
@@ -0,0 +1,53 @@
+SUMMARY = "Inspect and apply cgroup v2 memory limits for systemd services"
+DESCRIPTION = "Provides a CLI tool to inspect MemoryMax/MemoryHigh settings \
+for any systemd service, and a bbclass to bake per-service drop-ins into an \
+image at build time."
+HOMEPAGE = "https://www.openembedded.org"
+BUGTRACKER = "https://bugzilla.yoctoproject.org"
+LICENSE = "MIT"
+LIC_FILES_CHKSUM = "file://${COMMON_LICENSE_DIR}/MIT;md5=0835ade698e0bcf8506ecda2f7b4f302"
+
+inherit systemd
+
+SRC_URI = " \
+ file://memory-control.sh \
+ file://memory-control@.service \
+ file://cgroups-v2.cfg \
+"
+
+SYSTEMD_SERVICE:${PN} = ""
+SYSTEMD_AUTO_ENABLE = "disable"
+
+RDEPENDS:${PN} = "bash systemd"
+
+do_install() {
+ install -d ${D}${bindir}
+ install -m 0755 ${UNPACKDIR}/memory-control.sh ${D}${bindir}/memory-control
+
+ install -d ${D}${systemd_system_unitdir}
+ install -m 0644 ${UNPACKDIR}/memory-control@.service \
+ ${D}${systemd_system_unitdir}/
+
+ install -d ${D}${datadir}/memory-control
+ install -m 0644 ${UNPACKDIR}/cgroups-v2.cfg \
+ ${D}${datadir}/memory-control/
+}
+
+pkg_postinst:${PN}() {
+ #!/bin/sh
+ if [ -n "$D" ]; then
+ exit 0
+ fi
+ if [ -f /sys/fs/cgroup/cgroup.controllers ]; then
+ grep -q memory /sys/fs/cgroup/cgroup.controllers || \
+ echo "memory-control: WARNING: memory controller not available in cgroups v2"
+ else
+ echo "memory-control: WARNING: cgroups v2 not detected"
+ fi
+}
+
+FILES:${PN} = " \
+ ${bindir}/memory-control \
+ ${systemd_system_unitdir}/memory-control@.service \
+ ${datadir}/memory-control \
+"
Adds a CLI tool and bbclass for applying per-service memory limits via systemd resource control and Linux cgroups v2. The memory-control command inspects MemoryMax and MemoryHigh settings for any running systemd service and lists all services with a drop-in applied. memory-control.bbclass allows any recipe to set a memory limit at build time by inheriting the class and setting MEMORY_LIMIT. Two OOM policies are available via MEMORY_OOM_POLICY: "reboot" (default) reboots on OOM, and "kill" terminates only the offending process. AI-Generated: Uses IBM Bob Signed-off-by: Ishaan Desai <Ishaan.Desai@ibm.com> --- meta/classes-recipe/memory-control.bbclass | 95 +++++++++ .../memory-control/cgroups-v2.cfg | 19 ++ .../memory-control/memory-control.sh | 196 ++++++++++++++++++ .../memory-control/memory-control@.service | 13 ++ .../memory-control/memory-control_1.0.bb | 53 +++++ 5 files changed, 376 insertions(+) create mode 100644 meta/classes-recipe/memory-control.bbclass create mode 100644 meta/recipes-support/memory-control/memory-control/cgroups-v2.cfg create mode 100755 meta/recipes-support/memory-control/memory-control/memory-control.sh create mode 100644 meta/recipes-support/memory-control/memory-control/memory-control@.service create mode 100644 meta/recipes-support/memory-control/memory-control_1.0.bb