✓ CPUQuotaPeriodSec=
✓ MemoryAccounting=
✓ MemoryMin=
+✓ DefaultMemoryLow=
✓ MemoryLow=
✓ MemoryHigh=
✓ MemoryMax=
<para>This setting is supported only if the unified control group hierarchy is used and disables
<varname>MemoryLimit=</varname>.</para>
+
+ <para>Units may can have their children use a default <literal>memory.low</literal> value by specifying
+ <varname>DefaultMemoryLow=</varname>, which has the same usage as <varname>MemoryLow=</varname>. This setting
+ does not affect <literal>memory.low</literal> in the unit itself.</para>
</listitem>
</varlistentry>
"%sStartupIOWeight=%" PRIu64 "\n"
"%sBlockIOWeight=%" PRIu64 "\n"
"%sStartupBlockIOWeight=%" PRIu64 "\n"
+ "%sDefaultMemoryLow=%" PRIu64 "\n"
"%sMemoryMin=%" PRIu64 "\n"
"%sMemoryLow=%" PRIu64 "\n"
"%sMemoryHigh=%" PRIu64 "\n"
prefix, c->startup_io_weight,
prefix, c->blockio_weight,
prefix, c->startup_blockio_weight,
+ prefix, c->default_memory_low,
prefix, c->memory_min,
prefix, c->memory_low,
prefix, c->memory_high,
return 0;
}
+uint64_t unit_get_ancestor_memory_low(Unit *u) {
+ CGroupContext *c;
+
+ /* 1. Is MemoryLow set in this unit? If so, use that.
+ * 2. Is DefaultMemoryLow set in any ancestor? If so, use that.
+ * 3. Otherwise, return CGROUP_LIMIT_MIN. */
+
+ assert(u);
+
+ c = unit_get_cgroup_context(u);
+
+ if (c->memory_low_set)
+ return c->memory_low;
+
+ while (UNIT_ISSET(u->slice)) {
+ u = UNIT_DEREF(u->slice);
+ c = unit_get_cgroup_context(u);
+
+ if (c->default_memory_low_set)
+ return c->default_memory_low;
+ }
+
+ /* We've reached the root, but nobody had DefaultMemoryLow set, so set it to the kernel default. */
+ return CGROUP_LIMIT_MIN;
+}
+
static void cgroup_xattr_apply(Unit *u) {
char ids[SD_ID128_STRING_MAX];
int r;
(void) set_attribute_and_warn(u, "blkio", "blkio.throttle.write_bps_device", buf);
}
-static bool cgroup_context_has_unified_memory_config(CGroupContext *c) {
- return c->memory_min > 0 || c->memory_low > 0 || c->memory_high != CGROUP_LIMIT_MAX || c->memory_max != CGROUP_LIMIT_MAX || c->memory_swap_max != CGROUP_LIMIT_MAX;
+static bool unit_has_unified_memory_config(Unit *u) {
+ CGroupContext *c;
+
+ assert(u);
+
+ c = unit_get_cgroup_context(u);
+ assert(c);
+
+ return c->memory_min > 0 || unit_get_ancestor_memory_low(u) > 0 ||
+ c->memory_high != CGROUP_LIMIT_MAX || c->memory_max != CGROUP_LIMIT_MAX ||
+ c->memory_swap_max != CGROUP_LIMIT_MAX;
}
static void cgroup_apply_unified_memory_limit(Unit *u, const char *file, uint64_t v) {
if (cg_all_unified() > 0) {
uint64_t max, swap_max = CGROUP_LIMIT_MAX;
- if (cgroup_context_has_unified_memory_config(c)) {
+ if (unit_has_unified_memory_config(u)) {
max = c->memory_max;
swap_max = c->memory_swap_max;
} else {
}
cgroup_apply_unified_memory_limit(u, "memory.min", c->memory_min);
- cgroup_apply_unified_memory_limit(u, "memory.low", c->memory_low);
+ cgroup_apply_unified_memory_limit(u, "memory.low", unit_get_ancestor_memory_low(u));
cgroup_apply_unified_memory_limit(u, "memory.high", c->memory_high);
cgroup_apply_unified_memory_limit(u, "memory.max", max);
cgroup_apply_unified_memory_limit(u, "memory.swap.max", swap_max);
char buf[DECIMAL_STR_MAX(uint64_t) + 1];
uint64_t val;
- if (cgroup_context_has_unified_memory_config(c)) {
+ if (unit_has_unified_memory_config(u)) {
val = c->memory_max;
log_cgroup_compat(u, "Applying MemoryMax=%" PRIi64 " as MemoryLimit=", val);
} else
return false;
}
-static CGroupMask cgroup_context_get_mask(CGroupContext *c) {
+static CGroupMask unit_get_cgroup_mask(Unit *u) {
CGroupMask mask = 0;
+ CGroupContext *c;
+
+ assert(u);
+
+ c = unit_get_cgroup_context(u);
/* Figure out which controllers we need, based on the cgroup context object */
if (c->memory_accounting ||
c->memory_limit != CGROUP_LIMIT_MAX ||
- cgroup_context_has_unified_memory_config(c))
+ unit_has_unified_memory_config(u))
mask |= CGROUP_MASK_MEMORY;
if (c->device_allow ||
if (!c)
return 0;
- return (cgroup_context_get_mask(c) | unit_get_bpf_mask(u) | unit_get_delegate_mask(u)) & ~unit_get_ancestor_disable_mask(u);
+ return (unit_get_cgroup_mask(u) | unit_get_bpf_mask(u) | unit_get_delegate_mask(u)) & ~unit_get_ancestor_disable_mask(u);
}
CGroupMask unit_get_delegate_mask(Unit *u) {
LIST_HEAD(CGroupIODeviceLimit, io_device_limits);
LIST_HEAD(CGroupIODeviceLatency, io_device_latencies);
+ uint64_t default_memory_low;
uint64_t memory_min;
uint64_t memory_low;
uint64_t memory_high;
uint64_t memory_max;
uint64_t memory_swap_max;
+ bool default_memory_low_set;
+ bool memory_low_set;
+
LIST_HEAD(IPAddressAccessItem, ip_address_allow);
LIST_HEAD(IPAddressAccessItem, ip_address_deny);
Unit *manager_get_unit_by_pid_cgroup(Manager *m, pid_t pid);
Unit* manager_get_unit_by_pid(Manager *m, pid_t pid);
+uint64_t unit_get_ancestor_memory_low(Unit *u);
+
int unit_search_main_pid(Unit *u, pid_t *ret);
int unit_watch_all_pids(Unit *u);
SD_BUS_PROPERTY("BlockIOReadBandwidth", "a(st)", property_get_blockio_device_bandwidths, 0, 0),
SD_BUS_PROPERTY("BlockIOWriteBandwidth", "a(st)", property_get_blockio_device_bandwidths, 0, 0),
SD_BUS_PROPERTY("MemoryAccounting", "b", bus_property_get_bool, offsetof(CGroupContext, memory_accounting), 0),
+ SD_BUS_PROPERTY("DefaultMemoryLow", "t", NULL, offsetof(CGroupContext, default_memory_low), 0),
SD_BUS_PROPERTY("MemoryMin", "t", NULL, offsetof(CGroupContext, memory_min), 0),
SD_BUS_PROPERTY("MemoryLow", "t", NULL, offsetof(CGroupContext, memory_low), 0),
SD_BUS_PROPERTY("MemoryHigh", "t", NULL, offsetof(CGroupContext, memory_high), 0),
if (streq(name, "MemoryLow"))
return bus_cgroup_set_memory(u, name, &c->memory_low, message, flags, error);
+ if (streq(name, "DefaultMemoryLow"))
+ return bus_cgroup_set_memory(u, name, &c->default_memory_low, message, flags, error);
+
if (streq(name, "MemoryHigh"))
return bus_cgroup_set_memory(u, name, &c->memory_high, message, flags, error);
if (streq(name, "MemoryLowScale"))
return bus_cgroup_set_memory_scale(u, name, &c->memory_low, message, flags, error);
+ if (streq(name, "DefaultMemoryLowScale"))
+ return bus_cgroup_set_memory_scale(u, name, &c->default_memory_low, message, flags, error);
+
if (streq(name, "MemoryHighScale"))
return bus_cgroup_set_memory_scale(u, name, &c->memory_high, message, flags, error);
$1.CPUQuotaPeriodSec, config_parse_sec_def_infinity, 0, offsetof($1, cgroup_context.cpu_quota_period_usec)
$1.MemoryAccounting, config_parse_bool, 0, offsetof($1, cgroup_context.memory_accounting)
$1.MemoryMin, config_parse_memory_limit, 0, offsetof($1, cgroup_context)
+$1.DefaultMemoryLow, config_parse_memory_limit, 0, offsetof($1, cgroup_context)
$1.MemoryLow, config_parse_memory_limit, 0, offsetof($1, cgroup_context)
$1.MemoryHigh, config_parse_memory_limit, 0, offsetof($1, cgroup_context)
$1.MemoryMax, config_parse_memory_limit, 0, offsetof($1, cgroup_context)
}
}
- if (streq(lvalue, "MemoryMin"))
+ if (streq(lvalue, "DefaultMemoryLow")) {
+ c->default_memory_low_set = true;
+ if (isempty(rvalue))
+ c->default_memory_low = CGROUP_LIMIT_MIN;
+ else
+ c->default_memory_low = bytes;
+ } else if (streq(lvalue, "MemoryMin"))
c->memory_min = bytes;
- else if (streq(lvalue, "MemoryLow"))
+ else if (streq(lvalue, "MemoryLow")) {
c->memory_low = bytes;
- else if (streq(lvalue, "MemoryHigh"))
+ c->memory_low_set = true;
+ } else if (streq(lvalue, "MemoryHigh"))
c->memory_high = bytes;
else if (streq(lvalue, "MemoryMax"))
c->memory_max = bytes;
return 1;
}
- if (STR_IN_SET(field, "MemoryMin", "MemoryLow", "MemoryHigh", "MemoryMax", "MemorySwapMax", "MemoryLimit", "TasksMax")) {
+ if (STR_IN_SET(field, "MemoryMin", "DefaultMemoryLow", "MemoryLow", "MemoryHigh", "MemoryMax", "MemorySwapMax", "MemoryLimit", "TasksMax")) {
if (isempty(eq) || streq(eq, "infinity")) {
r = sd_bus_message_append(m, "(sv)", field, "t", CGROUP_LIMIT_MAX);
bus_print_property_value(name, expected_value, value, "[not set]");
- else if ((STR_IN_SET(name, "MemoryLow", "MemoryHigh", "MemoryMax", "MemorySwapMax", "MemoryLimit") && u == CGROUP_LIMIT_MAX) ||
+ else if ((STR_IN_SET(name, "DefaultMemoryLow", "MemoryLow", "MemoryHigh", "MemoryMax", "MemorySwapMax", "MemoryLimit") && u == CGROUP_LIMIT_MAX) ||
(STR_IN_SET(name, "TasksMax", "DefaultTasksMax") && u == (uint64_t) -1) ||
(startswith(name, "Limit") && u == (uint64_t) -1) ||
(startswith(name, "DefaultLimit") && u == (uint64_t) -1))
uint64_t ip_ingress_bytes;
uint64_t ip_egress_bytes;
+ uint64_t default_memory_low;
+
LIST_HEAD(ExecStatusInfo, exec);
} UnitStatusInfo;
{ "Where", "s", NULL, offsetof(UnitStatusInfo, where) },
{ "What", "s", NULL, offsetof(UnitStatusInfo, what) },
{ "MemoryCurrent", "t", NULL, offsetof(UnitStatusInfo, memory_current) },
+ { "DefaultMemoryLow", "t", NULL, offsetof(UnitStatusInfo, default_memory_low) },
{ "MemoryMin", "t", NULL, offsetof(UnitStatusInfo, memory_min) },
{ "MemoryLow", "t", NULL, offsetof(UnitStatusInfo, memory_low) },
{ "MemoryHigh", "t", NULL, offsetof(UnitStatusInfo, memory_high) },
libshared],
[]],
+ [['src/test/test-cgroup-unit-default.c',
+ 'src/test/test-helper.c'],
+ [libcore,
+ libshared],
+ []],
+
[['src/test/test-cgroup-mask.c',
'src/test/test-helper.c'],
[libcore,
--- /dev/null
+/* SPDX-License-Identifier: LGPL-2.1+ */
+
+#include <stdio.h>
+
+#include "cgroup.h"
+#include "manager.h"
+#include "rm-rf.h"
+#include "test-helper.h"
+#include "tests.h"
+#include "unit.h"
+
+static int test_default_memory_low(void) {
+ _cleanup_(rm_rf_physical_and_freep) char *runtime_dir = NULL;
+ _cleanup_(manager_freep) Manager *m = NULL;
+ Unit *root, *dml,
+ *dml_passthrough, *dml_passthrough_empty, *dml_passthrough_set_dml, *dml_passthrough_set_ml,
+ *dml_override, *dml_override_empty,
+ *dml_discard, *dml_discard_empty, *dml_discard_set_ml;
+ uint64_t dml_tree_default;
+ int r;
+
+ r = enter_cgroup_subroot();
+ if (r == -ENOMEDIUM)
+ return log_tests_skipped("cgroupfs not available");
+
+ assert_se(set_unit_path(get_testdata_dir()) >= 0);
+ assert_se(runtime_dir = setup_fake_runtime_dir());
+ r = manager_new(UNIT_FILE_USER, MANAGER_TEST_RUN_BASIC, &m);
+ if (IN_SET(r, -EPERM, -EACCES)) {
+ log_error_errno(r, "manager_new: %m");
+ return log_tests_skipped("cannot create manager");
+ }
+
+ assert_se(r >= 0);
+ assert_se(manager_startup(m, NULL, NULL) >= 0);
+
+ /* dml.slice has DefaultMemoryLow=50. Beyond that, individual subhierarchies look like this:
+ *
+ * 1. dml-passthrough.slice sets MemoryLow=100. This should not affect its children, as only
+ * DefaultMemoryLow is propagated, not MemoryLow. As such, all leaf services should end up with
+ * memory.low as 50, inherited from dml.slice, *except* for dml-passthrough-set-ml.service, which
+ * should have the value of 25, as it has MemoryLow explicitly set.
+ *
+ * ┌───────────┐
+ * │ dml.slice │
+ * └─────┬─────┘
+ * MemoryLow=100
+ * ┌───────────┴───────────┐
+ * │ dml-passthrough.slice │
+ * └───────────┬───────────┘
+ * ┌───────────────────────────────────┼───────────────────────────────────┐
+ * no new settings DefaultMemoryLow=15 MemoryLow=25
+ * ┌───────────────┴───────────────┐ ┌────────────────┴────────────────┐ ┌───────────────┴────────────────┐
+ * │ dml-passthrough-empty.service │ │ dml-passthrough-set-dml.service │ │ dml-passthrough-set-ml.service │
+ * └───────────────────────────────┘ └─────────────────────────────────┘ └────────────────────────────────┘
+ *
+ * 2. dml-override.slice sets DefaultMemoryLow=10. As such, dml-override-empty.service should also
+ * end up with a memory.low of 10. dml-override.slice should still have a memory.low of 50.
+ *
+ * ┌───────────┐
+ * │ dml.slice │
+ * └─────┬─────┘
+ * DefaultMemoryLow=10
+ * ┌─────────┴──────────┐
+ * │ dml-override.slice │
+ * └─────────┬──────────┘
+ * no new settings
+ * ┌─────────────┴──────────────┐
+ * │ dml-override-empty.service │
+ * └────────────────────────────┘
+ *
+ * 3. dml-discard.slice sets DefaultMemoryLow= with no rvalue. As such,
+ * dml-discard-empty.service should end up with a value of 0.
+ * dml-discard-explicit-ml.service sets MemoryLow=70, and as such should have that override the
+ * reset DefaultMemoryLow value. dml-discard.slice should still have an eventual memory.low of 50.
+ *
+ * ┌───────────┐
+ * │ dml.slice │
+ * └─────┬─────┘
+ * DefaultMemoryLow=
+ * ┌─────────┴─────────┐
+ * │ dml-discard.slice │
+ * └─────────┬─────────┘
+ * ┌──────────────┴───────────────┐
+ * no new settings MemoryLow=15
+ * ┌─────────────┴─────────────┐ ┌─────────────┴──────────────┐
+ * │ dml-discard-empty.service │ │ dml-discard-set-ml.service │
+ * └───────────────────────────┘ └────────────────────────────┘
+ */
+ assert_se(manager_load_startable_unit_or_warn(m, "dml.slice", NULL, &dml) >= 0);
+
+ assert_se(manager_load_startable_unit_or_warn(m, "dml-passthrough.slice", NULL, &dml_passthrough) >= 0);
+ assert_se(UNIT_DEREF(dml_passthrough->slice) == dml);
+ assert_se(manager_load_startable_unit_or_warn(m, "dml-passthrough-empty.service", NULL, &dml_passthrough_empty) >= 0);
+ assert_se(UNIT_DEREF(dml_passthrough_empty->slice) == dml_passthrough);
+ assert_se(manager_load_startable_unit_or_warn(m, "dml-passthrough-set-dml.service", NULL, &dml_passthrough_set_dml) >= 0);
+ assert_se(UNIT_DEREF(dml_passthrough_set_dml->slice) == dml_passthrough);
+ assert_se(manager_load_startable_unit_or_warn(m, "dml-passthrough-set-ml.service", NULL, &dml_passthrough_set_ml) >= 0);
+ assert_se(UNIT_DEREF(dml_passthrough_set_ml->slice) == dml_passthrough);
+
+ assert_se(manager_load_startable_unit_or_warn(m, "dml-override.slice", NULL, &dml_override) >= 0);
+ assert_se(UNIT_DEREF(dml_override->slice) == dml);
+ assert_se(manager_load_startable_unit_or_warn(m, "dml-override-empty.service", NULL, &dml_override_empty) >= 0);
+ assert_se(UNIT_DEREF(dml_override_empty->slice) == dml_override);
+
+ assert_se(manager_load_startable_unit_or_warn(m, "dml-discard.slice", NULL, &dml_discard) >= 0);
+ assert_se(UNIT_DEREF(dml_discard->slice) == dml);
+ assert_se(manager_load_startable_unit_or_warn(m, "dml-discard-empty.service", NULL, &dml_discard_empty) >= 0);
+ assert_se(UNIT_DEREF(dml_discard_empty->slice) == dml_discard);
+ assert_se(manager_load_startable_unit_or_warn(m, "dml-discard-set-ml.service", NULL, &dml_discard_set_ml) >= 0);
+ assert_se(UNIT_DEREF(dml_discard_set_ml->slice) == dml_discard);
+
+ root = UNIT_DEREF(dml->slice);
+ assert_se(!UNIT_ISSET(root->slice));
+
+ assert_se(unit_get_ancestor_memory_low(root) == CGROUP_LIMIT_MIN);
+
+ assert_se(unit_get_ancestor_memory_low(dml) == CGROUP_LIMIT_MIN);
+ dml_tree_default = unit_get_cgroup_context(dml)->default_memory_low;
+ assert_se(dml_tree_default == 50);
+
+ assert_se(unit_get_ancestor_memory_low(dml_passthrough) == 100);
+ assert_se(unit_get_ancestor_memory_low(dml_passthrough_empty) == dml_tree_default);
+ assert_se(unit_get_ancestor_memory_low(dml_passthrough_set_dml) == 50);
+ assert_se(unit_get_ancestor_memory_low(dml_passthrough_set_ml) == 25);
+
+ assert_se(unit_get_ancestor_memory_low(dml_override) == dml_tree_default);
+ assert_se(unit_get_ancestor_memory_low(dml_override_empty) == 10);
+
+ assert_se(unit_get_ancestor_memory_low(dml_discard) == dml_tree_default);
+ assert_se(unit_get_ancestor_memory_low(dml_discard_empty) == CGROUP_LIMIT_MIN);
+ assert_se(unit_get_ancestor_memory_low(dml_discard_set_ml) == 15);
+
+ return 0;
+}
+
+int main(int argc, char* argv[]) {
+ int rc = EXIT_SUCCESS;
+
+ test_setup_logging(LOG_DEBUG);
+
+ TEST_REQ_RUNNING_SYSTEMD(rc = test_default_memory_low());
+
+ return rc;
+}
--- /dev/null
+[Unit]
+Description=DML discard empty service
+
+[Service]
+Slice=dml-discard.slice
+Type=oneshot
+ExecStart=/bin/true
--- /dev/null
+[Unit]
+Description=DML discard set ml service
+
+[Service]
+Slice=dml-discard.slice
+Type=oneshot
+ExecStart=/bin/true
+MemoryLow=15
--- /dev/null
+[Unit]
+Description=DML discard slice
+
+[Slice]
+DefaultMemoryLow=
--- /dev/null
+[Unit]
+Description=DML override empty service
+
+[Service]
+Slice=dml-override.slice
+Type=oneshot
+ExecStart=/bin/true
--- /dev/null
+[Unit]
+Description=DML override slice
+
+[Slice]
+DefaultMemoryLow=10
--- /dev/null
+[Unit]
+Description=DML passthrough empty service
+
+[Service]
+Slice=dml-passthrough.slice
+Type=oneshot
+ExecStart=/bin/true
--- /dev/null
+[Unit]
+Description=DML passthrough set DML service
+
+[Service]
+Slice=dml-passthrough.slice
+Type=oneshot
+ExecStart=/bin/true
+DefaultMemoryLow=15
--- /dev/null
+[Unit]
+Description=DML passthrough set ML service
+
+[Service]
+Slice=dml-passthrough.slice
+Type=oneshot
+ExecStart=/bin/true
+MemoryLow=25
--- /dev/null
+[Unit]
+Description=DML passthrough slice
+
+[Slice]
+MemoryLow=100
--- /dev/null
+[Unit]
+Description=DML slice
+
+[Slice]
+DefaultMemoryLow=50
c.service
d.service
daughter.service
+ dml.slice
+ dml-passthrough.slice
+ dml-passthrough-empty.service
+ dml-passthrough-set-dml.service
+ dml-passthrough-set-ml.service
+ dml-override.slice
+ dml-override-empty.service
+ dml-discard.slice
+ dml-discard-empty.service
+ dml-discard-set-ml.service
e.service
end.service
f.service