// This file is part of the Essence operating system. // It is released under the terms of the MIT license -- see LICENSE.md. // Written by: nakst. // TODO Review vforking interaction from the POSIX subsystem with the process termination algorithm. // TODO Simplify or remove asynchronous task thread semantics. // TODO Break up or remove dispatchSpinlock. // How thread termination works: // 1. ThreadTerminate // - terminating is set to true. // - If the thread is executing, then on the next context switch, ThreadKill is called on a async task. // - If it is not executing, ThreadKill is called on a async task. // - Note, terminatableState must be set to THREAD_TERMINATABLE. // - When a thread terminates itself, its terminatableState is automatically set to THREAD_TERMINATABLE. // 2. ThreadKill // - Removes the thread from the lists, frees the stacks, and sets killedEvent. // - The thread's handles to itself and its process are closed. // - If this is the last thread in the process, ProcessKill is called. // 3. CloseHandleToObject KERNEL_OBJECT_THREAD // - If the last handle to the thread has been closed, then ThreadRemove is called. // 4. ThreadRemove // - The thread structure is deallocated. // How process termination works: // 1. ProcessTerminate (optional) // - preventNewThreads is set to true. // - ThreadTerminate is called on each thread, leading to an eventual call to ProcessKill. // - This is optional because ProcessKill is called if all threads get terminated naturally; in this case, preventNewThreads is never set. // 2. ProcessKill // - Removes the process from the lists, destroys the handle table and memory space, and sets killedEvent. // - Sends a message to Desktop informing it the process was killed. // - Since ProcessKill is only called from ThreadKill, there is an associated closing of a process handle from the killed thread. // 3. CloseHandleToObject KERNEL_OBJECT_PROCESS // - If the last handle to the process has been closed, then ProcessRemove is called. // 4. ProcessRemove // - Destroys the message queue, and closes the handle to the executable node. // - The process and memory space structures are deallocated. #ifndef IMPLEMENTATION #define THREAD_PRIORITY_NORMAL (0) // Lower value = higher priority. #define THREAD_PRIORITY_LOW (1) #define THREAD_PRIORITY_COUNT (2) enum ThreadState : int8_t { THREAD_ACTIVE, // An active thread. Not necessarily executing; `executing` determines if it executing. THREAD_WAITING_MUTEX, // Waiting for a mutex to be released. THREAD_WAITING_EVENT, // Waiting for a event to be notified. THREAD_WAITING_WRITER_LOCK, // Waiting for a writer lock to be notified. THREAD_TERMINATED, // The thread has been terminated. It will be deallocated when all handles are closed. }; enum ThreadType : int8_t { THREAD_NORMAL, // A normal thread. THREAD_IDLE, // The CPU's idle thread. THREAD_ASYNC_TASK, // A thread that processes the CPU's asynchronous tasks. }; enum ThreadTerminatableState : int8_t { THREAD_INVALID_TS, THREAD_TERMINATABLE, // The thread is currently executing user code. THREAD_IN_SYSCALL, // The thread is currently executing kernel code from a system call. // It cannot be terminated/paused until it returns from the system call. THREAD_USER_BLOCK_REQUEST, // The thread is sleeping because of a user system call to sleep. // It can be unblocked, and then terminated when it returns from the system call. }; struct Thread { // ** Must be the first item in the structure; see MMArchSafeCopy. ** bool inSafeCopy; LinkedItem item; // Entry in relevent thread queue or blockedThreads list for mutexes/writer locks. LinkedItem allItem; // Entry in the allThreads list. LinkedItem processItem; // Entry in the process's list of threads. struct Process *process; EsObjectID id; volatile uintptr_t cpuTimeSlices; volatile size_t handles; uint32_t executingProcessorID; uintptr_t userStackBase; uintptr_t kernelStackBase; uintptr_t kernelStack; size_t userStackReserve; volatile size_t userStackCommit; uintptr_t tlsAddress; uintptr_t timerAdjustAddress; uint64_t timerAdjustTicks; uint64_t lastInterruptTimeStamp; ThreadType type; bool isKernelThread, isPageGenerator; int8_t priority; int32_t blockedThreadPriorities[THREAD_PRIORITY_COUNT]; // The number of threads blocking on this thread at each priority level. volatile ThreadState state; volatile ThreadTerminatableState terminatableState; volatile bool executing; volatile bool terminating; // Set when a request to terminate the thread has been registered. volatile bool paused; // Set to pause a thread. Paused threads are not executed (unless the terminatableState prevents that). volatile bool receivedYieldIPI; // Used to terminate a thread executing on a different processor. union { KMutex *volatile mutex; struct { KWriterLock *volatile writerLock; bool writerLockType; }; struct { LinkedItem *eventItems; // Entries in the blockedThreads lists (one per event). KEvent *volatile events[ES_MAX_WAIT_COUNT]; volatile size_t eventCount; }; } blocking; KEvent killedEvent; KAsyncTask killAsyncTask; // If the type of the thread is THREAD_ASYNC_TASK, // then this is the virtual address space that should be loaded // when the task is being executed. MMSpace *volatile temporaryAddressSpace; InterruptContext *interruptContext; // TODO Store the userland interrupt context instead? uintptr_t lastKnownExecutionAddress; // For debugging. #ifdef ENABLE_POSIX_SUBSYSTEM struct POSIXThread *posixData; #endif const char *cName; }; enum ProcessType { PROCESS_NORMAL, PROCESS_KERNEL, PROCESS_DESKTOP, }; struct Process { MMSpace *vmm; HandleTable handleTable; MessageQueue messageQueue; LinkedList threads; KMutex threadsMutex; // Creation information: KNode *executableNode; char cExecutableName[ES_SNAPSHOT_MAX_PROCESS_NAME_LENGTH + 1]; EsProcessCreateData data; uint64_t permissions; uint32_t creationFlags; ProcessType type; // Object management: EsObjectID id; volatile size_t handles; LinkedItem allItem; // Crashing: KMutex crashMutex; EsCrashReason crashReason; bool crashed; #ifdef PAUSE_ON_USERLAND_CRASH // To allow for better remote debugging if PAUSE_ON_USERLAND_CRASH is defined, // when a process crashes it will return to where the crash occurred and enter an infinite loop. bool pausedFromCrash; #endif // Termination: bool allThreadsTerminated; bool blockShutdown; bool preventNewThreads; // Set by ProcessTerminate. int exitStatus; // TODO Remove this. KEvent killedEvent; // Executable state: uint8_t executableState; bool executableStartRequest; KEvent executableLoadAttemptComplete; Thread *executableMainThread; // Statistics: uintptr_t cpuTimeSlices, idleTimeSlices; // POSIX: #ifdef ENABLE_POSIX_SUBSYSTEM bool posixForking; int pgid; #endif }; struct Scheduler { void Yield(InterruptContext *context); void CreateProcessorThreads(CPULocalStorage *local); void AddActiveThread(Thread *thread, bool start /* put it at the start of the active list */); // Add an active thread into the queue. void MaybeUpdateActiveList(Thread *thread); // After changing the priority of a thread, call this to move it to the correct active thread queue if needed. void NotifyObject(LinkedList *blockedThreads, bool unblockAll, Thread *previousMutexOwner = nullptr); void UnblockThread(Thread *unblockedThread, Thread *previousMutexOwner = nullptr); Thread *PickThread(CPULocalStorage *local); // Pick the next thread to execute. int8_t GetThreadEffectivePriority(Thread *thread); KSpinlock dispatchSpinlock; // For accessing synchronisation objects, thread states, scheduling lists, etc. TODO Break this up! KSpinlock activeTimersSpinlock; // For accessing the activeTimers lists. LinkedList activeThreads[THREAD_PRIORITY_COUNT]; LinkedList pausedThreads; LinkedList activeTimers; KMutex allThreadsMutex; // For accessing the allThreads list. KMutex allProcessesMutex; // For accessing the allProcesses list. KSpinlock asyncTaskSpinlock; // For accessing the per-CPU asyncTaskList. LinkedList allThreads; LinkedList allProcesses; Pool threadPool, processPool, mmSpacePool; EsObjectID nextThreadID, nextProcessID, nextProcessorID; KEvent allProcessesTerminatedEvent; // Set during shutdown when all processes have been terminated. volatile uintptr_t blockShutdownProcessCount; volatile size_t activeProcessCount; volatile bool started, panic, shutdown; uint64_t timeMs; #ifdef DEBUG_BUILD EsThreadEventLogEntry *volatile threadEventLog; volatile uintptr_t threadEventLogPosition; volatile size_t threadEventLogAllocated; #endif }; Process *ProcessSpawn(ProcessType processType); void ProcessRemove(Process *process); void ProcessTerminate(Process *process, int status); void ThreadRemove(Thread *thread); void ThreadTerminate(Thread *thread); void ThreadSetTemporaryAddressSpace(MMSpace *space); #define SPAWN_THREAD_USERLAND (1 << 0) #define SPAWN_THREAD_LOW_PRIORITY (1 << 1) #define SPAWN_THREAD_PAUSED (1 << 2) #define SPAWN_THREAD_ASYNC_TASK (1 << 3) #define SPAWN_THREAD_IDLE (1 << 4) Thread *ThreadSpawn(const char *cName, uintptr_t startAddress, uintptr_t argument1 = 0, uint32_t flags = ES_FLAGS_DEFAULT, Process *process = nullptr, uintptr_t argument2 = 0); bool DesktopSendMessage(_EsMessageWithObject *message); EsHandle DesktopOpenHandle(void *object, uint32_t flags, KernelObjectType type); void DesktopCloseHandle(EsHandle handle); Process _kernelProcess; Process *kernelProcess = &_kernelProcess; Process *desktopProcess; KMutex desktopMutex; Scheduler scheduler; #endif #ifdef IMPLEMENTATION void KRegisterAsyncTask(KAsyncTask *task, KAsyncTaskCallback callback) { KSpinlockAcquire(&scheduler.asyncTaskSpinlock); if (!task->callback) { task->callback = callback; GetLocalStorage()->asyncTaskList.Insert(&task->item, false); } KSpinlockRelease(&scheduler.asyncTaskSpinlock); } int8_t Scheduler::GetThreadEffectivePriority(Thread *thread) { KSpinlockAssertLocked(&dispatchSpinlock); for (int8_t i = 0; i < thread->priority; i++) { if (thread->blockedThreadPriorities[i]) { // A thread is blocking on a resource owned by this thread, // and the blocking thread has a higher priority than this thread. // Therefore, this thread should assume that higher priority, // until it releases the resource. return i; } } return thread->priority; } void Scheduler::AddActiveThread(Thread *thread, bool start) { if (thread->type == THREAD_ASYNC_TASK) { // An asynchronous task thread was unblocked. // It will be run immediately, so there's no need to add it to the active thread list. return; } KSpinlockAssertLocked(&dispatchSpinlock); if (thread->state != THREAD_ACTIVE) { KernelPanic("Scheduler::AddActiveThread - Thread %d not active\n", thread->id); } else if (thread->executing) { KernelPanic("Scheduler::AddActiveThread - Thread %d executing\n", thread->id); } else if (thread->type != THREAD_NORMAL) { KernelPanic("Scheduler::AddActiveThread - Thread %d has type %d\n", thread->id, thread->type); } else if (thread->item.list) { KernelPanic("Scheduler::AddActiveThread - Thread %d is already in queue %x.\n", thread->id, thread->item.list); } if (thread->paused && thread->terminatableState == THREAD_TERMINATABLE) { // The thread is paused, so we can put it into the paused queue until it is resumed. pausedThreads.InsertStart(&thread->item); } else { int8_t effectivePriority = GetThreadEffectivePriority(thread); if (start) { activeThreads[effectivePriority].InsertStart(&thread->item); } else { activeThreads[effectivePriority].InsertEnd(&thread->item); } } } void Scheduler::MaybeUpdateActiveList(Thread *thread) { // TODO Is this correct with regards to paused threads? if (thread->type == THREAD_ASYNC_TASK) { // Asynchronous task threads do not go in the activeThreads lists. return; } if (thread->type != THREAD_NORMAL) { KernelPanic("Scheduler::MaybeUpdateActiveList - Trying to update the active list of a non-normal thread %x.\n", thread); } KSpinlockAssertLocked(&dispatchSpinlock); if (thread->state != THREAD_ACTIVE || thread->executing) { // The thread is not currently in an active list, // so it'll end up in the correct activeThreads list when it becomes active. return; } if (!thread->item.list) { KernelPanic("Scheduler::MaybeUpdateActiveList - Despite thread %x being active and not executing, it is not in an activeThreads lists.\n", thread); } int8_t effectivePriority = GetThreadEffectivePriority(thread); if (&activeThreads[effectivePriority] == thread->item.list) { // The thread's effective priority has not changed. // We don't need to do anything. return; } // Remove the thread from its previous active list. thread->item.RemoveFromList(); // Add it to the start of its new active list. // TODO I'm not 100% sure we want to always put it at the start. activeThreads[effectivePriority].InsertStart(&thread->item); } Thread *ThreadSpawn(const char *cName, uintptr_t startAddress, uintptr_t argument1, uint32_t flags, Process *process, uintptr_t argument2) { bool userland = flags & SPAWN_THREAD_USERLAND; Thread *parentThread = GetCurrentThread(); if (!process) { process = kernelProcess; } if (userland && process == kernelProcess) { KernelPanic("ThreadSpawn - Cannot add userland thread to kernel process.\n"); } // Adding the thread to the owner's list of threads and adding the thread to a scheduling list // need to be done in the same atomic block. KMutexAcquire(&process->threadsMutex); EsDefer(KMutexRelease(&process->threadsMutex)); if (process->preventNewThreads) { return nullptr; } Thread *thread = (Thread *) scheduler.threadPool.Add(sizeof(Thread)); if (!thread) return nullptr; KernelLog(LOG_INFO, "Scheduler", "spawn thread", "Created thread, %x to start at %x\n", thread, startAddress); // Allocate the thread's stacks. #if defined(ES_BITS_64) uintptr_t kernelStackSize = 0x5000 /* 20KB */; #elif defined(ES_BITS_32) uintptr_t kernelStackSize = 0x4000 /* 16KB */; #endif uintptr_t userStackReserve = userland ? 0x400000 /* 4MB */ : kernelStackSize; uintptr_t userStackCommit = userland ? 0x10000 /* 64KB */ : 0; uintptr_t stack = 0, kernelStack = 0; if (flags & SPAWN_THREAD_IDLE) goto skipStackAllocation; kernelStack = (uintptr_t) MMStandardAllocate(kernelMMSpace, kernelStackSize, MM_REGION_FIXED); if (!kernelStack) goto fail; if (userland) { stack = (uintptr_t) MMStandardAllocate(process->vmm, userStackReserve, ES_FLAGS_DEFAULT, nullptr, false); MMRegion *region = MMFindAndPinRegion(process->vmm, stack, userStackReserve); KMutexAcquire(&process->vmm->reserveMutex); #ifdef K_ARCH_STACK_GROWS_DOWN bool success = MMCommitRange(process->vmm, region, (userStackReserve - userStackCommit) / K_PAGE_SIZE, userStackCommit / K_PAGE_SIZE); #else bool success = MMCommitRange(process->vmm, region, 0, userStackCommit / K_PAGE_SIZE); #endif KMutexRelease(&process->vmm->reserveMutex); MMUnpinRegion(process->vmm, region); if (!success) goto fail; } else { stack = kernelStack; } if (!stack) goto fail; skipStackAllocation:; // If ProcessPause is called while a thread in that process is spawning a new thread, mark the thread as paused. // This is synchronized under the threadsMutex. thread->paused = (parentThread && process == parentThread->process && parentThread->paused) || (flags & SPAWN_THREAD_PAUSED); // 2 handles to the thread: // One for spawning the thread, // and the other for remaining during the thread's life. thread->handles = 2; thread->isKernelThread = !userland; thread->priority = (flags & SPAWN_THREAD_LOW_PRIORITY) ? THREAD_PRIORITY_LOW : THREAD_PRIORITY_NORMAL; thread->cName = cName; thread->kernelStackBase = kernelStack; thread->userStackBase = userland ? stack : 0; thread->userStackReserve = userStackReserve; thread->userStackCommit = userStackCommit; thread->terminatableState = userland ? THREAD_TERMINATABLE : THREAD_IN_SYSCALL; thread->type = (flags & SPAWN_THREAD_ASYNC_TASK) ? THREAD_ASYNC_TASK : (flags & SPAWN_THREAD_IDLE) ? THREAD_IDLE : THREAD_NORMAL; thread->id = __sync_fetch_and_add(&scheduler.nextThreadID, 1); thread->process = process; thread->item.thisItem = thread; thread->allItem.thisItem = thread; thread->processItem.thisItem = thread; if (thread->type != THREAD_IDLE) { thread->interruptContext = ArchInitialiseThread(kernelStack, kernelStackSize, thread, startAddress, argument1, argument2, userland, stack, userStackReserve); } else { thread->state = THREAD_ACTIVE; thread->executing = true; } process->threads.InsertEnd(&thread->processItem); KMutexAcquire(&scheduler.allThreadsMutex); scheduler.allThreads.InsertStart(&thread->allItem); KMutexRelease(&scheduler.allThreadsMutex); // Each thread owns a handles to the owner process. // This makes sure the process isn't destroyed before all its threads have been destroyed. OpenHandleToObject(process, KERNEL_OBJECT_PROCESS, ES_FLAGS_DEFAULT); KernelLog(LOG_INFO, "Scheduler", "thread stacks", "Spawning thread with stacks (k,u): %x->%x, %x->%x\n", kernelStack, kernelStack + kernelStackSize, stack, stack + userStackReserve); KernelLog(LOG_INFO, "Scheduler", "create thread", "Create thread ID %d, type %d, owner process %d\n", thread->id, thread->type, process->id); if (thread->type == THREAD_NORMAL) { // Add the thread to the start of the active thread list to make sure that it runs immediately. KSpinlockAcquire(&scheduler.dispatchSpinlock); scheduler.AddActiveThread(thread, true); KSpinlockRelease(&scheduler.dispatchSpinlock); } else { // Idle and asynchronous task threads don't need to be added to a scheduling list. } // The thread may now be terminated at any moment. return thread; fail:; if (stack) MMFree(process->vmm, (void *) stack); if (kernelStack) MMFree(kernelMMSpace, (void *) kernelStack); scheduler.threadPool.Remove(thread); return nullptr; } void ProcessKill(Process *process) { // This function should only be called by ThreadKill, when the last thread in the process exits. // There should be at least one remaining handle to the process here, owned by that thread. // It will be closed at the end of the ThreadKill function. if (!process->handles) { KernelPanic("ProcessKill - Process %x is on the allProcesses list but there are no handles to it.\n", process); } KernelLog(LOG_INFO, "Scheduler", "killing process", "Killing process (%d) %x...\n", process->id, process); __sync_fetch_and_sub(&scheduler.activeProcessCount, 1); // Remove the process from the list of processes. KMutexAcquire(&scheduler.allProcessesMutex); scheduler.allProcesses.Remove(&process->allItem); if (pmm.nextProcessToBalance == process) { // If the balance thread got interrupted while balancing this process, // start at the beginning of the next process. pmm.nextProcessToBalance = process->allItem.nextItem ? process->allItem.nextItem->thisItem : nullptr; pmm.nextRegionToBalance = nullptr; pmm.balanceResumePosition = 0; } KMutexRelease(&scheduler.allProcessesMutex); KSpinlockAcquire(&scheduler.dispatchSpinlock); process->allThreadsTerminated = true; bool setProcessKilledEvent = true; #ifdef ENABLE_POSIX_SUBSYSTEM if (process->posixForking) { // If the process is from an incomplete vfork(), // then the parent process gets to set the killed event // and the exit status. setProcessKilledEvent = false; } #endif KSpinlockRelease(&scheduler.dispatchSpinlock); if (setProcessKilledEvent) { // We can now also set the killed event on the process. KEventSet(&process->killedEvent, true); } // There are no threads left in this process. // We should destroy the handle table at this point. // Otherwise, the process might never be freed // because of a cyclic-dependency. process->handleTable.Destroy(); // Destroy the virtual memory space. // Don't actually deallocate it yet though; that is done on an async task queued by ProcessRemove. // This must be destroyed after the handle table! MMSpaceDestroy(process->vmm); } void ThreadKill(KAsyncTask *task) { Thread *thread = EsContainerOf(Thread, killAsyncTask, task); ThreadSetTemporaryAddressSpace(thread->process->vmm); KMutexAcquire(&scheduler.allThreadsMutex); scheduler.allThreads.Remove(&thread->allItem); KMutexRelease(&scheduler.allThreadsMutex); KMutexAcquire(&thread->process->threadsMutex); thread->process->threads.Remove(&thread->processItem); bool lastThread = thread->process->threads.count == 0; KMutexRelease(&thread->process->threadsMutex); KernelLog(LOG_INFO, "Scheduler", "killing thread", "Killing thread (ID %d, %d remain in process %d) %x...\n", thread->id, thread->process->threads.count, thread->process->id, thread); if (lastThread) { ProcessKill(thread->process); } MMFree(kernelMMSpace, (void *) thread->kernelStackBase); if (thread->userStackBase) MMFree(thread->process->vmm, (void *) thread->userStackBase); KEventSet(&thread->killedEvent); // Close the handle that this thread owns of its owner process, and the handle it owns of itself. CloseHandleToObject(thread->process, KERNEL_OBJECT_PROCESS); CloseHandleToObject(thread, KERNEL_OBJECT_THREAD); } void ProcessTerminate(Process *process, int status) { KMutexAcquire(&process->threadsMutex); KernelLog(LOG_INFO, "Scheduler", "terminate process", "Terminating process %d '%z' with status %i...\n", process->id, process->cExecutableName, status); process->exitStatus = status; process->preventNewThreads = true; Thread *currentThread = GetCurrentThread(); bool isCurrentProcess = process == currentThread->process; bool foundCurrentThread = false; LinkedItem *thread = process->threads.firstItem; while (thread) { Thread *threadObject = thread->thisItem; thread = thread->nextItem; if (threadObject != currentThread) { ThreadTerminate(threadObject); } else if (isCurrentProcess) { foundCurrentThread = true; } else { KernelPanic("Scheduler::ProcessTerminate - Found current thread in the wrong process?!\n"); } } KMutexRelease(&process->threadsMutex); if (!foundCurrentThread && isCurrentProcess) { KernelPanic("Scheduler::ProcessTerminate - Could not find current thread in the current process?!\n"); } else if (isCurrentProcess) { // This doesn't return. ThreadTerminate(currentThread); } } void ThreadTerminate(Thread *thread) { // Overview: // Set terminating true, and paused false. // Is this the current thread? // Mark as terminatable, then yield. // Else, is thread->terminating not set? // Set thread->terminating. // // Is this the current thread? // Mark as terminatable, then yield. // Else, are we executing user code? // If we aren't currently executing the thread, remove the thread from its scheduling queue and kill it. // Else, is the user waiting on a mutex/event? // If we aren't currently executing the thread, unblock the thread. KSpinlockAcquire(&scheduler.dispatchSpinlock); bool yield = false; if (thread->terminating) { KernelLog(LOG_INFO, "Scheduler", "thread already terminating", "Already terminating thread %d.\n", thread->id); if (thread == GetCurrentThread()) goto terminateThisThread; else goto done; } KernelLog(LOG_INFO, "Scheduler", "terminate thread", "Terminating thread %d...\n", thread->id); thread->terminating = true; thread->paused = false; if (thread == GetCurrentThread()) { terminateThisThread:; // Mark the thread as terminatable. thread->terminatableState = THREAD_TERMINATABLE; KSpinlockRelease(&scheduler.dispatchSpinlock); // We cannot return to the previous function as it expects to be killed. ProcessorFakeTimerInterrupt(); KernelPanic("Scheduler::ThreadTerminate - ProcessorFakeTimerInterrupt returned.\n"); } else { if (thread->terminatableState == THREAD_TERMINATABLE) { // We're in user code.. if (thread->executing) { // The thread is executing, so the next time it tries to make a system call or // is pre-empted, it will be terminated. } else { if (thread->state != THREAD_ACTIVE) { KernelPanic("Scheduler::ThreadTerminate - Terminatable thread non-active.\n"); } // The thread is terminatable and it isn't executing. // Remove it from its queue, and then remove the thread. thread->item.RemoveFromList(); KRegisterAsyncTask(&thread->killAsyncTask, ThreadKill); yield = true; } } else if (thread->terminatableState == THREAD_USER_BLOCK_REQUEST) { // We're in the kernel, but because the user wanted to block on a mutex/event. if (thread->executing) { // The mutex and event waiting code is designed to recognise when a thread is in this state, // and exit to the system call handler immediately. // If the thread however is pre-empted while in a blocked state before this code can execute, // Scheduler::Yield will automatically force the thread to be active again. } else { // Unblock the thread. // See comment above. if (thread->state == THREAD_WAITING_MUTEX || thread->state == THREAD_WAITING_EVENT) { scheduler.UnblockThread(thread); } } } else { // The thread is executing kernel code. // Therefore, we can't simply terminate the thread. // The thread will set its state to THREAD_TERMINATABLE whenever it can be terminated. } } done:; KSpinlockRelease(&scheduler.dispatchSpinlock); if (yield) ProcessorFakeTimerInterrupt(); // Process the asynchronous task. } void ProcessLoadExecutable() { Process *thisProcess = GetCurrentThread()->process; KernelLog(LOG_INFO, "Scheduler", "new process", "New process %d %x, '%z'.\n", thisProcess->id, thisProcess, thisProcess->cExecutableName); KLoadedExecutable api = {}; api.isDesktop = true; EsError loadError = ES_SUCCESS; { uint64_t fileFlags = ES_FILE_READ | ES_NODE_FAIL_IF_NOT_FOUND; KNodeInformation node = FSNodeOpen(EsLiteral(K_DESKTOP_EXECUTABLE), fileFlags); loadError = node.error; if (node.error == ES_SUCCESS) { if (node.node->directoryEntry->type != ES_NODE_FILE) { loadError = ES_ERROR_INCORRECT_NODE_TYPE; } else { loadError = KLoadELF(node.node, &api); } CloseHandleToObject(node.node, KERNEL_OBJECT_NODE, fileFlags); } else { KernelLog(LOG_ERROR, "Scheduler", "desktop executable open error", "The Desktop executable could not be opened.\n"); } } KLoadedExecutable application = {}; if (thisProcess->type != PROCESS_DESKTOP && loadError == ES_SUCCESS) { loadError = KLoadELF(thisProcess->executableNode, &application); if (loadError != ES_SUCCESS) { KernelLog(LOG_ERROR, "Scheduler", "executable load error", "The executable could not be loaded by the ELF loader.\n"); } } bool success = loadError == ES_SUCCESS; if (success) { // We "link" the API by putting its table of function pointers at a known address. // TODO Write protection. if (!MMMapShared(thisProcess->vmm, mmAPITableRegion, 0, mmAPITableRegion->sizeBytes, ES_FLAGS_DEFAULT, ES_API_BASE)) { success = false; KernelLog(LOG_ERROR, "Scheduler", "executable load error", "The API table could not be mapped.\n"); } } EsProcessStartupInformation *startupInformation; if (success) { startupInformation = (EsProcessStartupInformation *) MMStandardAllocate( thisProcess->vmm, sizeof(EsProcessStartupInformation), ES_FLAGS_DEFAULT); if (!startupInformation) { success = false; KernelLog(LOG_ERROR, "Scheduler", "executable load error", "The process startup information could not be allocated.\n"); } else { startupInformation->isDesktop = thisProcess->type == PROCESS_DESKTOP; startupInformation->isBundle = application.isBundle; startupInformation->applicationStartAddress = application.startAddress; startupInformation->tlsImageStart = application.tlsImageStart; startupInformation->tlsImageBytes = application.tlsImageBytes; startupInformation->tlsBytes = application.tlsBytes; startupInformation->timeStampTicksPerMs = timeStampTicksPerMs; uint32_t globalDataRegionFlags = thisProcess->type == PROCESS_DESKTOP ? ES_SHARED_MEMORY_READ_WRITE : ES_FLAGS_DEFAULT; if (OpenHandleToObject(mmGlobalDataRegion, KERNEL_OBJECT_SHMEM, globalDataRegionFlags)) { startupInformation->globalDataRegion = thisProcess->handleTable.OpenHandle(mmGlobalDataRegion, globalDataRegionFlags, KERNEL_OBJECT_SHMEM); } EsMemoryCopy(&startupInformation->data, &thisProcess->data, sizeof(EsProcessCreateData)); } } if (success) { uint64_t threadFlags = SPAWN_THREAD_USERLAND; if (thisProcess->creationFlags & ES_PROCESS_CREATE_PAUSED) threadFlags |= SPAWN_THREAD_PAUSED; thisProcess->executableState = ES_PROCESS_EXECUTABLE_LOADED; thisProcess->executableMainThread = ThreadSpawn("MainThread", api.startAddress, (uintptr_t) startupInformation, threadFlags, thisProcess); if (!thisProcess->executableMainThread) { success = false; KernelLog(LOG_ERROR, "Scheduler", "executable load error", "The main thread could not be spawned.\n"); } } if (!success) { if (thisProcess->type != PROCESS_NORMAL) { KernelPanic("ProcessLoadExecutable - Failed to start the critical process %z.\n", thisProcess->cExecutableName); } thisProcess->executableState = ES_PROCESS_EXECUTABLE_FAILED_TO_LOAD; } KEventSet(&thisProcess->executableLoadAttemptComplete); } bool ProcessStartWithNode(Process *process, KNode *node) { // Make sure nobody has tried to start the process. KSpinlockAcquire(&scheduler.dispatchSpinlock); if (process->executableStartRequest) { KSpinlockRelease(&scheduler.dispatchSpinlock); return false; } process->executableStartRequest = true; KSpinlockRelease(&scheduler.dispatchSpinlock); // Get the name of the process from the node. KWriterLockTake(&node->writerLock, K_LOCK_SHARED); size_t byteCount = node->directoryEntry->item.key.longKeyBytes; if (byteCount > ES_SNAPSHOT_MAX_PROCESS_NAME_LENGTH) byteCount = ES_SNAPSHOT_MAX_PROCESS_NAME_LENGTH; EsMemoryCopy(process->cExecutableName, node->directoryEntry->item.key.longKey, byteCount); process->cExecutableName[byteCount] = 0; KWriterLockReturn(&node->writerLock, K_LOCK_SHARED); // Initialise the memory space. bool success = MMSpaceInitialise(process->vmm); if (!success) return false; // NOTE If you change these flags, make sure to update the flags when the handle is closed! if (!OpenHandleToObject(node, KERNEL_OBJECT_NODE, ES_FILE_READ)) { KernelPanic("ProcessStartWithNode - Could not open read handle to node %x.\n", node); } if (KEventPoll(&scheduler.allProcessesTerminatedEvent)) { KernelPanic("ProcessStartWithNode - allProcessesTerminatedEvent was set.\n"); } process->executableNode = node; process->blockShutdown = true; __sync_fetch_and_add(&scheduler.activeProcessCount, 1); __sync_fetch_and_add(&scheduler.blockShutdownProcessCount, 1); // Add the process to the list of all processes, // and spawn the kernel thread to load the executable. // This is synchronized under allProcessesMutex so that the process can't be terminated or paused // until loadExecutableThread has been spawned. KMutexAcquire(&scheduler.allProcessesMutex); scheduler.allProcesses.InsertEnd(&process->allItem); Thread *loadExecutableThread = ThreadSpawn("ExecLoad", (uintptr_t) ProcessLoadExecutable, 0, ES_FLAGS_DEFAULT, process); KMutexRelease(&scheduler.allProcessesMutex); if (!loadExecutableThread) { CloseHandleToObject(process, KERNEL_OBJECT_PROCESS); return false; } // Wait for the executable to be loaded. CloseHandleToObject(loadExecutableThread, KERNEL_OBJECT_THREAD); KEventWait(&process->executableLoadAttemptComplete, ES_WAIT_NO_TIMEOUT); if (process->executableState == ES_PROCESS_EXECUTABLE_FAILED_TO_LOAD) { KernelLog(LOG_ERROR, "Scheduler", "executable load failure", "Executable failed to load.\n"); return false; } return true; } bool ProcessStart(Process *process, char *imagePath, size_t imagePathLength, KNode *baseDirectory = nullptr) { uint64_t flags = ES_FILE_READ | ES_NODE_FAIL_IF_NOT_FOUND; KNodeInformation node = FSNodeOpen(imagePath, imagePathLength, flags, baseDirectory); bool result = false; if (!ES_CHECK_ERROR(node.error)) { if (node.node->directoryEntry->type == ES_NODE_FILE) { result = ProcessStartWithNode(process, node.node); } CloseHandleToObject(node.node, KERNEL_OBJECT_NODE, flags); } if (!result && process->type == PROCESS_DESKTOP) { KernelPanic("ProcessStart - Could not load the desktop executable.\n"); } return result; } Process *ProcessSpawn(ProcessType processType) { if (scheduler.shutdown) return nullptr; Process *process = processType == PROCESS_KERNEL ? kernelProcess : (Process *) scheduler.processPool.Add(sizeof(Process)); if (!process) { return nullptr; } process->vmm = processType == PROCESS_KERNEL ? kernelMMSpace : (MMSpace *) scheduler.mmSpacePool.Add(sizeof(MMSpace)); if (!process->vmm) { scheduler.processPool.Remove(process); return nullptr; } process->id = __sync_fetch_and_add(&scheduler.nextProcessID, 1); process->vmm->referenceCount = 1; process->allItem.thisItem = process; process->handles = 1; process->handleTable.process = process; process->permissions = ES_PERMISSION_ALL; process->type = processType; if (processType == PROCESS_KERNEL) { EsCRTstrcpy(process->cExecutableName, "Kernel"); scheduler.allProcesses.InsertEnd(&process->allItem); } return process; } void ThreadSetTemporaryAddressSpace(MMSpace *space) { KSpinlockAcquire(&scheduler.dispatchSpinlock); Thread *thread = GetCurrentThread(); MMSpace *oldSpace = thread->temporaryAddressSpace ?: kernelMMSpace; thread->temporaryAddressSpace = space; MMSpace *newSpace = space ?: kernelMMSpace; MMSpaceOpenReference(newSpace); // Open our reference to the space. MMSpaceOpenReference(newSpace); // This reference will be closed in PostContextSwitch, simulating the reference opened in Scheduler::Yield. ProcessorSetAddressSpace(&newSpace->data); KSpinlockRelease(&scheduler.dispatchSpinlock); MMSpaceCloseReference(oldSpace); // Close our reference to the space. MMSpaceCloseReference(oldSpace); // This reference was opened by Scheduler::Yield, and would have been closed in PostContextSwitch. } void AsyncTaskThread() { CPULocalStorage *local = GetLocalStorage(); while (true) { if (!local->asyncTaskList.first) { ProcessorFakeTimerInterrupt(); } else { KSpinlockAcquire(&scheduler.asyncTaskSpinlock); SimpleList *item = local->asyncTaskList.first; KAsyncTask *task = EsContainerOf(KAsyncTask, item, item); KAsyncTaskCallback callback = task->callback; task->callback = nullptr; local->inAsyncTask = true; item->Remove(); KSpinlockRelease(&scheduler.asyncTaskSpinlock); callback(task); // This may cause the task to be deallocated. ThreadSetTemporaryAddressSpace(nullptr); // The task may have modified the address space. local->inAsyncTask = false; } } } void Scheduler::CreateProcessorThreads(CPULocalStorage *local) { local->asyncTaskThread = ThreadSpawn("AsyncTasks", (uintptr_t) AsyncTaskThread, 0, SPAWN_THREAD_ASYNC_TASK); local->currentThread = local->idleThread = ThreadSpawn("Idle", 0, 0, SPAWN_THREAD_IDLE); local->processorID = __sync_fetch_and_add(&nextProcessorID, 1); if (local->processorID >= K_MAX_PROCESSORS) { KernelPanic("Scheduler::CreateProcessorThreads - Maximum processor count (%d) exceeded.\n", local->processorID); } } void ProcessRemove(Process *process) { KernelLog(LOG_INFO, "Scheduler", "remove process", "Removing process %d.\n", process->id); if (process->executableNode) { // Close the handle to the executable node. CloseHandleToObject(process->executableNode, KERNEL_OBJECT_NODE, ES_FILE_READ); process->executableNode = nullptr; } // Destroy the process's handle table, if it hasn't already been destroyed. // For most processes, the handle table is destroyed when the last thread terminates. process->handleTable.Destroy(); // Free all the remaining messages in the message queue. // This is done after closing all handles, since closing handles can generate messages. process->messageQueue.messages.Free(); if (process->blockShutdown) { if (1 == __sync_fetch_and_sub(&scheduler.blockShutdownProcessCount, 1)) { // If this is the last process to exit, set the allProcessesTerminatedEvent. KEventSet(&scheduler.allProcessesTerminatedEvent); } } // Free the process. MMSpaceCloseReference(process->vmm); scheduler.processPool.Remove(process); } void ThreadRemove(Thread *thread) { KernelLog(LOG_INFO, "Scheduler", "remove thread", "Removing thread %d.\n", thread->id); // The last handle to the thread has been closed, // so we can finally deallocate the thread. #ifdef ENABLE_POSIX_SUBSYSTEM if (thread->posixData) { if (thread->posixData->forkStack) { EsHeapFree(thread->posixData->forkStack, thread->posixData->forkStackSize, K_PAGED); CloseHandleToObject(thread->posixData->forkProcess, KERNEL_OBJECT_PROCESS); } EsHeapFree(thread->posixData, sizeof(POSIXThread), K_PAGED); } #endif scheduler.threadPool.Remove(thread); } void ThreadPause(Thread *thread, bool resume) { KSpinlockAcquire(&scheduler.dispatchSpinlock); if (thread->paused == !resume) { return; } thread->paused = !resume; if (!resume && thread->terminatableState == THREAD_TERMINATABLE) { if (thread->state == THREAD_ACTIVE) { if (thread->executing) { if (thread == GetCurrentThread()) { KSpinlockRelease(&scheduler.dispatchSpinlock); // Yield. ProcessorFakeTimerInterrupt(); if (thread->paused) { KernelPanic("ThreadPause - Current thread incorrectly resumed.\n"); } } else { // The thread is executing, but on a different processor. // Send them an IPI to stop. ProcessorSendYieldIPI(thread); // TODO The interrupt context might not be set at this point. } } else { // Remove the thread from the active queue, and put it into the paused queue. thread->item.RemoveFromList(); scheduler.AddActiveThread(thread, false); } } else { // The thread doesn't need to be in the paused queue as it won't run anyway. // If it is unblocked, then AddActiveThread will put it into the correct queue. } } else if (resume && thread->item.list == &scheduler.pausedThreads) { // Remove the thread from the paused queue, and put it into the active queue. scheduler.pausedThreads.Remove(&thread->item); scheduler.AddActiveThread(thread, false); } KSpinlockRelease(&scheduler.dispatchSpinlock); } void ProcessPause(Process *process, bool resume) { KMutexAcquire(&process->threadsMutex); LinkedItem *thread = process->threads.firstItem; while (thread) { Thread *threadObject = thread->thisItem; thread = thread->nextItem; ThreadPause(threadObject, resume); } KMutexRelease(&process->threadsMutex); } void ProcessCrash(Process *process, EsCrashReason *crashReason) { EsAssert(process == GetCurrentThread()->process); // TODO Test this function when crashing other processes! if (process == kernelProcess) { KernelPanic("ProcessCrash - Kernel process has crashed (%d).\n", crashReason->errorCode); } if (process->type != PROCESS_NORMAL) { KernelPanic("ProcessCrash - A critical process has crashed (%d).\n", crashReason->errorCode); } bool pauseProcess = false; KMutexAcquire(&process->crashMutex); if (!process->crashed) { process->crashed = true; pauseProcess = true; KernelLog(LOG_ERROR, "Scheduler", "process crashed", "Process %x has crashed! (%d)\n", process, crashReason->errorCode); EsMemoryCopy(&process->crashReason, crashReason, sizeof(EsCrashReason)); if (!scheduler.shutdown) { _EsMessageWithObject m; EsMemoryZero(&m, sizeof(m)); m.message.type = ES_MSG_APPLICATION_CRASH; m.message.crash.pid = process->id; EsMemoryCopy(&m.message.crash.reason, crashReason, sizeof(EsCrashReason)); DesktopSendMessage(&m); } } KMutexRelease(&process->crashMutex); if (pauseProcess) { // TODO Shouldn't this be done before sending the desktop message? ProcessPause(process, false); } } Thread *Scheduler::PickThread(CPULocalStorage *local) { KSpinlockAssertLocked(&dispatchSpinlock); if ((local->asyncTaskList.first || local->inAsyncTask) && local->asyncTaskThread->state == THREAD_ACTIVE) { // If the asynchronous task thread for this processor isn't blocked, and has tasks to process, execute it. return local->asyncTaskThread; } for (int i = 0; i < THREAD_PRIORITY_COUNT; i++) { // For every priority, check if there is a thread available. If so, execute it. LinkedItem *item = activeThreads[i].firstItem; if (!item) continue; item->RemoveFromList(); return item->thisItem; } // If we couldn't find a thread to execute, idle. return local->idleThread; } void Scheduler::Yield(InterruptContext *context) { CPULocalStorage *local = GetLocalStorage(); if (!started || !local || !local->schedulerReady) { return; } if (!local->processorID) { // Update the scheduler's time. timeMs = ArchGetTimeMs(); mmGlobalData->schedulerTimeMs = timeMs; // Notify the necessary timers. KSpinlockAcquire(&activeTimersSpinlock); LinkedItem *_timer = activeTimers.firstItem; while (_timer) { KTimer *timer = _timer->thisItem; LinkedItem *next = _timer->nextItem; if (timer->triggerTimeMs <= timeMs) { activeTimers.Remove(_timer); KEventSet(&timer->event); if (timer->callback) { KRegisterAsyncTask(&timer->asyncTask, timer->callback); } } else { break; // Timers are kept sorted, so there's no point continuing. } _timer = next; } KSpinlockRelease(&activeTimersSpinlock); } if (local->spinlockCount) { KernelPanic("Scheduler::Yield - Spinlocks acquired while attempting to yield.\n"); } ProcessorDisableInterrupts(); // We don't want interrupts to get reenabled after the context switch. KSpinlockAcquire(&dispatchSpinlock); if (dispatchSpinlock.interruptsEnabled) { KernelPanic("Scheduler::Yield - Interrupts were enabled when scheduler lock was acquired.\n"); } if (!local->currentThread->executing) { KernelPanic("Scheduler::Yield - Current thread %x marked as not executing (%x).\n", local->currentThread, local); } MMSpace *oldAddressSpace = local->currentThread->temporaryAddressSpace ?: local->currentThread->process->vmm; local->currentThread->interruptContext = context; local->currentThread->executing = false; bool killThread = local->currentThread->terminatableState == THREAD_TERMINATABLE && local->currentThread->terminating; bool keepThreadAlive = local->currentThread->terminatableState == THREAD_USER_BLOCK_REQUEST && local->currentThread->terminating; // The user can't make the thread block if it is terminating. if (killThread) { local->currentThread->state = THREAD_TERMINATED; KernelLog(LOG_INFO, "Scheduler", "terminate yielded thread", "Terminated yielded thread %x\n", local->currentThread); KRegisterAsyncTask(&local->currentThread->killAsyncTask, ThreadKill); } // If the thread is waiting for an object to be notified, put it in the relevant blockedThreads list. // But if the object has been notified yet hasn't made itself active yet, do that for it. else if (local->currentThread->state == THREAD_WAITING_MUTEX) { KMutex *mutex = local->currentThread->blocking.mutex; if (!keepThreadAlive && mutex->owner) { mutex->owner->blockedThreadPriorities[local->currentThread->priority]++; MaybeUpdateActiveList(mutex->owner); mutex->blockedThreads.InsertEnd(&local->currentThread->item); } else { local->currentThread->state = THREAD_ACTIVE; } } else if (local->currentThread->state == THREAD_WAITING_EVENT) { if (keepThreadAlive) { local->currentThread->state = THREAD_ACTIVE; } else { bool unblocked = false; for (uintptr_t i = 0; i < local->currentThread->blocking.eventCount; i++) { if (local->currentThread->blocking.events[i]->state) { local->currentThread->state = THREAD_ACTIVE; unblocked = true; break; } } if (!unblocked) { for (uintptr_t i = 0; i < local->currentThread->blocking.eventCount; i++) { local->currentThread->blocking.events[i]->blockedThreads.InsertEnd(&local->currentThread->blocking.eventItems[i]); } } } } else if (local->currentThread->state == THREAD_WAITING_WRITER_LOCK) { KWriterLock *lock = local->currentThread->blocking.writerLock; if ((local->currentThread->blocking.writerLockType == K_LOCK_SHARED && lock->state >= 0) || (local->currentThread->blocking.writerLockType == K_LOCK_EXCLUSIVE && lock->state == 0)) { local->currentThread->state = THREAD_ACTIVE; } else { local->currentThread->blocking.writerLock->blockedThreads.InsertEnd(&local->currentThread->item); } } // Put the current thread at the end of the activeThreads list. if (!killThread && local->currentThread->state == THREAD_ACTIVE) { if (local->currentThread->type == THREAD_NORMAL) { AddActiveThread(local->currentThread, false); } else if (local->currentThread->type == THREAD_IDLE || local->currentThread->type == THREAD_ASYNC_TASK) { // Do nothing. } else { KernelPanic("Scheduler::Yield - Unrecognised thread type\n"); } } // Get the next thread to execute. Thread *newThread = local->currentThread = PickThread(local); if (!newThread) { KernelPanic("Scheduler::Yield - Could not find a thread to execute.\n"); } if (newThread->executing) { KernelPanic("Scheduler::Yield - Thread (ID %d) in active queue already executing with state %d, type %d.\n", local->currentThread->id, local->currentThread->state, local->currentThread->type); } // Store information about the thread. newThread->executing = true; newThread->executingProcessorID = local->processorID; newThread->cpuTimeSlices++; if (newThread->type == THREAD_IDLE) newThread->process->idleTimeSlices++; else newThread->process->cpuTimeSlices++; // Prepare the next timer interrupt. ArchNextTimer(1 /* ms */); InterruptContext *newContext = newThread->interruptContext; MMSpace *addressSpace = newThread->temporaryAddressSpace ?: newThread->process->vmm; MMSpaceOpenReference(addressSpace); ArchSwitchContext(newContext, &addressSpace->data, newThread->kernelStack, newThread, oldAddressSpace); KernelPanic("Scheduler::Yield - DoContextSwitch unexpectedly returned.\n"); } void ProcessTerminateAll() { KMutexAcquire(&desktopMutex); scheduler.shutdown = true; // Close our handle to the desktop process. CloseHandleToObject(desktopProcess->executableMainThread, KERNEL_OBJECT_THREAD); CloseHandleToObject(desktopProcess, KERNEL_OBJECT_PROCESS); desktopProcess = nullptr; KMutexRelease(&desktopMutex); KernelLog(LOG_INFO, "Scheduler", "terminating all processes", "ProcessTerminateAll - Terminating all processes....\n"); while (true) { KMutexAcquire(&scheduler.allProcessesMutex); Process *process = scheduler.allProcesses.firstItem->thisItem; while (process && (process->preventNewThreads || process == kernelProcess)) { LinkedItem *item = process->allItem.nextItem; process = item ? item->thisItem : nullptr; } KMutexRelease(&scheduler.allProcessesMutex); if (!process) break; ProcessTerminate(process, -1); } KEventWait(&scheduler.allProcessesTerminatedEvent); } Process *ProcessOpen(uint64_t id) { KMutexAcquire(&scheduler.allProcessesMutex); LinkedItem *item = scheduler.allProcesses.firstItem; Process *result = nullptr; while (item) { Process *process = item->thisItem; if (process->id == id && process->type != PROCESS_KERNEL /* the kernel process cannot be opened */) { OpenHandleToObject(process, KERNEL_OBJECT_PROCESS, ES_FLAGS_DEFAULT); result = item->thisItem; break; } item = item->nextItem; } KMutexRelease(&scheduler.allProcessesMutex); return result; } bool KThreadCreate(const char *cName, void (*startAddress)(uintptr_t), uintptr_t argument) { return ThreadSpawn(cName, (uintptr_t) startAddress, argument) ? true : false; } void KThreadTerminate() { ThreadTerminate(GetCurrentThread()); } void KYield() { ProcessorFakeTimerInterrupt(); } bool DesktopSendMessage(_EsMessageWithObject *message) { bool result = false; KMutexAcquire(&desktopMutex); if (desktopProcess) result = desktopProcess->messageQueue.SendMessage(message); KMutexRelease(&desktopMutex); return result; } EsHandle DesktopOpenHandle(void *object, uint32_t flags, KernelObjectType type) { EsHandle result = ES_INVALID_HANDLE; bool close = false; KMutexAcquire(&desktopMutex); if (desktopProcess) result = desktopProcess->handleTable.OpenHandle(object, flags, type); else close = true; KMutexRelease(&desktopMutex); if (close) CloseHandleToObject(object, type, flags); return result; } void DesktopCloseHandle(EsHandle handle) { KMutexAcquire(&desktopMutex); if (desktopProcess) desktopProcess->handleTable.CloseHandle(handle); // This will check that the handle is still valid. KMutexRelease(&desktopMutex); } uint64_t KCPUCurrentID() { return GetLocalStorage() ->processorID; } uint64_t KProcessCurrentID() { return GetCurrentThread()->process->id; } uint64_t KThreadCurrentID() { return GetCurrentThread() ->id; } #endif