Add native SCHED_FIFO promotion for the Linux build without dbus - #45
Conversation
The Linux build without the `dbus` feature previously fell through to the blanket fallback, where promotion was a no-op that returned Ok, so callers believed they had real-time priority when they did not. Give that build a real implementation: promote the current thread directly with pthread_setschedparam(SCHED_FIFO | SCHED_RESET_ON_FORK) at priority 10, restoring the saved policy on demotion. This needs no D-Bus daemon and works whenever the process may request real-time scheduling (root, CAP_SYS_NICE, or an RLIMIT_RTPRIO budget). The rtkit path and other platforms are untouched. - New rt_linux_native.rs with the native backend, wired into a dedicated cfg branch after the rtkit branch. - The requested priority defaults to 10 and can be overridden with the AUDIO_RT_PRIORITY environment variable (1-99). - Tests exercise the RLIMIT_RTPRIO boundary and the priority override; the fallback CI job raises RLIMIT_RTPRIO so promotion runs for real. - Docs: add a Platforms section and correct API docs that assumed D-Bus was the only Linux backend.
A library reading an environment variable it was never told about is a hidden global input that can surprise the embedding application. Replace it with an explicit, optional setter that the caller drives: set_rt_priority(Some(n)) overrides the default of 10, None restores it. Linux no-dbus only; if never called, promotion uses priority 10 as before.
- Drop the `param` field from RtPriorityThreadInfoInternal so the struct has no padding, matching the rtkit path's struct. `serialize`'s transmute no longer reads uninitialized padding bytes (which was UB). Demotion now restores the saved policy with a zeroed param, as the rtkit path already does. - rt_scheduling_available: treat a demotion failure after a successful promotion as fatal rather than ignoring it. - Fix a stale test comment that referred to the removed environment variable.
padenot
left a comment
There was a problem hiding this comment.
Looks good, one small question before we can merge. I'll tag a release when it's done.
It claimed the thread info was only meaningful within the same process, which contradicts promote_thread_to_real_time_internal promoting a thread in another process. The thread is identified by its system-wide tid, so cross-process promotion works; correct the comment to say so.
roderickvd
left a comment
There was a problem hiding this comment.
Thanks for putting in this PR. I stumbled on it as I was investigating the same thing on cpal. Hence my drive-by review 😄
Would this be SemVer-breaking for audio_thread_priority?
|
|
||
| let rc = unsafe { | ||
| libc::sched_setscheduler( | ||
| thread_info.thread_id as libc::pid_t, |
There was a problem hiding this comment.
thread_id is c_long and i64 on 64-bit Linux. Casting it to i32 pid_t is probably safe, but defensively it could make sense to keep it in its original width?
There was a problem hiding this comment.
sched_setscheduler takes pid_t (i32) so we narrow at the syscall regardless, but you're right it's cleaner to be explicit. Switched to a checked try_from in f34b7be. The field stays c_long to mirror the rtkit struct.
| let rc = unsafe { | ||
| libc::sched_setscheduler( | ||
| thread_info.thread_id as libc::pid_t, | ||
| thread_info.policy | SCHED_RESET_ON_FORK, |
There was a problem hiding this comment.
This restores the original policy, whereas the D-Bus version hardcodes SCHED_OTHER. I like yours better... regardless of which it'll be, consistency would be a good thing to have.
There was a problem hiding this comment.
Agreed, and the dbus path is inconsistent with itself too: current-thread demote restores the saved policy, remote demote hardcodes SCHED_OTHER. I'd rather just make everything restore the saved policy here in this PR than split it into a follow-up. It touches the dbus path though, so @padenot your call.
There was a problem hiding this comment.
Done in ac3a784. The rtkit remote demote now restores the saved policy too, so both paths (and both the current-thread and remote demotes) are consistent.
| // Keep SCHED_RESET_ON_FORK set: the kernel forbids an unprivileged thread from clearing that | ||
| // flag once set (and promotion set it), so restoring the bare saved policy would fail with | ||
| // EPERM. The flag is harmless on a non-real-time thread. | ||
| let param = unsafe { std::mem::zeroed::<libc::sched_param>() }; |
There was a problem hiding this comment.
This breaks if a thread already was SCHED_FIFO or SCHED_RR with nonzero priority before we promoted it. I think it's the same in the D-Bus version.
There was a problem hiding this comment.
Good catch, same as the dbus version. I saved and restored the full param in an earlier revision but dropped it to fix a padding-UB in the serialized struct. I can bring it back and restore the priority faithfully (with padding-safe serialization). Since it's the same issue in both paths, I'd fix it in both rather than non-dbus only. @padenot, ok to do that here?
There was a problem hiding this comment.
Done in ac3a784. Both paths now capture the priority in get_current_thread_info and restore it on demote. Adding the field gives the struct tail padding, so I switched serialize/deserialize to explicit byte packing instead of transmuting, so no uninitialized padding is read. ATP_THREAD_INFO_SIZE goes 24 to 32 as a result.
sched_setscheduler takes pid_t (i32), so the tid (c_long) must be narrowed at the syscall. A tid always fits, but convert with try_from and surface an error instead of truncating silently.
|
Thanks for taking a look! Not SemVer-breaking API-wise, it only adds |
|
Thanks for the quick adjustments too! I would also prefer fixing the consistency in one swoop, over the administrative burden of split PRs. I wouldn’t mind a SemVer bump because the behavior would be different on non-dbus targets. And I could then adjust the documentation accordingly in cpal, when Cargo and I can clearly discern between two releases. |
|
Yes, let's get everything fixed here and now, thanks a lot for the comments and prompt patch updates. I'll do a SemVer bump (minor) as well right after merging this. |
Demotion restored the saved policy but always with priority 0, so a thread that was already SCHED_FIFO/RR with a nonzero priority before promotion could not be restored (it would fail with EINVAL). Capture the priority in get_current_thread_info and restore it on demotion, in both the native and rtkit paths. Also make the rtkit remote demote restore the saved policy instead of hardcoding SCHED_OTHER, matching the native path and the current-thread demote. Storing the extra field gives the info struct tail padding, so serialize the fields explicitly (chaining their bytes) instead of transmuting the whole struct, which would read the uninitialized padding bytes.
padenot
left a comment
There was a problem hiding this comment.
Thanks a lot for the extra roundtrip here. Merging and doing a release now.
| // Keep SCHED_RESET_ON_FORK set: the kernel forbids an unprivileged thread from clearing that | ||
| // flag once set (and promotion set it), so restoring the bare saved policy would fail with | ||
| // EPERM. The flag is harmless on a non-real-time thread. | ||
| let param = unsafe { std::mem::zeroed::<libc::sched_param>() }; |
Give the Linux no-
dbusbuild a real implementation instead of a no-op:promote the current thread directly with
pthread_setschedparam(SCHED_FIFO).The rtkit path and other platforms are untouched. See #44 for the rationale.
rt_linux_native.rs, wired in via a dedicated cfg branch after the rtkit branchset_rt_priority()(1-99)SCHED_RESET_ON_FORKset, which anunprivileged thread cannot clear)
RLIMIT_RTPRIOboundary and the priority override; the fallback CIjob raises
RLIMIT_RTPRIOso promotion runs for realLinux backend
Edit: the priority override was originally an
AUDIO_RT_PRIORITYenvironment variable; it is nowthe optional
set_rt_priority()method instead (a library should not read env vars on its own).