From 335792b12b1d7993fec8f06026769c24f32684a7 Mon Sep 17 00:00:00 2001 From: Giuseppe Scrivano Date: Mon, 26 Jan 2026 15:26:41 +0100 Subject: [PATCH] scheduler: add diagnostic messages for SCHED_DEADLINE Signed-off-by: Giuseppe Scrivano Signed-off-by: Kir Kolyshkin --- src/libcrun/scheduler.c | 48 ++++++- tests/test_scheduler.py | 300 ++++++++++++++++++++++++++++++++++++++-- tests/tests_utils.py | 6 +- 3 files changed, 342 insertions(+), 12 deletions(-) diff --git a/src/libcrun/scheduler.c b/src/libcrun/scheduler.c index ba48ed6199..2aa5e75202 100644 --- a/src/libcrun/scheduler.c +++ b/src/libcrun/scheduler.c @@ -96,6 +96,52 @@ libcrun_reset_cpu_affinity_mask (pid_t pid, libcrun_error_t *err) return 0; } +static int +diagnose_scheduler_failure (libcrun_error_t *err, runtime_spec_schema_config_schema_process *process, + struct sched_attr_s *attr) +{ + if (attr->sched_policy == SCHED_DEADLINE && errno == EINVAL) + { + if (! process->scheduler->runtime_present) + return crun_make_error (err, errno, "sched_setattr: `SCHED_DEADLINE` requires `runtime`"); + if (! process->scheduler->deadline_present) + return crun_make_error (err, errno, "sched_setattr: `SCHED_DEADLINE` requires `deadline`"); + + if (attr->sched_runtime == 0) + return crun_make_error (err, errno, "sched_setattr: `SCHED_DEADLINE` runtime must be greater than 0"); + if (attr->sched_deadline == 0) + return crun_make_error (err, errno, "sched_setattr: `SCHED_DEADLINE` deadline must be greater than 0"); + /* Per sched(7), ff sched_period is specified as 0, then it is made the same as sched_deadline. */ + + if (attr->sched_runtime > attr->sched_deadline) + return crun_make_error (err, errno, "sched_setattr: `SCHED_DEADLINE` runtime (%" PRIu64 ") must be <= deadline (%" PRIu64 ")", + attr->sched_runtime, attr->sched_deadline); + if (attr->sched_period != 0 && attr->sched_deadline > attr->sched_period) + return crun_make_error (err, errno, "sched_setattr: `SCHED_DEADLINE` deadline (%" PRIu64 ") must be <= period (%" PRIu64 ")", + attr->sched_deadline, attr->sched_period); + + /* sched(7) says "under the current implementation, all of the parameter values + * must be at least 1024 <...> and less than 2^63". */ + const uint64_t min = 1024; + const uint64_t max = 1ULL << 63; + + if (attr->sched_runtime < min || attr->sched_runtime > max) + return crun_make_error (err, errno, "sched_setattr: `SCHED_DEADLINE` runtime (%" PRIu64 ") must be between %" PRIu64 " and %" PRIu64, + attr->sched_runtime, min, max); + if (attr->sched_deadline < min || attr->sched_deadline > max) + return crun_make_error (err, errno, "sched_setattr: `SCHED_DEADLINE` deadline (%" PRIu64 ") must be between %" PRIu64 " and %" PRIu64, + attr->sched_deadline, min, max); + if (attr->sched_period != 0 && (attr->sched_period < min || attr->sched_period > max)) + return crun_make_error (err, errno, "sched_setattr: `SCHED_DEADLINE` period (%" PRIu64 ") must be between %" PRIu64 " and %" PRIu64, + attr->sched_period, min, max); + + return crun_make_error (err, errno, "sched_setattr: invalid `SCHED_DEADLINE` parameters (runtime=%" PRIu64 ", deadline=%" PRIu64 ", period=%" PRIu64 ")", + attr->sched_runtime, attr->sched_deadline, attr->sched_period); + } + + return crun_make_error (err, errno, "sched_setattr"); +} + int libcrun_set_scheduler (pid_t pid, runtime_spec_schema_config_schema_process *process, libcrun_error_t *err) { @@ -176,7 +222,7 @@ libcrun_set_scheduler (pid_t pid, runtime_spec_schema_config_schema_process *pro ret = syscall_sched_setattr (pid, &attr, 0); if (UNLIKELY (ret < 0)) - return crun_make_error (err, errno, "sched_setattr"); + return diagnose_scheduler_failure (err, process, &attr); return 0; } diff --git a/tests/test_scheduler.py b/tests/test_scheduler.py index 3f91a83889..6e4dba6151 100755 --- a/tests/test_scheduler.py +++ b/tests/test_scheduler.py @@ -194,9 +194,11 @@ def test_scheduler_nice_value(): def test_scheduler_deadline(): - """Test SCHED_DEADLINE scheduler policy.""" + """Test SCHED_DEADLINE scheduler policy with all parameters (working case).""" if is_rootless(): return (77, "SCHED_DEADLINE requires root") + if not is_sched_deadline_available(): + return (77, "SCHED_DEADLINE not available in kernel") conf = base_config() add_all_namespaces(conf) @@ -216,16 +218,41 @@ def test_scheduler_deadline(): return 0 except subprocess.CalledProcessError as e: - output = e.output.decode('utf-8', errors='ignore') if e.output else '' - # SCHED_DEADLINE often not available - if "scheduler" in output.lower() or "permission" in output.lower() or "deadline" in output.lower(): - return (77, "SCHED_DEADLINE not available") logger.info("test failed: %s", e) return -1 except Exception as e: - error_str = str(e).lower() - if "deadline" in error_str or "scheduler" in error_str: - return (77, "SCHED_DEADLINE not available") + logger.info("test failed: %s", e) + return -1 + + +def test_scheduler_deadline_no_period(): + """Test SCHED_DEADLINE scheduler policy without period (should work - kernel allows it).""" + if is_rootless(): + return (77, "SCHED_DEADLINE requires root") + if not is_sched_deadline_available(): + return (77, "SCHED_DEADLINE not available in kernel") + + conf = base_config() + add_all_namespaces(conf) + + # SCHED_DEADLINE with runtime and deadline but no period + # Kernel should accept this and use deadline as period + conf['process']['scheduler'] = { + 'policy': 'SCHED_DEADLINE', + 'runtime': 10000000, # 10ms + 'deadline': 20000000 # 20ms, no period + } + + conf['process']['args'] = ['/init', 'true'] + + try: + out, _ = run_and_get_output(conf, hide_stderr=True) + return 0 # Should succeed + + except subprocess.CalledProcessError as e: + logger.info("test failed: %s", e) + return -1 + except Exception as e: logger.info("test failed: %s", e) return -1 @@ -251,12 +278,257 @@ def test_scheduler_flags(): out, _ = run_and_get_output(conf, hide_stderr=True) return 0 + except subprocess.CalledProcessError as e: + logger.info("test failed: %s", e) + return -1 + except Exception as e: + logger.info("test failed: %s", e) + return -1 + + +def test_scheduler_deadline_missing_runtime(): + """Test SCHED_DEADLINE validation - missing runtime parameter.""" + if is_rootless(): + return (77, "SCHED_DEADLINE requires root") + if not is_sched_deadline_available(): + return (77, "SCHED_DEADLINE not available in kernel") + + conf = base_config() + add_all_namespaces(conf) + + # Missing runtime parameter + conf['process']['scheduler'] = { + 'policy': 'SCHED_DEADLINE', + 'deadline': 20000000, # 20ms + 'period': 20000000 # 20ms + } + + conf['process']['args'] = ['/init', 'true'] + + try: + out, _ = run_and_get_output(conf, hide_stderr=False) + # Should have failed due to missing runtime + return -1 + except subprocess.CalledProcessError as e: output = e.output.decode('utf-8', errors='ignore') if e.output else '' - if "scheduler" in output.lower() or "permission" in output.lower(): - return (77, "scheduler flags not available") + if "sched_setattr: `SCHED_DEADLINE` requires `runtime`" in output: + return 0 # Expected validation error + logger.info("unexpected error: %s", output) + return -1 + except Exception as e: logger.info("test failed: %s", e) return -1 + + +def test_scheduler_deadline_missing_deadline(): + """Test SCHED_DEADLINE validation - missing deadline parameter.""" + if is_rootless(): + return (77, "SCHED_DEADLINE requires root") + if not is_sched_deadline_available(): + return (77, "SCHED_DEADLINE not available in kernel") + + conf = base_config() + add_all_namespaces(conf) + + # Missing deadline parameter + conf['process']['scheduler'] = { + 'policy': 'SCHED_DEADLINE', + 'runtime': 10000000, # 10ms + 'period': 20000000 # 20ms + } + + conf['process']['args'] = ['/init', 'true'] + + try: + out, _ = run_and_get_output(conf, hide_stderr=False) + # Should have failed due to missing deadline + return -1 + + except subprocess.CalledProcessError as e: + output = e.output.decode('utf-8', errors='ignore') if e.output else '' + if "sched_setattr: `SCHED_DEADLINE` requires `deadline`" in output: + return 0 # Expected validation error + logger.info("unexpected error: %s", output) + return -1 + except Exception as e: + logger.info("test failed: %s", e) + return -1 + + + + +def test_scheduler_deadline_zero_runtime(): + """Test SCHED_DEADLINE validation - zero runtime.""" + if is_rootless(): + return (77, "SCHED_DEADLINE requires root") + if not is_sched_deadline_available(): + return (77, "SCHED_DEADLINE not available in kernel") + + conf = base_config() + add_all_namespaces(conf) + + # Zero runtime + conf['process']['scheduler'] = { + 'policy': 'SCHED_DEADLINE', + 'runtime': 0, + 'deadline': 20000000, # 20ms + 'period': 20000000 # 20ms + } + + conf['process']['args'] = ['/init', 'true'] + + try: + out, _ = run_and_get_output(conf, hide_stderr=False) + # Should have failed due to zero runtime + return -1 + + except subprocess.CalledProcessError as e: + output = e.output.decode('utf-8', errors='ignore') if e.output else '' + if "sched_setattr: `SCHED_DEADLINE` runtime must be greater than 0" in output: + return 0 # Expected validation error + logger.info("unexpected error: %s", output) + return -1 + except Exception as e: + logger.info("test failed: %s", e) + return -1 + + +def test_scheduler_deadline_invalid_order(): + """Test SCHED_DEADLINE validation - runtime > deadline.""" + if is_rootless(): + return (77, "SCHED_DEADLINE requires root") + if not is_sched_deadline_available(): + return (77, "SCHED_DEADLINE not available in kernel") + + conf = base_config() + add_all_namespaces(conf) + + # runtime > deadline (invalid) + conf['process']['scheduler'] = { + 'policy': 'SCHED_DEADLINE', + 'runtime': 30000000, # 30ms + 'deadline': 20000000, # 20ms + 'period': 40000000 # 40ms + } + + conf['process']['args'] = ['/init', 'true'] + + try: + out, _ = run_and_get_output(conf, hide_stderr=False) + # Should have failed due to invalid order + return -1 + + except subprocess.CalledProcessError as e: + output = e.output.decode('utf-8', errors='ignore') if e.output else '' + if "sched_setattr: `SCHED_DEADLINE` runtime" in output and "must be <=" in output and "deadline" in output: + return 0 # Expected validation error + logger.info("unexpected error: %s", output) + return -1 + except Exception as e: + logger.info("test failed: %s", e) + return -1 + + +def test_scheduler_deadline_invalid_deadline_period(): + """Test SCHED_DEADLINE validation - deadline > period.""" + if is_rootless(): + return (77, "SCHED_DEADLINE requires root") + if not is_sched_deadline_available(): + return (77, "SCHED_DEADLINE not available in kernel") + + conf = base_config() + add_all_namespaces(conf) + + # deadline > period (invalid) + conf['process']['scheduler'] = { + 'policy': 'SCHED_DEADLINE', + 'runtime': 10000000, # 10ms + 'deadline': 30000000, # 30ms + 'period': 20000000 # 20ms + } + + conf['process']['args'] = ['/init', 'true'] + + try: + out, _ = run_and_get_output(conf, hide_stderr=False) + # Should have failed due to invalid order + return -1 + + except subprocess.CalledProcessError as e: + output = e.output.decode('utf-8', errors='ignore') if e.output else '' + if "sched_setattr: `SCHED_DEADLINE` deadline" in output and "must be <=" in output and "period" in output: + return 0 # Expected validation error + logger.info("unexpected error: %s", output) + return -1 + except Exception as e: + logger.info("test failed: %s", e) + return -1 + + +def test_scheduler_deadline_too_small_runtime(): + """Test SCHED_DEADLINE validation - runtime < min.""" + if is_rootless(): + return (77, "SCHED_DEADLINE requires root") + if not is_sched_deadline_available(): + return (77, "SCHED_DEADLINE not available in kernel") + + conf = base_config() + add_all_namespaces(conf) + + conf['process']['scheduler'] = { + 'policy': 'SCHED_DEADLINE', + 'runtime': 1023, # too small + 'deadline': 10000000, # 10ms + } + + conf['process']['args'] = ['/init', 'true'] + + try: + out, _ = run_and_get_output(conf, hide_stderr=False) + # Should have failed due to too small runtime. + return -1 + + except subprocess.CalledProcessError as e: + output = e.output.decode('utf-8', errors='ignore') if e.output else '' + if "sched_setattr: `SCHED_DEADLINE` runtime " in output and " must be between " in output: + return 0 # Expected validation error + logger.info("unexpected error: %s", output) + return -1 + except Exception as e: + logger.info("test failed: %s", e) + return -1 + + +def test_scheduler_deadline_too_big_runtime(): + """Test SCHED_DEADLINE validation - runtime > max.""" + if is_rootless(): + return (77, "SCHED_DEADLINE requires root") + if not is_sched_deadline_available(): + return (77, "SCHED_DEADLINE not available in kernel") + + conf = base_config() + add_all_namespaces(conf) + + conf['process']['scheduler'] = { + 'policy': 'SCHED_DEADLINE', + 'runtime': 9223372036854775809, + 'deadline': 9223372036854775810, + } + + conf['process']['args'] = ['/init', 'true'] + + try: + out, _ = run_and_get_output(conf, hide_stderr=False) + # Should have failed due to too big runtime. + return -1 + + except subprocess.CalledProcessError as e: + output = e.output.decode('utf-8', errors='ignore') if e.output else '' + if "sched_setattr: `SCHED_DEADLINE` runtime " in output and " must be between " in output: + return 0 # Expected validation error + logger.info("unexpected error: %s", output) + return -1 except Exception as e: logger.info("test failed: %s", e) return -1 @@ -270,7 +542,15 @@ def test_scheduler_flags(): "scheduler-other": test_scheduler_other, "scheduler-nice-value": test_scheduler_nice_value, "scheduler-deadline": test_scheduler_deadline, + "scheduler-deadline-no-period": test_scheduler_deadline_no_period, "scheduler-flags": test_scheduler_flags, + "scheduler-deadline-missing-runtime": test_scheduler_deadline_missing_runtime, + "scheduler-deadline-missing-deadline": test_scheduler_deadline_missing_deadline, + "scheduler-deadline-zero-runtime": test_scheduler_deadline_zero_runtime, + "scheduler-deadline-invalid-order": test_scheduler_deadline_invalid_order, + "scheduler-deadline-invalid-deadline-period": test_scheduler_deadline_invalid_deadline_period, + "scheduler-deadline-too-small-runtime": test_scheduler_deadline_too_small_runtime, + "scheduler-deadline-too-big-runtime": test_scheduler_deadline_too_big_runtime, } if __name__ == "__main__": diff --git a/tests/tests_utils.py b/tests/tests_utils.py index 1bfce451ce..783424f0c3 100755 --- a/tests/tests_utils.py +++ b/tests/tests_utils.py @@ -38,7 +38,7 @@ # Export logger for use in test files __all__ = ['logger', 'base_config', 'run_and_get_output', 'run_crun_command', 'run_crun_command_raw', 'parse_proc_status', 'add_all_namespaces', 'tests_main', 'is_rootless', - 'is_cgroup_v2_unified', 'get_crun_feature_string', 'running_on_systemd', + 'is_cgroup_v2_unified', 'is_sched_deadline_available', 'get_crun_feature_string', 'running_on_systemd', 'get_tests_root', 'get_tests_root_status', 'get_init_path', 'get_crun_path', 'get_cgroup_manager', 'get_test_environment'] @@ -484,6 +484,10 @@ def is_rootless(): def is_cgroup_v2_unified(): return subprocess.check_output("stat -c%T -f /sys/fs/cgroup".split()).decode("utf-8").strip() == "cgroup2fs" +def is_sched_deadline_available(): + """Check if SCHED_DEADLINE is available in the kernel.""" + return os.path.exists("/proc/sys/kernel/sched_deadline_period_max_us") + def get_crun_feature_string(): for i in run_crun_command(['--version']).split('\n'): if i.startswith('+'):