| // Copyright Microsoft and CHERIoT Contributors. |
| // SPDX-License-Identifier: MIT |
| |
| #define CHERIOT_NO_AMBIENT_MALLOC |
| #define CHERIOT_NO_NEW_DELETE |
| #include "../switcher/tstack.h" |
| #include "multiwait.h" |
| #include "plic.h" |
| #include "thread.h" |
| #include "timer.h" |
| #include <cdefs.h> |
| #include <cheri.hh> |
| #include <compartment.h> |
| #include <futex.h> |
| #include <interrupt.h> |
| #include <locks.hh> |
| #include <new> |
| #include <priv/riscv.h> |
| #include <riscvreg.h> |
| #include <simulator.h> |
| #include <stdint.h> |
| #include <stdlib.h> |
| #include <thread.h> |
| #include <token.h> |
| |
| using namespace CHERI; |
| |
| #ifdef SIMULATION |
| /** |
| * This is a special MMIO register. Writing an LSB of 1 terminates |
| * simulation. The upper 31 bits can pass extra metadata. We use all 0s |
| * to indicates success. |
| * |
| * This symbol doesn't need to be exported. The simulator is clever |
| * enough to find it even if it's local. |
| */ |
| volatile uint32_t tohost[2]; |
| |
| /** |
| * Exit simulation, reporting the error code given as the argument. |
| */ |
| void simulation_exit(uint32_t code) |
| { |
| // If this is using the standard RISC-V to-host mechanism, this will exit. |
| tohost[0] = 0x1 | (code << 1); |
| tohost[1] = 0; |
| // If we didn't exit with to-host, try writing a non-ASCII character to the |
| // UART. This is how we exit the CHERIoT Ibex simulator for the SAFE |
| // platform. |
| MMIO_CAPABILITY(Uart, uart)->blocking_write(0x80 + code); |
| } |
| |
| #endif |
| |
| /** |
| * The value of the cycle counter at the last scheduling event. |
| */ |
| static uint64_t cyclesAtLastSchedulingEvent; |
| |
| namespace |
| { |
| /** |
| * Priority-sorted list of threads waiting for a futex. |
| */ |
| Thread *futexWaitingList; |
| |
| /** |
| * The value used for priority-boosting futexes that are not actually |
| * boosting a thread currently. |
| */ |
| constexpr uint16_t FutexBoostNotThread = |
| std::numeric_limits<uint16_t>::max(); |
| |
| /** |
| * Returns the boosted priority provided by waiters on a futex. |
| * |
| * This finds the maximum priority of all threads that are priority |
| * boosting the thread identified by `threadID`. Callers may be about to |
| * add a new thread to that list and so another priority can be provided, |
| * which will be used if it is larger than any of the priorities of the |
| * other waiters. |
| */ |
| uint8_t priority_boost_for_thread(uint16_t threadID, uint8_t priority = 0) |
| { |
| Thread::walk_thread_list(futexWaitingList, [&](Thread *thread) { |
| if ((thread->futexPriorityInheriting) && |
| (thread->futexPriorityBoostedThread == threadID)) |
| { |
| priority = std::max(priority, thread->priority_get()); |
| } |
| }); |
| return priority; |
| } |
| |
| /** |
| * If a new futex_wait has come in with an updated owner for a lock, update |
| * all of the blocking threads to boost the new owner. |
| */ |
| void priority_boost_update(ptraddr_t key, uint16_t threadID) |
| { |
| Thread::walk_thread_list(futexWaitingList, [&](Thread *thread) { |
| if ((thread->futexPriorityInheriting) && |
| (thread->futexWaitAddress = key)) |
| { |
| thread->futexPriorityBoostedThread = threadID; |
| } |
| }); |
| } |
| |
| /** |
| * Reset the boosting thread for all threads waiting on the current futex |
| * to not boosting anything when the owning thread wakes. |
| * |
| * There is a potential race here because the `futex_wait` call happens |
| * after unlocking the futex. This means that another thread may come in |
| * and acquire a lock and set itself as the owner before the update. We |
| * therefore need to update waiting threads only if they are boosting the |
| * thread that called wake, not any other thread. |
| */ |
| void priority_boost_reset(ptraddr_t key, uint16_t threadID) |
| { |
| Thread::walk_thread_list(futexWaitingList, [&](Thread *thread) { |
| if ((thread->futexPriorityInheriting) && |
| (thread->futexWaitAddress = key)) |
| { |
| if (thread->futexPriorityBoostedThread == threadID) |
| { |
| thread->futexPriorityBoostedThread = FutexBoostNotThread; |
| } |
| } |
| }); |
| } |
| |
| /** |
| * Constant value used to represent an unbounded sleep. |
| */ |
| static constexpr auto UnboundedSleep = std::numeric_limits<uint32_t>::max(); |
| |
| /** |
| * Helper that wakes a set of up to `count` threads waiting on the futex |
| * whose address is given by the `key` parameter. |
| * |
| * The return values are: |
| * |
| * - Whether a higher-priority thread has been woken, which would trigger |
| * an immediate yield. |
| * - Whether this futex was using priority inheritance and so should be |
| * dropped back to the previous priority. |
| * - The number of sleeper that were awoken. |
| */ |
| std::tuple<bool, bool, int> |
| futex_wake(ptraddr_t key, |
| uint32_t count = std::numeric_limits<uint32_t>::max()) |
| { |
| bool shouldYield = false; |
| bool shouldRecalculatePriorityBoost = false; |
| // The number of threads that we've woken, this is the return value on |
| // success. |
| int woke = 0; |
| Thread::walk_thread_list( |
| futexWaitingList, |
| [&](Thread *thread) { |
| if (thread->futexWaitAddress == key) |
| { |
| shouldRecalculatePriorityBoost |= |
| thread->futexPriorityInheriting; |
| shouldYield = thread->ready(Thread::WakeReason::Futex); |
| count--; |
| woke++; |
| } |
| }, |
| [&]() { return count == 0; }); |
| |
| if (count > 0) |
| { |
| auto multiwaitersWoken = |
| MultiWaiterInternal::wake_waiters(key, count); |
| count -= multiwaitersWoken; |
| woke += multiwaitersWoken; |
| shouldYield |= (multiwaitersWoken > 0); |
| } |
| return {shouldYield, shouldRecalculatePriorityBoost, woke}; |
| } |
| |
| } // namespace |
| |
| namespace sched |
| { |
| using namespace priv; |
| |
| /** |
| * Reserved spaces for thread blocks and the event signaling for external |
| * interrupts. These will be used for in-place new on the first sched entry. |
| */ |
| using ThreadSpace = char[sizeof(Thread)]; |
| alignas(Thread) ThreadSpace threadSpaces[CONFIG_THREADS_NUM]; |
| |
| /** |
| * Return the thread pointer for the specified thread ID. |
| */ |
| Thread *get_thread(uint16_t threadId) |
| { |
| if (threadId > CONFIG_THREADS_NUM || threadId == 0) |
| { |
| return nullptr; |
| } |
| return &(reinterpret_cast<Thread *>(threadSpaces))[threadId - 1]; |
| } |
| |
| [[cheri::interrupt_state(disabled)]] void __cheri_compartment("sched") |
| scheduler_entry(const ThreadLoaderInfo *info) |
| { |
| Debug::Invariant(Capability{info}.length() == |
| sizeof(*info) * CONFIG_THREADS_NUM, |
| "Thread info is {} bytes, expected {} for {} threads", |
| Capability{info}.length(), |
| sizeof(*info) * CONFIG_THREADS_NUM, |
| CONFIG_THREADS_NUM); |
| |
| for (size_t i = 0; auto *threadSpace : threadSpaces) |
| { |
| Debug::log("Created thread for trusted stack {}", |
| info[i].trustedStack); |
| Thread *th = new (threadSpace) |
| Thread(info[i].trustedStack, i + 1, info[i].priority); |
| th->ready(Thread::WakeReason::Timer); |
| i++; |
| } |
| |
| InterruptController::master_init(); |
| Timer::interrupt_setup(); |
| } |
| |
| static void __dead2 sched_panic(size_t mcause, size_t mepc, size_t mtval) |
| { |
| size_t capcause = mtval & 0x1f; |
| size_t badcap = (mtval >> 5) & 0x3f; |
| Debug::log("CRASH! exception level {}, mcause {}, mepc {}, " |
| "capcause {}, badcap {}\n", |
| static_cast<uint32_t>(ExceptionGuard::exceptionLevel), |
| static_cast<uint32_t>(mcause), |
| static_cast<uint32_t>(mepc), |
| static_cast<uint32_t>(capcause), |
| badcap); |
| |
| // If we're in simulation, exit here |
| simulation_exit(1); |
| |
| for (;;) |
| { |
| wfi(); |
| } |
| } |
| |
| [[cheri::interrupt_state(disabled)]] TrustedStack * |
| __cheri_compartment("sched") exception_entry(TrustedStack *sealedTStack, |
| size_t mcause, |
| size_t mepc, |
| size_t mtval) |
| { |
| if constexpr (DebugScheduler) |
| { |
| /* Ensure that we got here from an IRQ-s deferred context */ |
| Capability returnAddress{__builtin_return_address(0)}; |
| Debug::Assert( |
| returnAddress.type() == CheriSealTypeReturnSentryDisabling, |
| "Scheduler exception_entry called from IRQ-enabled context"); |
| } |
| |
| // The cycle count value the last time the scheduler returned. |
| bool schedNeeded; |
| if constexpr (Accounting) |
| { |
| uint64_t currentCycles = rdcycle64(); |
| auto *thread = Thread::current_get(); |
| uint64_t &threadCycleCounter = |
| thread ? thread->cycles : Thread::idleThreadCycles; |
| auto elapsedCycles = currentCycles - cyclesAtLastSchedulingEvent; |
| threadCycleCounter += elapsedCycles; |
| } |
| |
| ExceptionGuard g{[=]() { sched_panic(mcause, mepc, mtval); }}; |
| |
| bool tick = false; |
| switch (mcause) |
| { |
| // Explicit yield call |
| case MCAUSE_ECALL_MACHINE: |
| { |
| schedNeeded = true; |
| Thread *currentThread = Thread::current_get(); |
| tick = currentThread && currentThread->is_ready(); |
| break; |
| } |
| case MCAUSE_INTR | MCAUSE_MTIME: |
| schedNeeded = true; |
| tick = true; |
| break; |
| case MCAUSE_INTR | MCAUSE_MEXTERN: |
| schedNeeded = false; |
| InterruptController::master().do_external_interrupt().and_then( |
| [&](uint32_t &word) { |
| // Increment the futex word so that anyone preempted on |
| // the way into the scheduler sleeping on its old value |
| // will still see this update. |
| word++; |
| // Wake anyone sleeping on this futex. Interrupt futexes |
| // are not priority inheriting. |
| std::tie(schedNeeded, std::ignore, std::ignore) = |
| futex_wake(Capability{&word}.address()); |
| }); |
| tick = schedNeeded; |
| break; |
| case MCAUSE_THREAD_EXIT: |
| // Make the current thread non-runnable. |
| if (Thread::exit()) |
| { |
| // If we have no threads left (not counting the idle |
| // thread), exit. |
| simulation_exit(0); |
| } |
| // We cannot continue exiting this thread, make sure we will |
| // pick a new one. |
| schedNeeded = true; |
| tick = true; |
| sealedTStack = nullptr; |
| break; |
| default: |
| sched_panic(mcause, mepc, mtval); |
| } |
| if (tick || !Thread::any_ready()) |
| { |
| Timer::expiretimers(); |
| } |
| auto newContext = |
| schedNeeded ? Thread::schedule(sealedTStack) : sealedTStack; |
| #if 0 |
| Debug::log("Thread: {}", |
| Thread::current_get() ? Thread::current_get()->id_get() : 0); |
| #endif |
| Timer::update(); |
| |
| if constexpr (Accounting) |
| { |
| cyclesAtLastSchedulingEvent = rdcycle64(); |
| } |
| return newContext; |
| } |
| |
| /** |
| * Helper template to dispatch an operation to a typed value. The first |
| * argument is a sealed capability provided by the caller. The second is a |
| * callable object that takes a reference to the unsealed object of the |
| * correct type. The return type of the lambda is either a single integer |
| * or a pair of an integer and a boolean. The integer value is simply |
| * returned. If the boolean is present then it is used to determine |
| * whether to yield at the end. |
| */ |
| template<typename T> |
| int typed_op(void *sealed, auto &&fn) |
| { |
| auto *unsealed = T::template unseal<T>(sealed); |
| // If we can't unseal the sealed capability and have it be of the |
| // correct type then return an error. |
| if (!unsealed) |
| { |
| return -EINVAL; |
| } |
| // Does the implementation return a simple `int`? If so, just tail call |
| // it. |
| if constexpr (std::is_same_v<decltype(fn(std::declval<T &>())), int>) |
| { |
| return fn(*unsealed); |
| } |
| else |
| { |
| auto [ret, shouldYield] = fn(*unsealed); |
| |
| if (shouldYield) |
| { |
| yield(); |
| } |
| |
| return ret; |
| } |
| } |
| |
| /// Lock used to serialise deallocations. |
| FlagLock deallocLock; |
| |
| /// Helper to safely deallocate an instance of `T`. |
| template<typename T> |
| int deallocate(SObjStruct *heapCapability, void *object) |
| { |
| // Acquire the lock and hold it. We need to be careful of two attempts |
| // to free the same object racing, so we cause others to back up behind |
| // this one. They will then fail in the unseal operation. |
| LockGuard g{deallocLock}; |
| return typed_op<T>(object, [&](T &unsealed) { |
| if (int ret = heap_can_free(heapCapability, &unsealed); ret != 0) |
| { |
| return ret; |
| } |
| unsealed.~T(); |
| heap_free(heapCapability, &unsealed); |
| return 0; |
| }); |
| } |
| |
| } // namespace sched |
| |
| using namespace sched; |
| |
| // thread APIs |
| SystickReturn __cheri_compartment("sched") thread_systemtick_get() |
| { |
| uint64_t ticks = Thread::ticksSinceBoot; |
| uint32_t hi = ticks >> 32; |
| uint32_t lo = ticks; |
| SystickReturn ret = {.lo = lo, .hi = hi}; |
| |
| return ret; |
| } |
| |
| __cheriot_minimum_stack(0x90) int __cheri_compartment("sched") |
| thread_sleep(Timeout *timeout, uint32_t flags) |
| { |
| STACK_CHECK(0x90); |
| if (!check_timeout_pointer(timeout)) |
| { |
| return -EINVAL; |
| } |
| // Debug::log("Thread {} sleeping for {} ticks", |
| // Thread::current_get()->id_get(), timeout->remaining); |
| Thread *current = Thread::current_get(); |
| current->suspend(timeout, nullptr, true, !(flags & ThreadSleepNoEarlyWake)); |
| return 0; |
| } |
| |
| __cheriot_minimum_stack(0xb0) int futex_timed_wait(Timeout *timeout, |
| const uint32_t *address, |
| uint32_t expected, |
| uint32_t flags) |
| { |
| STACK_CHECK(0xb0); |
| if (!check_timeout_pointer(timeout) || |
| !check_pointer<PermissionSet{Permission::Load}>(address)) |
| { |
| Debug::log("futex_timed_wait: invalid timeout or address"); |
| return -EINVAL; |
| } |
| // If the address does not contain the expected value then this call |
| // raced with an update in another thread, return success immediately. |
| if (*address != expected) |
| { |
| Debug::log("futex_timed_wait: skip wait {} != {}", *address, expected); |
| return 0; |
| } |
| Thread *currentThread = Thread::current_get(); |
| Debug::log("Thread {} waiting on futex {} for {} ticks", |
| currentThread->id_get(), |
| address, |
| timeout->remaining); |
| bool isPriorityInheriting = flags & FutexPriorityInheritance; |
| ptraddr_t key = Capability{address}.address(); |
| currentThread->futexWaitAddress = key; |
| currentThread->futexPriorityInheriting = isPriorityInheriting; |
| Thread *owningThread = nullptr; |
| uint16_t owningThreadID; |
| if (isPriorityInheriting) |
| { |
| // For PI futexes, the low 16 bits store the thread ID. |
| owningThreadID = *address; |
| owningThread = get_thread(owningThreadID); |
| // If we try to block ourself, that's a mistake. |
| if ((owningThread == currentThread) || (owningThread == nullptr)) |
| { |
| Debug::log("futex_timed_wait: thread {} acquiring PI futex with " |
| "invalid owning thread {}", |
| currentThread->id_get(), |
| owningThreadID); |
| return -EINVAL; |
| } |
| Debug::log("Thread {} boosting priority of {} for futex {}", |
| currentThread->id_get(), |
| owningThread->id_get(), |
| key); |
| // If other threads are boosting either the wrong thread or are |
| // priority boosting but haven't managed to acquire the lock, update |
| // their target. |
| priority_boost_update(key, owningThreadID); |
| owningThread->priority_boost(priority_boost_for_thread( |
| owningThreadID, currentThread->priority_get())); |
| } |
| currentThread->suspend(timeout, &futexWaitingList); |
| bool timedout = currentThread->futexWaitAddress == 0; |
| currentThread->futexWaitAddress = 0; |
| if (isPriorityInheriting) |
| { |
| Debug::log("Undoing priority boost of {} by {}", |
| owningThread->id_get(), |
| currentThread->id_get()); |
| // Recalculate the priority boost from the remaining waiters, if any. |
| owningThread->priority_boost(priority_boost_for_thread(owningThreadID)); |
| } |
| // If we woke up from a timer, report timeout. |
| if (timedout) |
| { |
| return -ETIMEDOUT; |
| } |
| // If the memory for the futex was deallocated out from under us, |
| // return an error. |
| if (!Capability{address}.is_valid()) |
| { |
| Debug::log( |
| "futex_timed_wait: futex address {} is invalid (deallocated?)", |
| address); |
| return -EINVAL; |
| } |
| Debug::log("Thread {} ({}) woke after waiting on futex {}", |
| currentThread->id_get(), |
| currentThread, |
| address); |
| return 0; |
| } |
| |
| __cheriot_minimum_stack(0xa0) int futex_wake(uint32_t *address, uint32_t count) |
| { |
| STACK_CHECK(0xa0); |
| if (!check_pointer<PermissionSet{Permission::Store}>(address)) |
| { |
| return -EINVAL; |
| } |
| ptraddr_t key = Capability{address}.address(); |
| |
| auto [shouldYield, shouldResetPrioirity, woke] = futex_wake(key, count); |
| |
| // If this futex wake is dropping a priority boost, reset the boost. |
| if (shouldResetPrioirity) |
| { |
| Thread *currentThread = Thread::current_get(); |
| // We are removing ourself from the priority boost from *this* futex, |
| // we may still be boosted by another futex, but we have just dropped |
| // the lock and so we should not be boosted so clear this thread as the |
| // target for other priority boosts. |
| priority_boost_reset(key, currentThread->id_get()); |
| // If we have nested priority-inheriting locks, we may have dropped the |
| // inner one but still hold the outer one. In this case, we need to |
| // keep the priority boost. Similarly, if we've done a notify-one |
| // operation but two threads were blocked on a priority-inheriting |
| // futex, then we need to keep the priority boost from the other |
| // threads. |
| currentThread->priority_boost( |
| priority_boost_for_thread(currentThread->id_get())); |
| // If we have dropped priority below that of another runnable thread, we |
| // should yield now. |
| shouldYield |= !currentThread->is_highest_priority(); |
| } |
| |
| if (shouldYield) |
| { |
| yield(); |
| } |
| |
| return woke; |
| } |
| |
| __cheriot_minimum_stack(0x60) int multiwaiter_create( |
| Timeout *timeout, |
| struct SObjStruct *heapCapability, |
| MultiWaiter **ret, |
| size_t maxItems) |
| { |
| STACK_CHECK(0x60); |
| int error; |
| // Don't bother checking if timeout is valid, the allocator will check for |
| // us. |
| auto mw = |
| MultiWaiterInternal::create(timeout, heapCapability, maxItems, error); |
| if (!mw) |
| { |
| return error; |
| } |
| |
| // This can trap, but only if the caller has provided a bad pointer. |
| // In this case, the caller can leak memory, but only memory allocated |
| // against its own quota. |
| *reinterpret_cast<void **>(ret) = mw; |
| |
| return 0; |
| } |
| |
| __cheriot_minimum_stack(0x70) int multiwaiter_delete( |
| struct SObjStruct *heapCapability, |
| MultiWaiter *mw) |
| { |
| STACK_CHECK(0x70); |
| return deallocate<MultiWaiterInternal>(heapCapability, mw); |
| } |
| |
| __cheriot_minimum_stack(0xc0) int multiwaiter_wait(Timeout *timeout, |
| MultiWaiter *waiter, |
| EventWaiterSource *events, |
| size_t newEventsCount) |
| { |
| STACK_CHECK(0xc0); |
| return typed_op<MultiWaiterInternal>(waiter, [&](MultiWaiterInternal &mw) { |
| if (newEventsCount > mw.capacity()) |
| { |
| Debug::log("Too many events"); |
| return -EINVAL; |
| } |
| // We don't need to worry about overflow here because we have ensured |
| // newEventsCount is very small. |
| if (!check_pointer<PermissionSet{Permission::Load, |
| Permission::Store, |
| Permission::LoadStoreCapability}>( |
| events, newEventsCount * sizeof(newEventsCount))) |
| { |
| Debug::log("Invalid new events pointer: {}", events); |
| return -EINVAL; |
| } |
| if (!check_timeout_pointer(timeout)) |
| { |
| return -EINVAL; |
| } |
| switch (mw.set_events(events, newEventsCount)) |
| { |
| case MultiWaiterInternal::EventOperationResult::Error: |
| Debug::log("Adding events returned error"); |
| return -EINVAL; |
| case MultiWaiterInternal::EventOperationResult::Sleep: |
| Debug::log("Sleeping for {} ticks", timeout->remaining); |
| if (timeout->may_block()) |
| { |
| mw.wait(timeout); |
| // If we yielded then it's possible for either of the |
| // pointers that we were passed to have been freed out from |
| // under us. |
| if (!Capability{&mw}.is_valid() || |
| !Capability{events}.is_valid()) |
| { |
| return -EINVAL; |
| } |
| } |
| [[fallthrough]]; |
| case MultiWaiterInternal::EventOperationResult::Wake: |
| // If we didn't find any events, then we timed out. We may |
| // still have timed out but received some events in between |
| // being rescheduled and being run, but don't count that as a |
| // timeout because it's not helpful to the user. |
| if (!mw.get_results(events, newEventsCount)) |
| { |
| return -ETIMEDOUT; |
| } |
| } |
| return 0; |
| }); |
| } |
| |
| namespace |
| { |
| /** |
| * An interrupt capability. |
| */ |
| struct InterruptCapability : Handle</*IsDynamic=*/false> |
| { |
| /** |
| * Sealing type used by `Handle`. |
| */ |
| static SKey sealing_type() |
| { |
| return STATIC_SEALING_TYPE(InterruptKey); |
| } |
| |
| /** |
| * The public structure state. |
| */ |
| InterruptCapabilityState state; |
| }; |
| } // namespace |
| |
| [[cheri::interrupt_state(disabled)]] __cheriot_minimum_stack( |
| 0x30) const uint32_t *interrupt_futex_get(struct SObjStruct *sealed) |
| { |
| STACK_CHECK(0x30); |
| auto *interruptCapability = |
| InterruptCapability::unseal<InterruptCapability>(sealed); |
| uint32_t *result = nullptr; |
| if (interruptCapability && interruptCapability->state.mayWait) |
| { |
| InterruptController::master() |
| .futex_word_for_source(interruptCapability->state.interruptNumber) |
| .and_then([&](uint32_t &word) { |
| Capability capability{&word}; |
| capability.permissions() &= |
| {Permission::Load, Permission::Global}; |
| result = capability.get(); |
| }); |
| } |
| return result; |
| } |
| |
| [[cheri::interrupt_state(disabled)]] __cheriot_minimum_stack( |
| 0x20) int interrupt_complete(struct SObjStruct *sealed) |
| { |
| STACK_CHECK(0x20); |
| auto *interruptCapability = |
| InterruptCapability::unseal<InterruptCapability>(sealed); |
| if (interruptCapability && interruptCapability->state.mayComplete) |
| { |
| InterruptController::master().interrupt_complete( |
| interruptCapability->state.interruptNumber); |
| return 0; |
| } |
| return -EPERM; |
| } |
| |
| uint16_t thread_count() |
| { |
| return CONFIG_THREADS_NUM; |
| } |
| |
| #ifdef SCHEDULER_ACCOUNTING |
| [[cheri::interrupt_state(disabled)]] uint64_t thread_elapsed_cycles_idle() |
| { |
| return Thread::idleThreadCycles; |
| } |
| |
| [[cheri::interrupt_state(disabled)]] uint64_t thread_elapsed_cycles_current() |
| { |
| // Calculate the number of cycles not yet reported to the current thread. |
| uint64_t currentCycles = rdcycle64(); |
| currentCycles -= cyclesAtLastSchedulingEvent; |
| // Report the number of cycles accounted to this thread, plus the number |
| // that have occurred in the current quantum. |
| return Thread::current_get()->cycles + currentCycles; |
| } |
| #endif |