[libc-commits] [libc] [libc] Apply scheduler attributes on thread creation (PR #225674)
Pavel Labath via libc-commits
libc-commits at lists.llvm.org
Wed Sep 23 04:59:24 PDT 2026
https://github.com/labath updated https://github.com/llvm/llvm-project/pull/225674
>From 59a77042908b336651de6b24de15a95da32296b2 Mon Sep 17 00:00:00 2001
From: Pavel Labath <pavel at labath.sk>
Date: Tue, 22 Sep 2026 12:04:32 +0000
Subject: [PATCH] [libc] Apply scheduler attributes on thread creation
POSIX specifies that pthread_create should apply the scheduling policy
and parameters given in the attributes object (if PTHREAD_EXPLICIT_SCHED
is set). Linux's clone syscall doesn't support setting scheduling
parameters directly, so we have to apply them from userspace via
sched_setscheduler after the thread is created.
To make sure the thread doesn't start running user code with the wrong
scheduling parameters (and to allow returning an error if
sched_setscheduler fails without leaving a half-running thread), I've
added a startup handshake using the clear_tid futex on the child stack:
- the child waits on the futex if explicit scheduling was requested
- the parent calls setschedparam, and then signals the child to either
continue (running) or abort (exit)
- if the call fails, the child exits immediately, the parent reaps it
via Thread::wait(), and returns the error code to the caller
Assisted-by: Gemini
---
.../__support/threads/linux/CMakeLists.txt | 1 +
libc/src/__support/threads/linux/thread.cpp | 60 +++++-
libc/src/__support/threads/thread.h | 10 +-
libc/src/pthread/pthread_create.cpp | 17 +-
.../integration/src/pthread/CMakeLists.txt | 8 +
.../src/pthread/pthread_create_test.cpp | 195 ++++++++++++++++++
6 files changed, 276 insertions(+), 15 deletions(-)
diff --git a/libc/src/__support/threads/linux/CMakeLists.txt b/libc/src/__support/threads/linux/CMakeLists.txt
index d82b0c8e979187..ded24b9b25417e 100644
--- a/libc/src/__support/threads/linux/CMakeLists.txt
+++ b/libc/src/__support/threads/linux/CMakeLists.txt
@@ -70,6 +70,7 @@ add_object_library(
libc.hdr.sys_mman_macros
libc.src.errno.errno
libc.src.__support.CPP.atomic
+ libc.src.__support.CPP.optional
libc.src.__support.CPP.stringstream
libc.src.__support.CPP.string_view
libc.src.__support.common
diff --git a/libc/src/__support/threads/linux/thread.cpp b/libc/src/__support/threads/linux/thread.cpp
index df294e2e4b4fbf..df2d6cf6be8bfa 100644
--- a/libc/src/__support/threads/linux/thread.cpp
+++ b/libc/src/__support/threads/linux/thread.cpp
@@ -9,6 +9,7 @@
#include "src/__support/threads/thread.h"
#include "config/app.h"
#include "src/__support/CPP/atomic.h"
+#include "src/__support/CPP/optional.h"
#include "src/__support/CPP/string_view.h"
#include "src/__support/CPP/stringstream.h"
#include "src/__support/OSUtil/linux/syscall_wrappers/clone.h"
@@ -145,9 +146,29 @@ cleanup_thread_resources(ThreadAttributes *attrib) {
free_stack(attrib->stack, attrib->stacksize, attrib->guardsize);
}
+// Enum values used for controlling thread startup.
+enum class ClearTidState : FutexWordType {
+ EXITED = 0, // Set by the kernel when the thread exits.
+ WAITING, // Waiting for additional initialization.
+ RUNNING, // Initialization complete or not needed.
+ ABORT, // Initialization failed.
+};
+
static int start_thread(void *arg) {
auto *start_args = reinterpret_cast<StartArgs *>(arg);
auto *attrib = start_args->thread_attrib;
+ auto *clear_tid = static_cast<Futex *>(attrib->platform_data);
+
+ // If explicit scheduling parameters were requested, wait for the parent to
+ // apply them via setschedparam. If setschedparam fails, the parent signals
+ // ABORT, and the child exits immediately without running user code.
+ clear_tid->wait(static_cast<FutexWordType>(ClearTidState::WAITING),
+ cpp::nullopt, /*is_shared=*/false);
+ if (clear_tid->load(cpp::MemoryOrder::ACQUIRE) ==
+ static_cast<FutexWordType>(ClearTidState::ABORT)) {
+ LIBC_NAMESPACE::syscall_impl<long>(SYS_exit, 0);
+ __builtin_unreachable();
+ }
if (attrib->style == ThreadStyle::POSIX) {
ThreadReturnValue retval = start_args->runner.posix_runner(start_args->arg);
@@ -160,7 +181,8 @@ static int start_thread(void *arg) {
}
int Thread::run(ThreadStyle style, ThreadRunner runner, void *arg, void *stack,
- size_t stacksize, size_t guardsize, bool detached) {
+ size_t stacksize, size_t guardsize, bool detached,
+ cpp::optional<SchedParameters> sched_params) {
bool owned_stack = false;
if (stack == nullptr) {
// TODO: Should we return EINVAL here? Should we have a generic concept of a
@@ -249,17 +271,18 @@ int Thread::run(ThreadStyle style, ThreadRunner runner, void *arg, void *stack,
attrib->tls_size = tls.size;
attrib->joiner = nullptr;
- start_args->thread_attrib = attrib;
- start_args->runner = runner;
- start_args->arg = arg;
-
- auto clear_tid = reinterpret_cast<Futex *>(
+ auto *clear_tid = reinterpret_cast<Futex *>(
adjusted_stack + sizeof(StartArgs) + sizeof(ThreadAttributes));
- clear_tid->set(1);
+ clear_tid->set(static_cast<FutexWordType>(
+ sched_params ? ClearTidState::WAITING : ClearTidState::RUNNING));
attrib->platform_data = clear_tid;
get_tcb(tls.tp)->attrib = attrib;
+ start_args->thread_attrib = attrib;
+ start_args->runner = runner;
+ start_args->arg = arg;
+
auto clone_result = linux_syscalls::clone(
start_thread, reinterpret_cast<void *>(adjusted_stack),
CLONE_SYSCALL_FLAGS, start_args, &attrib->tid,
@@ -268,10 +291,29 @@ int Thread::run(ThreadStyle style, ThreadRunner runner, void *arg, void *stack,
if (!clone_result.has_value()) {
cleanup_thread_resources(attrib);
+ attrib = nullptr;
return clone_result.error();
}
- return 0;
+ if (!sched_params)
+ return 0;
+
+ // Linux clone does not accept scheduling parameters directly, so we must
+ // apply them after thread creation. If setschedparam fails, signal the child
+ // to abort, wait for it to exit, and clean up its resources.
+ int sched_result = setschedparam(*sched_params);
+ ClearTidState state =
+ sched_result != 0 ? ClearTidState::ABORT : ClearTidState::RUNNING;
+ clear_tid->store(static_cast<FutexWordType>(state),
+ cpp::MemoryOrder::RELEASE);
+ clear_tid->notify_one(/*is_shared=*/false);
+
+ if (sched_result != 0) {
+ wait();
+ cleanup_thread_resources(attrib);
+ attrib = nullptr;
+ }
+ return sched_result;
}
int Thread::join(ThreadReturnValue &retval) {
@@ -329,7 +371,7 @@ void Thread::wait() {
// The kernel should set the value at the clear tid address to zero.
// If not, it is a spurious wake and we should continue to wait on
// the futex.
- auto *clear_tid = reinterpret_cast<Futex *>(attrib->platform_data);
+ auto *clear_tid = static_cast<Futex *>(attrib->platform_data);
// We cannot do a FUTEX_WAIT_PRIVATE here as the kernel does a
// FUTEX_WAKE and not a FUTEX_WAKE_PRIVATE.
FutexWordType clear_tid_value;
diff --git a/libc/src/__support/threads/thread.h b/libc/src/__support/threads/thread.h
index 9971dc5f193a07..3f71e101c3714f 100644
--- a/libc/src/__support/threads/thread.h
+++ b/libc/src/__support/threads/thread.h
@@ -71,11 +71,12 @@ struct Thread {
int run(ThreadRunnerPosix *func, void *arg, void *stack = nullptr,
size_t stacksize = DEFAULT_STACKSIZE,
size_t guardsize = DEFAULT_GUARDSIZE,
- bool detached = DEFAULT_DETACHED) {
+ bool detached = DEFAULT_DETACHED,
+ cpp::optional<SchedParameters> sched_params = cpp::nullopt) {
ThreadRunner runner;
runner.posix_runner = func;
return run(ThreadStyle::POSIX, runner, arg, stack, stacksize, guardsize,
- detached);
+ detached, sched_params);
}
int run(ThreadRunnerStdc *func, void *arg, void *stack = nullptr,
@@ -85,7 +86,7 @@ struct Thread {
ThreadRunner runner;
runner.stdc_runner = func;
return run(ThreadStyle::STDC, runner, arg, stack, stacksize, guardsize,
- detached);
+ detached, cpp::nullopt);
}
int join(int *val) {
@@ -112,7 +113,8 @@ struct Thread {
// Return 0 on success or an error value on failure.
int run(ThreadStyle style, ThreadRunner runner, void *arg, void *stack,
- size_t stacksize, size_t guardsize, bool detached);
+ size_t stacksize, size_t guardsize, bool detached,
+ cpp::optional<SchedParameters> sched_params = cpp::nullopt);
// Return 0 on success or an error value on failure.
int join(ThreadReturnValue &retval);
diff --git a/libc/src/pthread/pthread_create.cpp b/libc/src/pthread/pthread_create.cpp
index 247a80c8c2c4a1..4308134d91aff0 100644
--- a/libc/src/pthread/pthread_create.cpp
+++ b/libc/src/pthread/pthread_create.cpp
@@ -39,6 +39,18 @@ LLVM_LIBC_FUNCTION(int, pthread_create,
size_t guardsize = attr->__guardsize;
int detachstate = attr->__detachstate;
+ cpp::optional<SchedParameters> sched_params;
+ switch (attr->__inheritsched) {
+ case PTHREAD_INHERIT_SCHED:
+ break;
+ case PTHREAD_EXPLICIT_SCHED:
+ sched_params = cpp::optional<SchedParameters>(
+ SchedParameters{attr->__schedpolicy, attr->__schedparam});
+ break;
+ default:
+ return EINVAL;
+ }
+
if (stacksize && stacksize < PTHREAD_STACK_MIN)
return EINVAL;
@@ -53,8 +65,9 @@ LLVM_LIBC_FUNCTION(int, pthread_create,
// universal, not sure a pthread requirement).
auto *thread = reinterpret_cast<LIBC_NAMESPACE::Thread *>(th);
- int result = thread->run(func, arg, stack, stacksize, guardsize,
- detachstate == PTHREAD_CREATE_DETACHED);
+ int result =
+ thread->run(func, arg, stack, stacksize, guardsize,
+ detachstate == PTHREAD_CREATE_DETACHED, sched_params);
if (result != 0 && result != EPERM && result != EINVAL)
return EAGAIN;
return result;
diff --git a/libc/test/integration/src/pthread/CMakeLists.txt b/libc/test/integration/src/pthread/CMakeLists.txt
index 66721f8a673584..62c4dc907d3076 100644
--- a/libc/test/integration/src/pthread/CMakeLists.txt
+++ b/libc/test/integration/src/pthread/CMakeLists.txt
@@ -294,7 +294,10 @@ add_integration_test(
SRCS
pthread_create_test.cpp
DEPENDS
+ libc.hdr.pthread_macros
+ libc.hdr.sched_macros
libc.hdr.sys_mman_macros
+ libc.hdr.types.struct_sched_param
libc.include.pthread
libc.src.errno.errno
libc.src.pthread.pthread_create
@@ -305,10 +308,14 @@ add_integration_test(
libc.src.pthread.pthread_attr_getstacksize
libc.src.pthread.pthread_attr_setdetachstate
libc.src.pthread.pthread_attr_setguardsize
+ libc.src.pthread.pthread_attr_setinheritsched
+ libc.src.pthread.pthread_attr_setschedparam
+ libc.src.pthread.pthread_attr_setschedpolicy
libc.src.pthread.pthread_attr_setstack
libc.src.pthread.pthread_attr_setstacksize
libc.src.pthread.pthread_attr_init
libc.src.pthread.pthread_attr_destroy
+ libc.src.pthread.pthread_getschedparam
libc.src.pthread.pthread_getunique_np
libc.src.pthread.pthread_self
libc.src.sys.mman.mmap
@@ -318,6 +325,7 @@ add_integration_test(
libc.src.__support.CPP.atomic
libc.src.__support.CPP.array
libc.src.__support.CPP.new
+ libc.src.__support.CPP.optional
)
add_integration_test(
diff --git a/libc/test/integration/src/pthread/pthread_create_test.cpp b/libc/test/integration/src/pthread/pthread_create_test.cpp
index dd8d3ef1c8e353..5b43c74314eb7a 100644
--- a/libc/test/integration/src/pthread/pthread_create_test.cpp
+++ b/libc/test/integration/src/pthread/pthread_create_test.cpp
@@ -6,7 +6,10 @@
//
//===----------------------------------------------------------------------===//
+#include "hdr/pthread_macros.h"
+#include "hdr/sched_macros.h"
#include "hdr/sys_mman_macros.h"
+#include "hdr/types/struct_sched_param.h"
#include "src/pthread/pthread_attr_destroy.h"
#include "src/pthread/pthread_attr_getdetachstate.h"
#include "src/pthread/pthread_attr_getguardsize.h"
@@ -15,9 +18,13 @@
#include "src/pthread/pthread_attr_init.h"
#include "src/pthread/pthread_attr_setdetachstate.h"
#include "src/pthread/pthread_attr_setguardsize.h"
+#include "src/pthread/pthread_attr_setinheritsched.h"
+#include "src/pthread/pthread_attr_setschedparam.h"
+#include "src/pthread/pthread_attr_setschedpolicy.h"
#include "src/pthread/pthread_attr_setstack.h"
#include "src/pthread/pthread_attr_setstacksize.h"
#include "src/pthread/pthread_create.h"
+#include "src/pthread/pthread_getschedparam.h"
#include "src/pthread/pthread_getunique_np.h"
#include "src/pthread/pthread_join.h"
#include "src/pthread/pthread_self.h"
@@ -334,11 +341,199 @@ static void run_failure_tests() {
ASSERT_ERRNO_SUCCESS();
attr.__detachstate = -1;
create_and_check_failure_thread(&attr);
+
+ // Inheritsched is unknown.
+ ASSERT_EQ(LIBC_NAMESPACE::pthread_attr_init(&attr), 0);
+ ASSERT_ERRNO_SUCCESS();
+ attr.__inheritsched = -1;
+ create_and_check_failure_thread(&attr);
+
+ // Schedpolicy is invalid when explicit sched is requested.
+ ASSERT_EQ(LIBC_NAMESPACE::pthread_attr_init(&attr), 0);
+ ASSERT_ERRNO_SUCCESS();
+ ASSERT_EQ(LIBC_NAMESPACE::pthread_attr_setinheritsched(
+ &attr, PTHREAD_EXPLICIT_SCHED),
+ 0);
+ ASSERT_ERRNO_SUCCESS();
+ attr.__schedpolicy = -1;
+ create_and_check_failure_thread(&attr);
+
+ // Sched priority is invalid for policy.
+ ASSERT_EQ(LIBC_NAMESPACE::pthread_attr_init(&attr), 0);
+ ASSERT_ERRNO_SUCCESS();
+ ASSERT_EQ(LIBC_NAMESPACE::pthread_attr_setinheritsched(
+ &attr, PTHREAD_EXPLICIT_SCHED),
+ 0);
+ ASSERT_ERRNO_SUCCESS();
+ ASSERT_EQ(LIBC_NAMESPACE::pthread_attr_setschedpolicy(&attr, SCHED_OTHER), 0);
+ ASSERT_ERRNO_SUCCESS();
+ sched_param bad_param;
+ bad_param.sched_priority = 1;
+ ASSERT_EQ(LIBC_NAMESPACE::pthread_attr_setschedparam(&attr, &bad_param), 0);
+ ASSERT_ERRNO_SUCCESS();
+ create_and_check_failure_thread(&attr);
+}
+
+struct SchedThreadArgs {
+ LIBC_NAMESPACE::cpp::Atomic<bool> executed = false;
+ int policy = 0;
+ int priority = 0;
+};
+
+static void *sched_runner(void *arg) {
+ auto *args = reinterpret_cast<SchedThreadArgs *>(arg);
+ sched_param param;
+ int policy;
+ ASSERT_EQ(LIBC_NAMESPACE::pthread_getschedparam(
+ LIBC_NAMESPACE::pthread_self(), &policy, ¶m),
+ 0);
+ ASSERT_ERRNO_SUCCESS();
+ args->policy = policy;
+ args->priority = param.sched_priority;
+ args->executed.store(true);
+ return nullptr;
+}
+
+static void test_sched_inherit() {
+ pthread_t self = LIBC_NAMESPACE::pthread_self();
+ int parent_policy = 0;
+ sched_param parent_param;
+ ASSERT_EQ(LIBC_NAMESPACE::pthread_getschedparam(self, &parent_policy,
+ &parent_param),
+ 0);
+ ASSERT_ERRNO_SUCCESS();
+
+ // 1. With default attr (nullptr)
+ {
+ SchedThreadArgs args;
+ pthread_t tid;
+ ASSERT_EQ(
+ LIBC_NAMESPACE::pthread_create(&tid, nullptr, sched_runner, &args), 0);
+ ASSERT_ERRNO_SUCCESS();
+ ASSERT_EQ(LIBC_NAMESPACE::pthread_join(tid, nullptr), 0);
+ ASSERT_ERRNO_SUCCESS();
+ ASSERT_TRUE(args.executed.load());
+ ASSERT_EQ(args.policy, parent_policy);
+ ASSERT_EQ(args.priority, parent_param.sched_priority);
+ }
+
+ // 2. Explicit PTHREAD_INHERIT_SCHED in attr, while setting schedpolicy to
+ // SCHED_BATCH
+ {
+ pthread_attr_t attr;
+ ASSERT_EQ(LIBC_NAMESPACE::pthread_attr_init(&attr), 0);
+ ASSERT_ERRNO_SUCCESS();
+ ASSERT_EQ(LIBC_NAMESPACE::pthread_attr_setinheritsched(
+ &attr, PTHREAD_INHERIT_SCHED),
+ 0);
+ ASSERT_ERRNO_SUCCESS();
+ // Even if schedpolicy is modified in attr, PTHREAD_INHERIT_SCHED means it
+ // must be ignored.
+ int dummy_policy =
+ (parent_policy == SCHED_OTHER) ? SCHED_BATCH : SCHED_OTHER;
+ ASSERT_EQ(LIBC_NAMESPACE::pthread_attr_setschedpolicy(&attr, dummy_policy),
+ 0);
+ ASSERT_ERRNO_SUCCESS();
+
+ SchedThreadArgs args;
+ pthread_t tid;
+ ASSERT_EQ(LIBC_NAMESPACE::pthread_create(&tid, &attr, sched_runner, &args),
+ 0);
+ ASSERT_ERRNO_SUCCESS();
+ ASSERT_EQ(LIBC_NAMESPACE::pthread_join(tid, nullptr), 0);
+ ASSERT_ERRNO_SUCCESS();
+ ASSERT_TRUE(args.executed.load());
+ ASSERT_EQ(args.policy, parent_policy);
+ ASSERT_EQ(args.priority, parent_param.sched_priority);
+
+ ASSERT_EQ(LIBC_NAMESPACE::pthread_attr_destroy(&attr), 0);
+ ASSERT_ERRNO_SUCCESS();
+ }
+}
+
+static void verify_sched_policy(pthread_attr_t &attr, int expected_policy,
+ int expected_priority) {
+ ASSERT_EQ(LIBC_NAMESPACE::pthread_attr_setschedpolicy(&attr, expected_policy),
+ 0);
+ ASSERT_ERRNO_SUCCESS();
+ sched_param param;
+ param.sched_priority = expected_priority;
+ ASSERT_EQ(LIBC_NAMESPACE::pthread_attr_setschedparam(&attr, ¶m), 0);
+ ASSERT_ERRNO_SUCCESS();
+
+ SchedThreadArgs args;
+ pthread_t tid;
+ ASSERT_EQ(LIBC_NAMESPACE::pthread_create(&tid, &attr, sched_runner, &args),
+ 0);
+ ASSERT_ERRNO_SUCCESS();
+
+ ASSERT_EQ(LIBC_NAMESPACE::pthread_join(tid, nullptr), 0);
+ ASSERT_ERRNO_SUCCESS();
+ ASSERT_TRUE(args.executed.load());
+ ASSERT_EQ(args.policy, expected_policy);
+ ASSERT_EQ(args.priority, expected_priority);
+}
+
+static void test_sched_explicit_success() {
+ pthread_attr_t attr;
+ ASSERT_EQ(LIBC_NAMESPACE::pthread_attr_init(&attr), 0);
+ ASSERT_ERRNO_SUCCESS();
+ ASSERT_EQ(LIBC_NAMESPACE::pthread_attr_setinheritsched(
+ &attr, PTHREAD_EXPLICIT_SCHED),
+ 0);
+ ASSERT_ERRNO_SUCCESS();
+
+ verify_sched_policy(attr, SCHED_OTHER, 0);
+ verify_sched_policy(attr, SCHED_BATCH, 0);
+
+ ASSERT_EQ(LIBC_NAMESPACE::pthread_attr_destroy(&attr), 0);
+ ASSERT_ERRNO_SUCCESS();
+}
+
+static void test_sched_realtime_permission() {
+ pthread_attr_t attr;
+ ASSERT_EQ(LIBC_NAMESPACE::pthread_attr_init(&attr), 0);
+ ASSERT_ERRNO_SUCCESS();
+ ASSERT_EQ(LIBC_NAMESPACE::pthread_attr_setinheritsched(
+ &attr, PTHREAD_EXPLICIT_SCHED),
+ 0);
+ ASSERT_ERRNO_SUCCESS();
+ ASSERT_EQ(LIBC_NAMESPACE::pthread_attr_setschedpolicy(&attr, SCHED_FIFO), 0);
+ ASSERT_ERRNO_SUCCESS();
+ sched_param param;
+ param.sched_priority = 1;
+ ASSERT_EQ(LIBC_NAMESPACE::pthread_attr_setschedparam(&attr, ¶m), 0);
+ ASSERT_ERRNO_SUCCESS();
+
+ SchedThreadArgs args;
+ pthread_t tid;
+ int res = LIBC_NAMESPACE::pthread_create(&tid, &attr, sched_runner, &args);
+ // Setting realtime scheduling policy SCHED_FIFO requires CAP_SYS_NICE or
+ // sufficient RLIMIT_RTPRIO. In unprivileged environments this will fail with
+ // EPERM, while in privileged environments it will succeed with 0.
+ ASSERT_TRUE(res == EPERM || res == 0);
+ ASSERT_ERRNO_SUCCESS();
+
+ if (res == 0) {
+ ASSERT_EQ(LIBC_NAMESPACE::pthread_join(tid, nullptr), 0);
+ ASSERT_ERRNO_SUCCESS();
+ ASSERT_TRUE(args.executed.load());
+ ASSERT_EQ(args.policy, SCHED_FIFO);
+ ASSERT_EQ(args.priority, 1);
+ } else {
+ ASSERT_FALSE(args.executed.load());
+ }
+
+ ASSERT_EQ(LIBC_NAMESPACE::pthread_attr_destroy(&attr), 0);
+ ASSERT_ERRNO_SUCCESS();
}
TEST_MAIN() {
errno = 0;
run_success_tests();
run_failure_tests();
+ test_sched_inherit();
+ test_sched_explicit_success();
+ test_sched_realtime_permission();
return 0;
}
More information about the libc-commits
mailing list