init: gain real-time through rtkit (#15411)

* init: gain real-time through rtkit

* init: use sdbus to talk with rtkit

* init: simplify

* ci: add sdbus-cpp dependency

* init: add HAS_RTKIT def
This commit is contained in:
André Silva
2026-07-16 20:11:37 +02:00
committed by GitHub
parent b500399878
commit 83cd85b3d5
4 changed files with 184 additions and 10 deletions
+1
View File
@@ -56,6 +56,7 @@ runs:
pkgconf \
pugixml \
scdoc \
sdbus-cpp \
seatd \
systemd \
tomlplusplus \
+9
View File
@@ -360,6 +360,15 @@ if(NOT HAS_INOTIFY AND inotify_FOUND)
target_link_libraries(hyprland_lib PUBLIC PkgConfig::inotify)
endif()
# sdbus-c++ is used to ask rtkit for realtime scheduling which is Linux-only
if(NO_RTKIT OR NOT CMAKE_SYSTEM_NAME STREQUAL "Linux")
message(STATUS "rtkit realtime scheduling is disabled!")
else()
pkg_check_modules(sdbus_dep REQUIRED IMPORTED_TARGET sdbus-c++>=2.0.0)
target_link_libraries(hyprland_lib PUBLIC PkgConfig::sdbus_dep)
add_compile_definitions(HAS_RTKIT)
endif()
if(NO_XWAYLAND)
message(STATUS "Using the NO_XWAYLAND flag, disabling XWayland!")
add_compile_definitions(NO_XWAYLAND)
+2
View File
@@ -43,6 +43,7 @@
pciutils,
python3,
re2,
sdbus-cpp_2,
systemd,
tomlplusplus,
udis86-hyprland,
@@ -202,6 +203,7 @@ customStdenv.mkDerivation (finalAttrs: {
pango
pciutils
re2
sdbus-cpp_2
tomlplusplus
udis86-hyprland
wayland
+172 -10
View File
@@ -3,24 +3,182 @@
#include "initHelpers.hpp"
#ifdef HAS_RTKIT
#include <sys/resource.h>
#include <sys/syscall.h>
#include <algorithm>
#include <cerrno>
#include <chrono>
#include <cstring>
#include <sdbus-c++/sdbus-c++.h>
#include <hyprutils/memory/Casts.hpp>
#endif
bool NInit::isSudo() {
return getuid() != geteuid() || !geteuid();
}
void NInit::gainRealTime() {
const int minPrio = sched_get_priority_min(SCHED_RR);
int old_policy;
struct sched_param param;
// Asks the kernel directly to put the calling thread on SCHED_RR. Only succeeds if
// the process is privileged to go realtime on its own: CAP_SYS_NICE (e.g. granted via
// setcap or a security wrapper) or a nonzero RLIMIT_RTPRIO (e.g. a "realtime" group
// entry in limits.conf).
static bool trySchedDirect(int prio) {
#ifdef HAS_RTKIT
// SCHED_RESET_ON_FORK keeps children from inheriting realtime, the same kernel mechanism
// rtkit's grants use. Raw syscall because not every libc exposes the per-thread Linux
// semantics of sched_setscheduler (musl returns ENOSYS).
const struct sched_param param = {.sched_priority = prio};
return syscall(SYS_sched_setscheduler, gettid(), SCHED_RR | SCHED_RESET_ON_FORK, &param) == 0;
#else
int oldPolicy = 0;
struct sched_param param = {};
if (pthread_getschedparam(pthread_self(), &old_policy, &param)) {
if (pthread_getschedparam(pthread_self(), &oldPolicy, &param)) {
Log::logger->log(Log::WARN, "Failed to get old pthread scheduling priority");
return;
return false;
}
param.sched_priority = minPrio;
param.sched_priority = prio;
if (pthread_setschedparam(pthread_self(), SCHED_RR, &param)) {
Log::logger->log(Log::WARN, "Failed to change process scheduling strategy");
return pthread_setschedparam(pthread_self(), SCHED_RR, &param) == 0;
#endif
}
#ifdef HAS_RTKIT
// Cached for the SIGXCPU handler: it must demote the RT thread specifically, but a
// process-directed signal can be delivered to any thread.
static pid_t mainThreadTid = 0;
// Demotes the main thread back to SCHED_OTHER instead of letting the process die. With a finite
// RLIMIT_RTTIME, an RT thread that overruns the soft limit without making a blocking syscall gets
// SIGXCPU (fatal by default), then SIGKILL at the hard limit. The counter resets every time the
// thread blocks (i.e. every frame), so this only fires on a genuinely runaway compositor.
// Only async-signal-safe calls in here: raw syscalls and write().
static void handleSigxcpu(int /* signo */) {
// realtime was granted with SCHED_RESET_ON_FORK set (on both paths), keep it as clearing it
// needs CAP_SYS_NICE and the kernel rejects the whole call with EPERM otherwise.
const struct sched_param param = {.sched_priority = 0};
if (syscall(SYS_sched_setscheduler, mainThreadTid, SCHED_OTHER | SCHED_RESET_ON_FORK, &param) == 0) {
constexpr char MSG[] = "Realtime budget exceeded (SIGXCPU), dropping realtime scheduling\n";
std::ignore = write(STDERR_FILENO, MSG, sizeof(MSG) - 1);
} else {
constexpr char MSG[] = "Realtime budget exceeded (SIGXCPU), failed to drop realtime scheduling\n";
std::ignore = write(STDERR_FILENO, MSG, sizeof(MSG) - 1);
}
}
// Installs handleSigxcpu. Must happen before the thread can go realtime, and stays installed for
// the lifetime of the process: SIGXCPU is not delivered to well-behaved non-realtime processes.
static bool installSigxcpuHandler() {
mainThreadTid = gettid();
struct sigaction action = {};
sigemptyset(&action.sa_mask);
action.sa_flags = SA_RESTART;
action.sa_handler = handleSigxcpu;
if (sigaction(SIGXCPU, &action, nullptr)) {
Log::logger->log(Log::WARN, "Failed to install the SIGXCPU handler: {}, not attempting realtime", strerror(errno));
return false;
}
return true;
}
constexpr const char* RTKIT_SERVICE = "org.freedesktop.RealtimeKit1";
constexpr const char* RTKIT_OBJECT = "/org/freedesktop/RealtimeKit1";
constexpr const char* RTKIT_INTERFACE = "org.freedesktop.RealtimeKit1";
constexpr auto RTKIT_DBUS_TIMEOUT = std::chrono::seconds(2); // D-Bus defaults to 25s, a wedged rtkit must not stall startup
// Asks rtkit (org.freedesktop.RealtimeKit1 on the system bus) to put the calling thread on
// SCHED_RR. rtkit hands out realtime scheduling to unprivileged processes, mediated by polkit
// (granted to active sessions only). It addresses threads by kernel tid (gettid), not pthread_t,
// and grants SCHED_RR with SCHED_RESET_ON_FORK set, so fork children reset to SCHED_OTHER
// kernel-side.
static bool tryRtkit(int prio) {
try {
auto connection = sdbus::createSystemBusConnection();
auto proxy = sdbus::createProxy(*connection, sdbus::ServiceName{RTKIT_SERVICE}, sdbus::ObjectPath{RTKIT_OBJECT});
// read RTTimeUSecMax, the largest RLIMIT_RTTIME rtkit grants under.
// Also probes rtkit's liveness before touching RLIMIT_RTTIME: lowering
// the hard limit must not happen unless rtkit is actually there to
// grant us realtime in exchange
sdbus::Variant maxVar;
proxy->callMethod("Get")
.onInterface("org.freedesktop.DBus.Properties")
.withTimeout(RTKIT_DBUS_TIMEOUT)
.withArguments(RTKIT_INTERFACE, "RTTimeUSecMax")
.storeResultsTo(maxVar);
const auto maxUs = maxVar.get<int64_t>();
if (maxUs <= 0) {
Log::logger->log(Log::DEBUG, "rtkit: no usable RTTimeUSecMax, not requesting realtime");
return false;
}
// rtkit refuses to grant realtime unless the caller has a finite RLIMIT_RTTIME at or
// below its RTTimeUSecMax. Lowering the hard limit is irreversible without
// CAP_SYS_RESOURCE and the new limit is inherited by forked children: apps launched from
// Hyprland that gain realtime through their own privileges will live under this budget
// too.
rlimit current = {};
if (getrlimit(RLIMIT_RTTIME, &current)) {
Log::logger->log(Log::WARN, "rtkit: failed to get RLIMIT_RTTIME: {}", strerror(errno));
return false;
}
// an unprivileged process cannot raise a finite hard limit, so stay under a pre-existing
// one (RLIM_INFINITY compares greater than any finite limit). Keep soft below hard: an
// overrun then raises a catchable SIGXCPU (which our handler turns into a demotion)
// before the uncatchable SIGKILL at the hard limit.
const rlim_t hard = std::min(current.rlim_max, sc<rlim_t>(maxUs));
const rlimit newLimit = {.rlim_cur = std::min(hard / 4 * 3, current.rlim_cur), .rlim_max = hard};
if (setrlimit(RLIMIT_RTTIME, &newLimit)) {
Log::logger->log(Log::WARN, "rtkit: failed to set RLIMIT_RTTIME: {}", strerror(errno));
return false;
}
// fails with a polkit denial for sessions that aren't active locally (e.g. ssh),
// which is fine because realtime is best-effort. the armed RLIMIT_RTTIME must stay,
// hard limits cannot be raised back.
proxy->callMethod("MakeThreadRealtime").onInterface(RTKIT_INTERFACE).withTimeout(RTKIT_DBUS_TIMEOUT).withArguments(sc<uint64_t>(gettid()), sc<uint32_t>(prio));
return true;
} catch (const sdbus::Error& e) {
Log::logger->log(Log::DEBUG, "rtkit: {}", e.what());
return false;
}
}
#endif
// Puts the main thread on SCHED_RR so a busy session cannot starve the compositor of CPU.
// Going realtime directly needs privilege and is tried first, rtkit is the standard
// unprivileged fallback on desktop systems. Best-effort: on failure we just run as SCHED_OTHER.
void NInit::gainRealTime() {
const int minPrio = sched_get_priority_min(SCHED_RR);
bool gained = false;
#ifdef HAS_RTKIT
if (!installSigxcpuHandler())
return;
#endif
gained = trySchedDirect(minPrio);
if (gained)
Log::logger->log(Log::DEBUG, "Gained realtime scheduling directly");
#ifdef HAS_RTKIT
if (!gained && (gained = tryRtkit(minPrio)))
Log::logger->log(Log::DEBUG, "Gained realtime scheduling via rtkit");
#endif
if (!gained) {
Log::logger->log(Log::WARN, "Failed to gain realtime scheduling");
return;
}
@@ -28,9 +186,13 @@ void NInit::gainRealTime() {
// CAP_SYS_NICE due to how the security wrapper works.
prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_LOWER, CAP_SYS_NICE, 0, 0);
#ifndef HAS_RTKIT
// spawned children must not inherit RT, on Linux the kernel handles this via
// SCHED_RESET_ON_FORK on both promotion paths
pthread_atfork(nullptr, nullptr, []() {
const struct sched_param param = {.sched_priority = 0};
if (pthread_setschedparam(pthread_self(), SCHED_OTHER, &param))
Log::logger->log(Log::WARN, "Failed to reset process scheduling strategy");
});
}
#endif
}