// SPDX-License-Identifier: GPL-2.0-or-later /* * Copyright (C) 2025 Sebastian Andrzej Siewior */ #define _GNU_SOURCE #include #include #include #include #include #include #include #include #include "kselftest_harness.h" #define MAX_THREADS 64 static pthread_barrier_t barrier_main; static pthread_mutex_t global_lock; static pthread_t threads[MAX_THREADS]; static int counter; #ifndef PR_FUTEX_HASH #define PR_FUTEX_HASH 78 # define PR_FUTEX_HASH_SET_SLOTS 1 # define PR_FUTEX_HASH_GET_SLOTS 2 #endif static int futex_hash_slots_set(unsigned int slots) { return prctl(PR_FUTEX_HASH, PR_FUTEX_HASH_SET_SLOTS, slots, 0); } static int futex_hash_slots_get(void) { return prctl(PR_FUTEX_HASH, PR_FUTEX_HASH_GET_SLOTS); } static void futex_hash_slots_set_verify(struct __test_metadata *_metadata, int slots) { int ret; ret = futex_hash_slots_set(slots); ASSERT_EQ(ret, 0) TH_LOG("Failed to set slots to %d: %s", slots, strerror(errno)); ret = futex_hash_slots_get(); ASSERT_EQ(ret, slots) { TH_LOG("Set %d slots but PR_FUTEX_HASH_GET_SLOTS returns: %d, %s", slots, ret, strerror(errno)); } } static void futex_hash_slots_set_must_fail(struct __test_metadata *_metadata, int slots) { int ret; ret = futex_hash_slots_set(slots); EXPECT_LT(ret, 0) TH_LOG("futex_hash_slots_set(%d) should fail but succeeded", slots); } static void *thread_return_fn(void *arg) { return NULL; } static void *thread_lock_fn(void *arg) { pthread_barrier_wait(&barrier_main); pthread_mutex_lock(&global_lock); counter++; usleep(20); pthread_mutex_unlock(&global_lock); return NULL; } static void create_max_threads(struct __test_metadata *_metadata, void *(*thread_fn)(void *)) { int i, ret; for (i = 0; i < MAX_THREADS; i++) { ret = pthread_create(&threads[i], NULL, thread_fn, NULL); ASSERT_EQ(ret, 0) TH_LOG("pthread_create failed: %s", strerror(errno)); } } static void join_max_threads(struct __test_metadata *_metadata) { int i, ret; for (i = 0; i < MAX_THREADS; i++) { ret = pthread_join(threads[i], NULL); ASSERT_EQ(ret, 0) TH_LOG("pthread_join failed for thread %d: %s", i, strerror(errno)); } } #define SEC_IN_NSEC 1000000000 #define MSEC_IN_NSEC 1000000 static void futex_dummy_op(struct __test_metadata *_metadata) { pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; struct timespec timeout; int ret; pthread_mutex_lock(&lock); clock_gettime(CLOCK_REALTIME, &timeout); timeout.tv_nsec += 100 * MSEC_IN_NSEC; if (timeout.tv_nsec >= SEC_IN_NSEC) { timeout.tv_nsec -= SEC_IN_NSEC; timeout.tv_sec++; } ret = pthread_mutex_timedlock(&lock, &timeout); ASSERT_NE(ret, 0) TH_LOG("Successfully locked an already locked mutex"); ASSERT_EQ(ret, ETIMEDOUT) TH_LOG("pthread_mutex_timedlock() did not timeout: %d", ret); } static const char *test_msg_auto_create = "Automatic hash bucket init on thread creation.\n"; static const char *test_msg_auto_inc = "Automatic increase with more than 16 CPUs\n"; TEST(priv_hash) { int futex_slots1, futex_slotsn, online_cpus; pthread_mutexattr_t mutex_attr_pi; int ret, retry = 20; ret = pthread_mutexattr_init(&mutex_attr_pi); ret |= pthread_mutexattr_setprotocol(&mutex_attr_pi, PTHREAD_PRIO_INHERIT); ret |= pthread_mutex_init(&global_lock, &mutex_attr_pi); ASSERT_EQ(ret, 0) TH_LOG("Failed to initialize pthread mutex"); /* First thread, expect to be 0, not yet initialized */ ret = futex_hash_slots_get(); if (ret < 0 && errno == EINVAL) SKIP(return, "PR_FUTEX_HASH not supported by kernel"); ASSERT_EQ(ret, 0) TH_LOG("futex_hash_slots_get() failed: %d, %s", ret, strerror(errno)); ret = pthread_create(&threads[0], NULL, thread_return_fn, NULL); ASSERT_EQ(ret, 0) TH_LOG("pthread_create() failed: %d, %s", ret, strerror(errno)); ret = pthread_join(threads[0], NULL); ASSERT_EQ(ret, 0) TH_LOG("pthread_join() failed: %d, %s", ret, strerror(errno)); /* First thread, has to initialize private hash */ futex_slots1 = futex_hash_slots_get(); EXPECT_GT(futex_slots1, 0) TH_LOG("Current hash buckets: %d. %s", futex_slots1, test_msg_auto_create); online_cpus = sysconf(_SC_NPROCESSORS_ONLN); ret = pthread_barrier_init(&barrier_main, NULL, MAX_THREADS + 1); ASSERT_EQ(ret, 0) TH_LOG("pthread_barrier_init failed: %s", strerror(errno)); ret = pthread_mutex_lock(&global_lock); ASSERT_EQ(ret, 0) TH_LOG("pthread_mutex_lock failed: %s", strerror(errno)); counter = 0; create_max_threads(_metadata, thread_lock_fn); pthread_barrier_wait(&barrier_main); /* * The current default size of hash buckets is 16. The auto increase * works only if more than 16 CPUs are available. */ TH_LOG("Online CPUs: %d", online_cpus); if (online_cpus > 16) { retry_getslots: futex_slotsn = futex_hash_slots_get(); if (futex_slotsn < 0 || futex_slots1 == futex_slotsn) { retry--; /* * Auto scaling on thread creation can be slightly delayed * because it waits for a RCU grace period twice. The new * private hash is assigned upon the first futex operation * after grace period. * To cover all this for testing purposes the function * below will acquire a lock and acquire it again with a * 100ms timeout which must timeout. This ensures we * sleep for 100ms and issue a futex operation. */ if (retry > 0) { futex_dummy_op(_metadata); goto retry_getslots; } EXPECT_NE(futex_slots1, futex_slotsn) { TH_LOG("Expected increase of hash buckets but got: %d -> %d. %s", futex_slots1, futex_slotsn, test_msg_auto_inc); } } } else { SKIP(return, "Automatic increase with more than 16 CPUs (only %d online)", online_cpus); } ret = pthread_mutex_unlock(&global_lock); /* Once the user changes it, it has to be what is set */ futex_hash_slots_set_verify(_metadata, 2); futex_hash_slots_set_verify(_metadata, 4); futex_hash_slots_set_verify(_metadata, 8); futex_hash_slots_set_verify(_metadata, 32); futex_hash_slots_set_verify(_metadata, 16); ret = futex_hash_slots_set(15); EXPECT_LT(ret, 0) TH_LOG("Use 15 slots should fail but succeeded"); futex_hash_slots_set_verify(_metadata, 2); join_max_threads(_metadata); EXPECT_EQ(counter, MAX_THREADS) TH_LOG("Created and waited for %d of %d threads", counter, MAX_THREADS); counter = 0; /* Once the user set something, auto resize must be disabled */ ret = pthread_barrier_init(&barrier_main, NULL, MAX_THREADS); ASSERT_EQ(ret, 0) TH_LOG("pthread_barrier_init failed: %s", strerror(errno)); create_max_threads(_metadata, thread_lock_fn); join_max_threads(_metadata); ret = futex_hash_slots_get(); EXPECT_EQ(ret, 2) TH_LOG("No more auto-resize after manual setting, got %d", ret); futex_hash_slots_set_must_fail(_metadata, 1 << 29); futex_hash_slots_set_verify(_metadata, 4); /* * Once the global hash has been requested, then this requested can not * be undone. */ ret = futex_hash_slots_set(0); ASSERT_EQ(ret, 0) TH_LOG("Global hash request failed: %s", strerror(errno)); futex_hash_slots_set_must_fail(_metadata, 4); futex_hash_slots_set_must_fail(_metadata, 8); futex_hash_slots_set_must_fail(_metadata, 8); futex_hash_slots_set_must_fail(_metadata, 0); futex_hash_slots_set_must_fail(_metadata, 6); ret = pthread_barrier_init(&barrier_main, NULL, MAX_THREADS); ASSERT_EQ(ret, 0) TH_LOG("pthread_barrier_init failed: %s", strerror(errno)); create_max_threads(_metadata, thread_lock_fn); join_max_threads(_metadata); ret = futex_hash_slots_get(); EXPECT_EQ(ret, 0) TH_LOG("Continue to use global hash failed"); } TEST_HARNESS_MAIN