| // Copyright 2016, 2018 The Fuchsia Authors |
| // Copyright (c) 2008-2015 Travis Geiselbrecht |
| // |
| // Use of this source code is governed by a MIT-style |
| // license that can be found in the LICENSE file or at |
| // https://opensource.org/licenses/MIT |
| |
| #include <assert.h> |
| #include <debug.h> |
| #include <lib/unittest/unittest.h> |
| #include <zircon/errors.h> |
| #include <zircon/types.h> |
| |
| #include <kernel/atomic.h> |
| #include <kernel/cpu.h> |
| #include <kernel/event.h> |
| #include <kernel/mp.h> |
| #include <kernel/mutex.h> |
| #include <kernel/thread.h> |
| #include <ktl/popcount.h> |
| #include <ktl/unique_ptr.h> |
| |
| #include "zircon/time.h" |
| |
| namespace { |
| |
| // Wait for condition |cond| to become true, polling with slow exponential backoff |
| // to avoid pegging the CPU. |
| template <typename F> |
| void wait_for_cond(F cond) { |
| zx_duration_t wait_duration = ZX_USEC(1); |
| while (!cond()) { |
| thread_sleep_relative(wait_duration); |
| // Increase wait_duration time by ~10%. |
| wait_duration += (wait_duration / 10u + 1u); |
| } |
| } |
| |
| // Create and manage a spinning thread. |
| class WorkerThread { |
| public: |
| explicit WorkerThread(const char* name) { |
| thread_ = thread_create(name, &WorkerThread::WorkerBody, this, LOW_PRIORITY); |
| ASSERT(thread_ != nullptr); |
| } |
| |
| ~WorkerThread() { Join(); } |
| |
| void Start() { thread_resume(thread_); } |
| |
| void Join() { |
| if (thread_ != nullptr) { |
| atomic_store(&worker_should_stop_, 1); |
| int unused_retcode; |
| zx_status_t result = thread_join(thread_, &unused_retcode, ZX_TIME_INFINITE); |
| ASSERT(result == ZX_OK); |
| thread_ = nullptr; |
| } |
| } |
| |
| void WaitForWorkerProgress() { |
| int start_iterations = worker_iterations(); |
| wait_for_cond([start_iterations, this]() { return start_iterations != worker_iterations(); }); |
| } |
| |
| thread_t* thread() const { return thread_; } |
| |
| int worker_iterations() { return atomic_load(&worker_iterations_); } |
| |
| DISALLOW_COPY_ASSIGN_AND_MOVE(WorkerThread); |
| |
| private: |
| static int WorkerBody(void* arg) { |
| auto* self = reinterpret_cast<WorkerThread*>(arg); |
| while (atomic_load(&self->worker_should_stop_) == 0) { |
| atomic_add(&self->worker_iterations_, 1); |
| } |
| return 0; |
| } |
| |
| volatile int worker_iterations_ = 0; |
| volatile int worker_should_stop_ = 0; |
| thread_t* thread_; |
| }; |
| |
| struct YieldData { |
| volatile int* done; |
| volatile int* started; |
| }; |
| |
| // This thread will immediately yield, resulting in not fully using a given |
| // quantum. |
| static int yielding_tester(void* arg) { |
| volatile int* done = reinterpret_cast<YieldData*>(arg)->done; |
| volatile int* started = reinterpret_cast<YieldData*>(arg)->started; |
| atomic_add(started, 1); |
| for (;;) { |
| thread_yield(); |
| arch_spinloop_pause(); |
| if (atomic_load(done) == 2) |
| break; |
| } |
| return 0; |
| } |
| |
| static int end_yielders_tester(void* arg) { |
| volatile int* done = reinterpret_cast<YieldData*>(arg)->done; |
| volatile int* started = reinterpret_cast<YieldData*>(arg)->started; |
| atomic_add(started, 1); |
| for (;;) { |
| if (atomic_load(done) == 1) { |
| atomic_add(done, 1); |
| break; |
| } |
| arch_spinloop_pause(); |
| } |
| return 0; |
| } |
| |
| // In https://crbug.com/959245 and ZX-4410 a bunch of userspace yield-spinlocks |
| // caused a test hang, when there was num_cpus of them, and the yield deboost |
| // (for not expiring the quantum) ended up keeping them at higher priority than |
| // thread doing actual work. |
| static bool yield_deboost_test() { |
| BEGIN_TEST; |
| |
| volatile int done = 0; |
| volatile int started = 0; |
| YieldData data = {&done, &started}; |
| |
| const int num_yield_threads = arch_max_num_cpus() * 2; |
| const int num_total_threads = num_yield_threads + 1; |
| fbl::AllocChecker ac; |
| auto threads = ktl::unique_ptr<thread_t*[]>(new (&ac) thread_t*[num_total_threads]); |
| ASSERT_TRUE(ac.check()); |
| |
| // Start a pile of threads that all spin-yield. |
| for (int i = 0; i < num_yield_threads; ++i) { |
| threads[i] = thread_create("yielder", &yielding_tester, reinterpret_cast<void*>(&data), |
| DEFAULT_PRIORITY); |
| ASSERT_NONNULL(threads[i], "thread_create"); |
| thread_resume(threads[i]); |
| } |
| |
| // Start the potentially-starved thread. |
| int starve = num_yield_threads; |
| threads[starve] = thread_create("ender", &end_yielders_tester, reinterpret_cast<void*>(&data), |
| DEFAULT_PRIORITY); |
| ASSERT_NONNULL(threads[starve], "thread_create"); |
| thread_resume(threads[starve]); |
| |
| while (atomic_load(&started) < num_total_threads) { |
| // Wait until all the threads have started. |
| } |
| |
| // This thread gets a positive boost when waking from sleep, so it should be |
| // able to set done to 1. If the yield bug isn't happening, the non-yielding |
| // thread will in turn set it to 2, which tells the yielders to exit. When |
| // yield()ing is keeping the yielding threads at a higher priority than the |
| // end_yielders, done will never move to 2, and so the test will hang when |
| // trying to join the yield threads below. |
| thread_sleep_relative(ZX_MSEC(100)); |
| atomic_add(&done, 1); |
| |
| for (int i = 0; i < num_total_threads; ++i) { |
| thread_join(threads[i], nullptr, ZX_TIME_INFINITE); |
| } |
| |
| END_TEST; |
| } |
| |
| bool set_affinity_self_test() { |
| BEGIN_TEST; |
| |
| // Our worker thread will attempt to schedule itself onto each core, one at |
| // a time, and ensure it landed in the right location. |
| cpu_mask_t online_cpus = mp_get_online_mask(); |
| ASSERT_NE(online_cpus, 0u, "Expected at least one CPU to be online."); |
| auto worker_body = +[](void* arg) -> int { |
| cpu_mask_t& online_cpus = *reinterpret_cast<cpu_mask_t*>(arg); |
| thread_t* const self = get_current_thread(); |
| |
| for (cpu_num_t c = 0u; c <= highest_cpu_set(online_cpus); c++) { |
| // Skip offline CPUs. |
| if ((cpu_num_to_mask(c) & online_cpus) == 0) { |
| continue; |
| } |
| |
| // Set affinity to the given core. |
| thread_set_cpu_affinity(self, cpu_num_to_mask(c)); |
| |
| // Ensure we are on the correct CPU. |
| const cpu_num_t current_cpu = arch_curr_cpu_num(); |
| if (current_cpu != c) { |
| UNITTEST_FAIL_TRACEF("Expected to be running on CPU %u, but actually running on %u.", c, |
| current_cpu); |
| return ZX_ERR_INTERNAL; |
| } |
| } |
| |
| return ZX_OK; |
| }; |
| thread_t* worker = |
| thread_create("set_affinity_self_test_worker", worker_body, &online_cpus, DEFAULT_PRIORITY); |
| ASSERT_NONNULL(worker, "thread_create failed."); |
| thread_resume(worker); |
| |
| // Wait for the worker thread to test itself. |
| int worker_retcode; |
| ASSERT_EQ(thread_join(worker, &worker_retcode, ZX_TIME_INFINITE), ZX_OK, |
| "Failed to join thread."); |
| EXPECT_EQ(worker_retcode, ZX_OK, "Worker thread failed."); |
| |
| END_TEST; |
| } |
| |
| bool set_affinity_other_test() { |
| BEGIN_TEST; |
| |
| struct WorkerState { |
| volatile int current_cpu = -1; |
| volatile int should_stop = 0; |
| } state; |
| |
| // Start a worker, which reports the CPU it is running on. |
| auto worker_body = [](void* arg) -> int { |
| WorkerState& state = *reinterpret_cast<WorkerState*>(arg); |
| while (atomic_load(&state.should_stop) != 1) { |
| atomic_store(&state.current_cpu, arch_curr_cpu_num()); |
| } |
| return 0; |
| }; |
| thread_t* worker = |
| thread_create("set_affinity_other_test_worker", worker_body, &state, LOW_PRIORITY); |
| thread_resume(worker); |
| |
| // Migrate the worker task amongst different threads. |
| const cpu_mask_t online_cpus = mp_get_online_mask(); |
| ASSERT_NE(online_cpus, 0u, "Expected at least one CPU to be online."); |
| for (cpu_num_t c = 0u; c <= highest_cpu_set(online_cpus); c++) { |
| // Skip offline CPUs. |
| if ((cpu_num_to_mask(c) & online_cpus) == 0) { |
| continue; |
| } |
| |
| // Set affinity to the given core. |
| thread_set_cpu_affinity(worker, cpu_num_to_mask(c)); |
| |
| // Wait for it to land on the correct CPU. |
| wait_for_cond( |
| [c, &state]() { return static_cast<cpu_num_t>(atomic_load(&state.current_cpu)) == c; }); |
| } |
| |
| // Done. |
| atomic_store(&state.should_stop, 1); |
| int worker_retcode; |
| ASSERT_EQ(thread_join(worker, &worker_retcode, ZX_TIME_INFINITE), ZX_OK, |
| "Failed to join thread."); |
| |
| END_TEST; |
| } |
| |
| bool thread_last_cpu_new_thread() { |
| BEGIN_TEST; |
| |
| // Create a worker, but don't start it. |
| WorkerThread worker("last_cpu_new_thread"); |
| |
| // Ensure we get INVALID_CPU as last cpu. |
| ASSERT_EQ(thread_last_cpu(worker.thread()), INVALID_CPU, "Last CPU on unstarted thread invalid."); |
| |
| // Clean up the thread. |
| worker.Start(); |
| worker.Join(); |
| |
| END_TEST; |
| } |
| |
| bool thread_last_cpu_running_thread() { |
| BEGIN_TEST; |
| |
| WorkerThread worker("last_cpu_running_thread"); |
| worker.Start(); |
| |
| // Migrate the worker task across different CPUs. |
| const cpu_mask_t online_cpus = mp_get_online_mask(); |
| ASSERT_NE(online_cpus, 0u, "Expected at least one CPU to be online."); |
| for (cpu_num_t c = 0u; c <= highest_cpu_set(online_cpus); c++) { |
| // Skip offline CPUs. |
| if ((cpu_num_to_mask(c) & online_cpus) == 0) { |
| continue; |
| } |
| |
| // Set affinity to the given core. |
| thread_set_cpu_affinity(worker.thread(), cpu_num_to_mask(c)); |
| |
| // Ensure it is reported at the correct CPU. |
| wait_for_cond([c, &worker]() { return thread_last_cpu(worker.thread()) == c; }); |
| } |
| |
| END_TEST; |
| } |
| |
| bool thread_empty_soft_affinity_mask() { |
| BEGIN_TEST; |
| |
| WorkerThread worker("empty_soft_affinity_mask"); |
| worker.Start(); |
| |
| // Wait for the thread to start running. |
| worker.WaitForWorkerProgress(); |
| |
| // Set affinity to an invalid (empty) mask. |
| thread_set_soft_cpu_affinity(worker.thread(), 0); |
| |
| // Ensure that the thread is still running. |
| worker.WaitForWorkerProgress(); |
| |
| END_TEST; |
| } |
| |
| bool thread_conflicting_soft_and_hard_affinity() { |
| BEGIN_TEST; |
| |
| // Find two different CPUs to run our tests on. |
| cpu_mask_t online = mp_get_online_mask(); |
| ASSERT_TRUE(online != 0, "No CPUs online."); |
| cpu_num_t a = highest_cpu_set(online); |
| cpu_num_t b = lowest_cpu_set(online); |
| if (a == b) { |
| // Skip test on single CPU machines. |
| unittest_printf("Only 1 CPU active in this machine. Skipping test.\n"); |
| return true; |
| } |
| |
| WorkerThread worker("conflicting_soft_and_hard_affinity"); |
| worker.Start(); |
| |
| // Set soft affinity to CPU A, wait for the thread to start running there. |
| thread_set_soft_cpu_affinity(worker.thread(), cpu_num_to_mask(a)); |
| wait_for_cond([&worker, a]() { return thread_last_cpu(worker.thread()) == a; }); |
| |
| // Set hard affinity to CPU B, ensure the thread migrates there. |
| thread_set_cpu_affinity(worker.thread(), cpu_num_to_mask(b)); |
| wait_for_cond([&worker, b]() { return thread_last_cpu(worker.thread()) == b; }); |
| |
| // Remove the hard affinity. Make sure the thread migrates back to CPU A. |
| thread_set_cpu_affinity(worker.thread(), CPU_MASK_ALL); |
| wait_for_cond([&worker, a]() { return thread_last_cpu(worker.thread()) == a; }); |
| |
| END_TEST; |
| } |
| |
| } // namespace |
| |
| UNITTEST_START_TESTCASE(thread_tests) |
| UNITTEST("yield_deboost_test", yield_deboost_test) |
| UNITTEST("set_affinity_self_test", set_affinity_self_test) |
| UNITTEST("set_affinity_other_test", set_affinity_other_test) |
| UNITTEST("thread_last_cpu_new_thread", thread_last_cpu_new_thread) |
| UNITTEST("thread_last_cpu_running_thread", thread_last_cpu_running_thread) |
| UNITTEST("thread_empty_soft_affinity_mask", thread_empty_soft_affinity_mask) |
| UNITTEST("thread_conflicting_soft_and_hard_affinity", thread_conflicting_soft_and_hard_affinity) |
| UNITTEST_END_TESTCASE(thread_tests, "thread", "thread tests") |